stats(p3): normalise cpu% against host CPU count, not process affinity

Per review: available_parallelism() respects the hive-core process's
CPU affinity, so if it's ever affinity-pinned the denominator would
under-count and inflate cpu_pct. Read the online host CPUs from
/sys/devices/system/cpu/online instead (fall back to the process count,
then 1) so the 'percent of total host CPU' definition holds regardless.
This commit is contained in:
iris 2026-06-05 23:03:30 +02:00 committed by mara
commit dbb9f2a787

View file

@ -91,6 +91,37 @@ fn read_usage_usec(dir: &std::path::Path) -> Option<u64> {
None
}
/// Total online host CPUs, for normalising CPU% to "% of total host".
/// Reads `/sys/devices/system/cpu/online` (a comma-separated list of
/// ranges, e.g. `0-19`) rather than `available_parallelism()`, which
/// reflects the hive-core process's CPU affinity — if hive-core is ever
/// affinity-pinned to a subset, that would under-count the denominator
/// and inflate the percentage. Falls back to the process count, then 1.
fn host_nproc() -> usize {
fn from_sysfs() -> Option<usize> {
let s = std::fs::read_to_string("/sys/devices/system/cpu/online").ok()?;
let mut count = 0usize;
for part in s.trim().split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
if let Some((a, b)) = part.split_once('-') {
let a: usize = a.trim().parse().ok()?;
let b: usize = b.trim().parse().ok()?;
count += b.saturating_sub(a) + 1;
} else {
part.parse::<usize>().ok()?;
count += 1;
}
}
(count > 0).then_some(count)
}
from_sysfs()
.or_else(|| std::thread::available_parallelism().ok().map(usize::from))
.unwrap_or(1)
}
/// Sample CPU + memory for every running agent container. Best-effort:
/// agents whose scope dir is absent (not running) or unreadable are
/// skipped, never fatal. Sorted by name for a stable display order.
@ -111,7 +142,7 @@ pub async fn gather() -> Vec<ContainerResource> {
sleep(CPU_SAMPLE).await;
#[allow(clippy::cast_precision_loss)]
let nproc = std::thread::available_parallelism().map_or(1.0, |n| n.get() as f64);
let nproc = host_nproc() as f64;
#[allow(clippy::cast_precision_loss)]
let interval_usec = CPU_SAMPLE.as_micros() as f64;