use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System};
#[cfg(target_os = "macos")]
pub fn physical_footprint_mb(pid: u32) -> Option<u64> {
let mut info: libc::rusage_info_v0 = unsafe { std::mem::zeroed() };
let ret = unsafe {
libc::proc_pid_rusage(
pid as libc::c_int,
libc::RUSAGE_INFO_V0,
std::ptr::addr_of_mut!(info).cast(),
)
};
if ret != 0 {
return None;
}
Some(info.ri_phys_footprint / (1024 * 1024))
}
pub struct SysMetrics {
sys: System,
pid: Pid,
}
impl SysMetrics {
#[must_use]
pub fn new() -> Self {
let pid = Pid::from_u32(std::process::id());
let mut sys = System::new_with_specifics(
RefreshKind::nothing()
.with_processes(ProcessRefreshKind::nothing().with_memory().with_cpu()),
);
sys.refresh_processes_specifics(
ProcessesToUpdate::Some(&[pid]),
true,
ProcessRefreshKind::nothing().with_memory().with_cpu(),
);
Self { sys, pid }
}
pub fn sample(&mut self) -> (u64, f32) {
self.sys.refresh_processes_specifics(
ProcessesToUpdate::Some(&[self.pid]),
true,
ProcessRefreshKind::nothing().with_memory().with_cpu(),
);
let Some(proc) = self.sys.process(self.pid) else {
return (0, 0.0);
};
let sysinfo_rss_mb = proc.memory() / (1024 * 1024);
let cpu_pct = proc.cpu_usage();
#[cfg(target_os = "macos")]
let rss_mb = physical_footprint_mb(self.pid.as_u32()).unwrap_or(sysinfo_rss_mb);
#[cfg(not(target_os = "macos"))]
let rss_mb = sysinfo_rss_mb;
(rss_mb, cpu_pct)
}
}
impl Default for SysMetrics {
fn default() -> Self {
Self::new()
}
}
const MAX_WALK_DEPTH: usize = 64;
const WALK_BUDGET: std::time::Duration = std::time::Duration::from_secs(30);
#[must_use]
pub fn dir_size_bytes(dir: &std::path::Path) -> u64 {
let total = std::cell::Cell::new(0u64);
let outcome =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| walk_bounded(dir, &total)));
if outcome.is_err() {
tracing::error!(
dir = %dir.display(),
"dir_size_bytes: directory walk panicked (see preceding PANIC log \
for the payload); reporting the partial total"
);
}
total.get()
}
fn walk_bounded(root: &std::path::Path, total: &std::cell::Cell<u64>) {
let started = std::time::Instant::now();
let mut stack: Vec<(std::path::PathBuf, usize)> = vec![(root.to_path_buf(), 0)];
while let Some((dir, depth)) = stack.pop() {
if started.elapsed() >= WALK_BUDGET {
tracing::warn!(
root = %root.display(),
pending = stack.len() + 1,
"dir_size_bytes: walk exceeded its {WALK_BUDGET:?} budget; \
reporting the partial total"
);
return;
}
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let Ok(file_type) = entry.file_type() else {
continue;
};
if file_type.is_symlink() {
continue;
}
if file_type.is_dir() {
if depth < MAX_WALK_DEPTH {
stack.push((entry.path(), depth + 1));
}
continue;
}
if !file_type.is_file() {
continue;
}
if let Ok(meta) = entry.metadata() {
total.set(total.get().saturating_add(meta.len()));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sample_does_not_panic() {
let mut m = SysMetrics::new();
let (_rss, _cpu) = m.sample();
let (_rss2, cpu2) = m.sample();
assert!(cpu2 >= 0.0, "cpu usage must be non-negative, got {cpu2}");
}
#[test]
fn rss_is_plausible() {
let mut m = SysMetrics::new();
let (rss, _cpu) = m.sample();
assert!(
rss < 1024 * 1024,
"RSS implausibly large ({rss} MB) — unit must be MB"
);
}
#[test]
fn dir_size_sums_files() {
let tmp = tempfile::tempdir().expect("tempdir");
std::fs::write(tmp.path().join("a.txt"), vec![0u8; 100]).unwrap();
std::fs::write(tmp.path().join("b.txt"), vec![0u8; 250]).unwrap();
let sub = tmp.path().join("sub");
std::fs::create_dir(&sub).unwrap();
std::fs::write(sub.join("c.txt"), vec![0u8; 50]).unwrap();
assert_eq!(dir_size_bytes(tmp.path()), 400);
}
#[test]
fn dir_size_missing_dir_is_zero() {
let missing = std::path::Path::new("/nonexistent/trusty/path/xyz");
assert_eq!(dir_size_bytes(missing), 0);
}
#[test]
fn dir_size_survives_concurrent_mutation() {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
const BRANCHES: u64 = 8;
const LEAF_BYTES: u64 = 64;
const TOP_BYTES: u64 = 32;
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path().to_path_buf();
for i in 0..BRANCHES {
let branch = root.join(format!("branch-{i}"));
std::fs::create_dir_all(branch.join("a/b/c")).expect("seed dirs");
std::fs::write(
branch.join("a/b/c/leaf.bin"),
vec![0u8; LEAF_BYTES as usize],
)
.expect("seed leaf");
std::fs::write(branch.join("a/top.bin"), vec![0u8; TOP_BYTES as usize])
.expect("seed top");
}
let stop = Arc::new(AtomicBool::new(false));
let mutators: Vec<_> = (0..3)
.map(|t| {
let root = root.clone();
let stop = Arc::clone(&stop);
std::thread::spawn(move || {
let mut n: u64 = 0;
while !stop.load(Ordering::Relaxed) {
let staged = root.join(format!("staged-{t}-{n}"));
let live = root.join(format!("live-{t}"));
if std::fs::create_dir_all(staged.join("nested")).is_ok() {
let _ = std::fs::write(staged.join("nested/data.bin"), vec![0u8; 128]);
let _ = std::fs::remove_dir_all(&live);
let _ = std::fs::rename(&staged, &live);
}
let _ = std::fs::remove_dir_all(&live);
let _ = std::fs::remove_dir_all(&staged);
n = n.wrapping_add(1);
}
})
})
.collect();
let mut min_observed = u64::MAX;
for _ in 0..30 {
min_observed = min_observed.min(dir_size_bytes(&root));
}
stop.store(true, Ordering::Relaxed);
for handle in mutators {
handle.join().expect("mutator thread must not panic");
}
let floor = BRANCHES * (LEAF_BYTES + TOP_BYTES);
assert!(
min_observed >= floor,
"a walk under concurrent mutation lost stable bytes: worst sample \
{min_observed}, floor {floor}"
);
}
#[test]
fn dir_size_depth_cap_boundary_is_exact() {
const TOP_BYTES: u64 = 7;
const AT_CAP_BYTES: u64 = 11;
const PAST_CAP_BYTES: u64 = 4096;
let tmp = tempfile::tempdir().expect("tempdir");
std::fs::write(tmp.path().join("top.bin"), vec![0u8; TOP_BYTES as usize])
.expect("write top");
let mut at_cap = tmp.path().to_path_buf();
for _ in 0..MAX_WALK_DEPTH {
at_cap.push("d");
}
std::fs::create_dir_all(&at_cap).expect("create at-cap tree");
std::fs::write(at_cap.join("at-cap.bin"), vec![0u8; AT_CAP_BYTES as usize])
.expect("write at-cap file");
let past_cap = at_cap.join("d");
std::fs::create_dir(&past_cap).expect("create past-cap dir");
std::fs::write(
past_cap.join("past-cap.bin"),
vec![0u8; PAST_CAP_BYTES as usize],
)
.expect("write past-cap file");
let total = dir_size_bytes(tmp.path());
assert_eq!(
total,
TOP_BYTES + AT_CAP_BYTES,
"depth cap is off by one: {} means the cap fired a level early \
(the at-cap file was dropped); {} means it fired a level late \
(the past-cap file was counted)",
TOP_BYTES,
TOP_BYTES + AT_CAP_BYTES + PAST_CAP_BYTES
);
}
#[cfg(target_os = "macos")]
#[test]
fn self_physical_footprint_is_plausible() {
let pid = std::process::id();
let mb = physical_footprint_mb(pid).expect("proc_pid_rusage must resolve our own pid");
assert!(mb > 0, "physical footprint should be > 0 MB, got {mb}");
assert!(
mb < 1024 * 1024,
"physical footprint implausibly large ({mb} MB) — unit must be MB"
);
}
#[cfg(target_os = "macos")]
#[test]
fn physical_footprint_bogus_pid_returns_none() {
assert_eq!(physical_footprint_mb(u32::MAX), None);
}
#[cfg(target_os = "macos")]
#[test]
fn physical_footprint_tracks_real_allocation_growth() {
let pid = std::process::id();
let before = physical_footprint_mb(pid).expect("must resolve our own pid");
let mut touched: Vec<u8> = vec![0u8; 200 * 1024 * 1024];
for byte in touched.iter_mut().step_by(4096) {
*byte = 1;
}
let after = physical_footprint_mb(pid).expect("must resolve our own pid");
assert!(
after >= before + 100,
"expected footprint to grow by >= 100 MB after touching a 200 MB \
allocation; before={before} after={after}"
);
drop(touched);
}
}