Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
67 lines
2.8 KiB
Python
67 lines
2.8 KiB
Python
"""Best-effort Linux process-tree observations without reading command payloads."""
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
import os
|
|
import resource
|
|
import time
|
|
|
|
|
|
class ProcessMetrics:
|
|
def __init__(self, pid):
|
|
self.pid = pid
|
|
self.started = time.monotonic()
|
|
self.initial = resource.getrusage(resource.RUSAGE_CHILDREN)
|
|
self.peak_rss = self.peak_threads = self.peak_processes = self.peak_fds = 0
|
|
self.samples = 0
|
|
self.wait_channels = Counter()
|
|
self.names = set()
|
|
self.sample()
|
|
|
|
def sample(self):
|
|
pending, seen = [self.pid], set()
|
|
rss = threads = fds = processes = 0
|
|
while pending:
|
|
pid = pending.pop()
|
|
if pid in seen:
|
|
continue
|
|
seen.add(pid)
|
|
directory = Path("/proc") / str(pid)
|
|
try:
|
|
fields = (directory / "stat").read_text().rsplit(")", 1)[1].split()
|
|
children = (directory / "task" / str(pid) / "children").read_text().split()
|
|
pending.extend(map(int, children))
|
|
rss += int(fields[21]) * os.sysconf("SC_PAGE_SIZE")
|
|
threads += int(fields[17])
|
|
fds += sum(1 for _ in (directory / "fd").iterdir())
|
|
processes += 1
|
|
self.names.add((directory / "comm").read_text().strip())
|
|
wait = (directory / "wchan").read_text().strip()
|
|
if wait and wait != "0":
|
|
self.wait_channels[wait] += 1
|
|
except (OSError, ValueError, IndexError):
|
|
continue
|
|
self.samples += 1
|
|
self.peak_rss = max(self.peak_rss, rss)
|
|
self.peak_threads = max(self.peak_threads, threads)
|
|
self.peak_processes = max(self.peak_processes, processes)
|
|
self.peak_fds = max(self.peak_fds, fds)
|
|
|
|
def finish(self):
|
|
usage = resource.getrusage(resource.RUSAGE_CHILDREN)
|
|
cpu = max(0, usage.ru_utime + usage.ru_stime - self.initial.ru_utime - self.initial.ru_stime)
|
|
wall = time.monotonic() - self.started
|
|
return {
|
|
"ownerProcessGroup": self.pid,
|
|
"wallSeconds": round(wall, 6),
|
|
"reapedChildCpuSeconds": round(cpu, 6),
|
|
"cpuPercent": round(100 * cpu / wall, 2) if wall else 0,
|
|
"peakSampledRssBytes": self.peak_rss,
|
|
"peakSampledThreads": self.peak_threads,
|
|
"peakSampledProcesses": self.peak_processes,
|
|
"peakSampledFileDescriptors": self.peak_fds,
|
|
"samples": self.samples,
|
|
"processNames": sorted(self.names),
|
|
"waitChannels": dict(self.wait_channels.most_common(8)),
|
|
"limits": "Sampled peaks may miss short-lived children; child CPU is wait4 accounting, not GIL or event-loop timing.",
|
|
}
|