use std::collections::HashMap;
use std::time::{Duration, Instant};
use super::model::ProcTick;
const REFRESH: Duration = Duration::from_secs(2);
const MAX_PROCS: usize = 64;
#[derive(Debug, Clone, Copy, Default)]
pub struct ProcMem {
pub footprint: Option<u64>,
pub pss: Option<u64>,
pub private: Option<u64>,
pub shared: Option<u64>,
pub swap: Option<u64>,
pub peak: Option<u64>,
}
impl ProcMem {
fn is_empty(&self) -> bool {
self.footprint.is_none() && self.pss.is_none()
}
}
pub struct ProcMemCollector {
last_sample_at: Option<Instant>,
cached: HashMap<u32, ProcMem>,
}
impl ProcMemCollector {
pub fn new() -> Self {
Self {
last_sample_at: None,
cached: HashMap::new(),
}
}
pub fn sample(&mut self, procs: &[ProcTick]) -> HashMap<u32, ProcMem> {
let stale = self
.last_sample_at
.map(|t| t.elapsed() >= REFRESH)
.unwrap_or(true);
if stale {
self.last_sample_at = Some(Instant::now());
self.cached = collect_top(procs);
}
self.cached.clone()
}
}
fn collect_top(procs: &[ProcTick]) -> HashMap<u32, ProcMem> {
let mut by_rss: Vec<(u32, u64)> = procs.iter().map(|p| (p.pid, p.mem_rss)).collect();
by_rss.sort_by_key(|&(_, rss)| std::cmp::Reverse(rss));
let mut out = HashMap::new();
for (pid, _) in by_rss.into_iter().take(MAX_PROCS) {
let m = collect_pid(pid);
if !m.is_empty() {
out.insert(pid, m);
}
}
out
}
#[cfg(target_os = "linux")]
fn collect_pid(pid: u32) -> ProcMem {
let Ok(text) = std::fs::read_to_string(format!("/proc/{}/smaps_rollup", pid)) else {
return ProcMem::default();
};
parse_smaps_rollup(&text)
}
#[cfg(target_os = "macos")]
fn collect_pid(pid: u32) -> ProcMem {
unsafe {
let mut info: libc::rusage_info_v4 = std::mem::zeroed();
let ret = libc::proc_pid_rusage(
pid as libc::c_int,
libc::RUSAGE_INFO_V4,
&mut info as *mut libc::rusage_info_v4 as *mut libc::rusage_info_t,
);
if ret != 0 {
return ProcMem::default();
}
ProcMem {
footprint: Some(info.ri_phys_footprint),
peak: Some(info.ri_lifetime_max_phys_footprint),
..ProcMem::default()
}
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn collect_pid(_pid: u32) -> ProcMem {
ProcMem::default()
}
#[cfg(any(target_os = "linux", test))]
pub(crate) fn parse_smaps_rollup(text: &str) -> ProcMem {
let mut pss = None;
let mut private = 0u64;
let mut shared = 0u64;
let mut swap = None;
let mut saw_private = false;
let mut saw_shared = false;
for line in text.lines() {
let Some((key, rest)) = line.split_once(':') else {
continue;
};
let Some(kb) = rest
.split_whitespace()
.next()
.and_then(|n| n.parse::<u64>().ok())
else {
continue;
};
let bytes = kb.saturating_mul(1024);
match key.trim() {
"Pss" => pss = Some(bytes),
"Private_Clean" | "Private_Dirty" => {
private = private.saturating_add(bytes);
saw_private = true;
}
"Shared_Clean" | "Shared_Dirty" => {
shared = shared.saturating_add(bytes);
saw_shared = true;
}
"Swap" => swap = Some(bytes),
_ => {}
}
}
ProcMem {
pss,
private: saw_private.then_some(private),
shared: saw_shared.then_some(shared),
swap,
..ProcMem::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_smaps_rollup_totals() {
let sample = "\
00400000-7ffd13a3c000 ---p 00000000 00:00 0 [rollup]
Rss: 123456 kB
Pss: 45678 kB
Pss_Anon: 30000 kB
Shared_Clean: 60000 kB
Shared_Dirty: 2000 kB
Private_Clean: 5000 kB
Private_Dirty: 40000 kB
Referenced: 100000 kB
Anonymous: 42000 kB
Swap: 1234 kB
SwapPss: 617 kB
";
let m = parse_smaps_rollup(sample);
assert_eq!(m.pss, Some(45678 * 1024));
assert_eq!(m.private, Some((5000 + 40000) * 1024));
assert_eq!(m.shared, Some((60000 + 2000) * 1024));
assert_eq!(m.swap, Some(1234 * 1024));
assert_eq!(m.footprint, None);
}
#[test]
fn empty_text_yields_no_data() {
let m = parse_smaps_rollup("");
assert!(m.is_empty());
assert_eq!(m.private, None);
assert_eq!(m.shared, None);
assert_eq!(m.swap, None);
}
#[test]
fn garbled_values_are_skipped() {
let m = parse_smaps_rollup("Pss: not-a-number kB\nSwap: 10 kB\n");
assert_eq!(m.pss, None);
assert_eq!(m.swap, Some(10 * 1024));
}
#[test]
fn zero_private_still_reports_some_when_keys_present() {
let m = parse_smaps_rollup("Private_Clean: 0 kB\nPrivate_Dirty: 0 kB\n");
assert_eq!(m.private, Some(0));
}
#[cfg(target_os = "macos")]
#[test]
fn live_footprint_for_own_pid() {
let m = collect_pid(std::process::id());
assert!(m.footprint.unwrap_or(0) > 0);
}
#[cfg(target_os = "linux")]
#[test]
fn live_smaps_rollup_for_own_pid() {
let m = collect_pid(std::process::id());
assert!(m.pss.unwrap_or(0) > 0);
}
#[test]
fn collector_caches_within_refresh_window() {
let mut c = ProcMemCollector::new();
let _ = c.sample(&[]);
let first_at = c.last_sample_at;
let _ = c.sample(&[]);
assert_eq!(c.last_sample_at, first_at);
}
}