From 4188bcd3340d725c285442abf0787d48d22608cc Mon Sep 17 00:00:00 2001 From: jasonwitty Date: Sun, 23 Aug 2026 06:46:58 -0700 Subject: [PATCH] fix(agent): drop processes that no longer exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `refresh_processes_specifics` was called with remove_dead_processes = false against a long-lived System, so the agent accumulated every process it had ever seen and went on reporting them. Measured on a Pi 5 after a few hours of build churn: 21,648 processes reported, 289 actually running, growing a few every poll. Three consequences, in ascending order of how confusing they are: * unbounded memory growth, and every poll iterating ~75x more entries than it should * process_count — the client's "Top Processes (N total)" — is meaningless * a process you kill keeps its row forever, because the agent keeps sending it. Killing it again reports "no longer exists", since the kernel is telling the truth and the agent is not. The third is how this was found: no amount of client-side reconciliation could fix a list whose producer never forgets anything. Passing `true` is only correct because these two sites use ProcessesToUpdate::All. With `Some(pids)` sysinfo treats every process outside the list as dead and removes it, so the per-PID refresh in collect_process_metrics must keep `false` — noted in a comment there. Verified by driving the TUI against a rebuilt agent: reported count 292 vs 290 real, and a killed row is removed once and never reappears. Co-Authored-By: Claude Opus 5 (1M context) --- socktop_agent/src/metrics.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/socktop_agent/src/metrics.rs b/socktop_agent/src/metrics.rs index ea1ad71..ddaff01 100644 --- a/socktop_agent/src/metrics.rs +++ b/socktop_agent/src/metrics.rs @@ -588,9 +588,17 @@ pub async fn collect_processes_all(state: &AppState) -> ProcessesPayload { // filter when downgrading to a minimal refresh spec. let mut sys_guard = state.sys.lock().await; let sys = &mut *sys_guard; + // `true` = remove processes that no longer exist. With `false`, this + // long-lived System kept every process it had ever seen: the list grew + // without bound (21,648 entries on a machine with 289 processes after a + // few hours of build churn), process_count was meaningless, and — the + // reason this was found — a process you killed kept its row forever, + // because the agent went on reporting it. Safe here only because this is + // `ProcessesToUpdate::All`; with `Some(pids)` it would treat every process + // outside that list as dead and drop it. sys.refresh_processes_specifics( ProcessesToUpdate::All, - false, + true, ProcessRefreshKind::nothing().with_memory().without_tasks(), ); @@ -719,8 +727,11 @@ pub async fn collect_processes_all(state: &AppState) -> ProcessesPayload { //JW too complicated. simplify to remove strange behavior - // For active systems, get accurate CPU metrics - sys.refresh_processes_specifics(ProcessesToUpdate::All, false, kind.with_cpu()); + // For active systems, get accurate CPU metrics. + // `true` = drop processes that have exited; see the Linux path above for + // what `false` cost us (an ever-growing list that kept reporting dead + // processes). Correct only because this is `ProcessesToUpdate::All`. + sys.refresh_processes_specifics(ProcessesToUpdate::All, true, kind.with_cpu()); // } else { // // For idle systems, just get basic process info