From 69ec496a6238f938059a3402507cc04dcd9867aa Mon Sep 17 00:00:00 2001 From: Mike Mueller Date: Thu, 18 Jun 2026 12:21:48 +0000 Subject: [PATCH] perf: report top-5 processes by CPU and memory Collect every named process per sample, then send the top-5 by CPU and the top-5 by memory as a `top_processes` object alongside the existing single top-CPU/top-memory scalars (kept for backward compatibility). opsbase shows these as 5-row lists in the device's Live performance card. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/perf.rs | 58 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/src/perf.rs b/src/perf.rs index c2f80ad..dc8bc29 100644 --- a/src/perf.rs +++ b/src/perf.rs @@ -57,6 +57,9 @@ struct PendingSample { top_cpu_pct: f64, top_mem_name: String, top_mem_mb: i64, + /// Top-5 processes by CPU (name, normalised %) and by memory (name, MB). + top_cpu_procs: Vec<(String, f64)>, + top_mem_procs: Vec<(String, i64)>, attempts: u32, } @@ -156,33 +159,48 @@ fn collect_sample(sys: &mut hbb_common::sysinfo::System) -> Option = None; - let mut top_mem: Option<(&str, u64)> = None; + // Collect every (named) process once, then derive the top-5 by CPU and by + // memory. The single top_cpu_*/top_mem_* scalars are kept for backward + // compatibility (older servers ignore top_processes). + let mut procs: Vec<(&str, f64, i64)> = Vec::new(); let mut proc_count = 0i64; for proc in sys.processes().values() { proc_count += 1; let name = proc.name(); // Some kernel-side rows show up with empty names on Windows; - // skip them so we don't ever render a top-CPU row with no - // label. + // skip them so we don't ever render a top row with no label. if name.is_empty() { continue; } let cu = proc.cpu_usage(); - if cu.is_finite() && cu > top_cpu.map(|(_, v)| v).unwrap_or(0.0) { - top_cpu = Some((name, cu)); - } - let mu = proc.memory(); - if mu > top_mem.map(|(_, v)| v).unwrap_or(0) { - top_mem = Some((name, mu)); - } + let cu = if cu.is_finite() { (cu as f64 / cpu_count).min(100.0) } else { 0.0 }; + let mb = (proc.memory() / 1024 / 1024) as i64; + procs.push((name, cu, mb)); } - let (top_cpu_name, top_cpu_pct) = top_cpu - .map(|(n, v)| (n.to_string(), (v as f64 / cpu_count).min(100.0))) + let mut by_cpu = procs.clone(); + by_cpu.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); + let top_cpu_procs: Vec<(String, f64)> = by_cpu + .iter() + .take(5) + .map(|(n, c, _)| (n.to_string(), *c)) + .collect(); + + let mut by_mem = procs; + by_mem.sort_by(|a, b| b.2.cmp(&a.2)); + let top_mem_procs: Vec<(String, i64)> = by_mem + .iter() + .take(5) + .map(|(n, _, m)| (n.to_string(), *m)) + .collect(); + + let (top_cpu_name, top_cpu_pct) = top_cpu_procs + .first() + .cloned() .unwrap_or_default(); - let (top_mem_name, top_mem_mb) = top_mem - .map(|(n, v)| (n.to_string(), (v / 1024 / 1024) as i64)) + let (top_mem_name, top_mem_mb) = top_mem_procs + .first() + .cloned() .unwrap_or_default(); Some(PendingSample { @@ -196,6 +214,8 @@ fn collect_sample(sys: &mut hbb_common::sysinfo::System) -> Option Result<()> { "top_cpu_pct": s.top_cpu_pct, "top_mem_name": s.top_mem_name, "top_mem_mb": s.top_mem_mb, + "top_processes": { + "cpu": s.top_cpu_procs.iter() + .map(|(n, p)| hbb_common::serde_json::json!({"name": n, "pct": p})) + .collect::>(), + "mem": s.top_mem_procs.iter() + .map(|(n, m)| hbb_common::serde_json::json!({"name": n, "mb": m})) + .collect::>(), + }, }) }) .collect();