|
1 | 1 | import multiprocessing |
| 2 | +import time |
2 | 3 | import psutil |
3 | 4 | import traceback |
4 | 5 | from typing import List, Any |
5 | 6 | from classes.ProcessMonitorHook import ProcessMonitorHook, ProcessMetricSnapshot |
6 | 7 |
|
| 8 | +_PRECOMPUTE_THREAD_PREFIX = "pc-worker" |
| 9 | + |
| 10 | + |
| 11 | +def _read_thread_cpu(pid: int) -> dict: |
| 12 | + """ |
| 13 | + Returns {tid: (thread_name, cpu_seconds)} for all threads of pid. |
| 14 | + cpu_seconds = user_time + system_time from psutil; name from /proc/[pid]/task/[tid]/comm. |
| 15 | + Silently skips threads that disappear mid-read. |
| 16 | + """ |
| 17 | + result = {} |
| 18 | + try: |
| 19 | + threads = psutil.Process(pid).threads() |
| 20 | + except (psutil.NoSuchProcess, psutil.AccessDenied): |
| 21 | + return result |
| 22 | + for t in threads: |
| 23 | + try: |
| 24 | + with open(f"/proc/{pid}/task/{t.id}/comm") as f: |
| 25 | + name = f.read().strip() |
| 26 | + result[t.id] = (name, t.user_time + t.system_time) |
| 27 | + except (FileNotFoundError, psutil.NoSuchProcess): |
| 28 | + pass |
| 29 | + return result |
| 30 | + |
7 | 31 |
|
8 | 32 | class MyMonitor(multiprocessing.Process): |
9 | 33 | def __init__( |
@@ -34,11 +58,20 @@ def __init__( |
34 | 58 | self.pid_monitor_map[pid] = {m: [] for m in self.monitors} |
35 | 59 | self.pid_monitor_map[pid]["keyword"] = keyword |
36 | 60 |
|
| 61 | + self._prev_thread_jiffies: dict = {} |
| 62 | + self._prev_poll_monotonic: float = 0.0 |
| 63 | + |
| 64 | + for pid in self.pids_to_monitor: |
| 65 | + self.pid_monitor_map[pid]["precompute_cpu_percent"] = [] |
| 66 | + self.pid_monitor_map[pid]["query_cpu_percent"] = [] |
| 67 | + |
37 | 68 | def add_child_pid_to_map(self, pid, child_pid): |
38 | 69 | self.pid_monitor_map[child_pid] = {m: [] for m in self.monitors} |
39 | 70 | self.pid_monitor_map[child_pid]["keyword"] = self.pid_monitor_map[pid][ |
40 | 71 | "keyword" |
41 | 72 | ] |
| 73 | + self.pid_monitor_map[child_pid]["precompute_cpu_percent"] = [] |
| 74 | + self.pid_monitor_map[child_pid]["query_cpu_percent"] = [] |
42 | 75 |
|
43 | 76 | def init_hooks(self): |
44 | 77 | """ |
@@ -70,6 +103,45 @@ def close_hooks(self): |
70 | 103 | hook.close() |
71 | 104 | return |
72 | 105 |
|
| 106 | + def _compute_thread_group_cpu(self, pid: int, elapsed: float): |
| 107 | + """ |
| 108 | + Reads current per-thread CPU seconds for pid, diffs against previous snapshot, |
| 109 | + and appends precompute_cpu_percent / query_cpu_percent to pid_monitor_map. |
| 110 | +
|
| 111 | + CPU% is on the same scale as psutil's cpu_percent: can exceed 100% on |
| 112 | + multi-core systems (e.g. 2 fully loaded cores → ~200%). |
| 113 | + """ |
| 114 | + current = _read_thread_cpu(pid) |
| 115 | + prev = self._prev_thread_jiffies.get(pid, {}) |
| 116 | + |
| 117 | + if not prev: |
| 118 | + self._prev_thread_jiffies[pid] = current |
| 119 | + self.pid_monitor_map[pid]["precompute_cpu_percent"].append(0.0) |
| 120 | + self.pid_monitor_map[pid]["query_cpu_percent"].append(0.0) |
| 121 | + return |
| 122 | + |
| 123 | + precompute_seconds = 0.0 |
| 124 | + query_seconds = 0.0 |
| 125 | + |
| 126 | + for tid, (name, cpu_secs) in current.items(): |
| 127 | + prev_secs = prev.get(tid, (None, 0.0))[1] |
| 128 | + delta = max(0.0, cpu_secs - prev_secs) |
| 129 | + if name.startswith(_PRECOMPUTE_THREAD_PREFIX): |
| 130 | + precompute_seconds += delta |
| 131 | + else: |
| 132 | + query_seconds += delta |
| 133 | + |
| 134 | + if elapsed > 0: |
| 135 | + precompute_pct = (precompute_seconds / elapsed) * 100.0 |
| 136 | + query_pct = (query_seconds / elapsed) * 100.0 |
| 137 | + else: |
| 138 | + precompute_pct = 0.0 |
| 139 | + query_pct = 0.0 |
| 140 | + |
| 141 | + self.pid_monitor_map[pid]["precompute_cpu_percent"].append(precompute_pct) |
| 142 | + self.pid_monitor_map[pid]["query_cpu_percent"].append(query_pct) |
| 143 | + self._prev_thread_jiffies[pid] = current |
| 144 | + |
73 | 145 | def update_pid_monitor_map(self, p) -> List[ProcessMetricSnapshot]: |
74 | 146 | # if p.pid not in self.pid_monitor_map: |
75 | 147 | # self.pid_monitor_map[p.pid] = {m: [] for m in self.monitors} |
@@ -97,18 +169,27 @@ def run(self): |
97 | 169 | # of the list of hooks |
98 | 170 | self.init_hooks() |
99 | 171 | self.pipe.send("ready") |
100 | | - stop = False |
| 172 | + |
| 173 | + self._prev_poll_monotonic = time.monotonic() |
| 174 | + for pid in self.pids_to_monitor: |
| 175 | + self._prev_thread_jiffies[pid] = _read_thread_cpu(pid) |
101 | 176 |
|
102 | 177 | try: |
103 | 178 | while True: |
104 | | - iteration_info = [] # list of process snapshots from this iteration |
| 179 | + now = time.monotonic() |
| 180 | + elapsed = now - self._prev_poll_monotonic |
| 181 | + self._prev_poll_monotonic = now |
| 182 | + |
| 183 | + iteration_info = [] |
105 | 184 | for pid, p in self.psutil_handles.items(): |
106 | 185 | iteration_info += self.update_pid_monitor_map(p) |
| 186 | + self._compute_thread_group_cpu(pid, elapsed) |
107 | 187 | if self.include_children: |
108 | 188 | for child in p.children(recursive=True): |
109 | 189 | if child.pid not in self.pid_monitor_map: |
110 | 190 | self.add_child_pid_to_map(pid, child.pid) |
111 | 191 | iteration_info += self.update_pid_monitor_map(child) |
| 192 | + self._compute_thread_group_cpu(child.pid, elapsed) |
112 | 193 |
|
113 | 194 | self.update_hooks(iteration_info) |
114 | 195 | stop = self.pipe.poll(self.interval) |
|
0 commit comments