use std::collections::HashMap;
use std::process::Command;
#[derive(Clone, Default)]
pub(crate) struct ProcStat {
pub(crate) cpu: f32,
pub(crate) mem: f32,
pub(crate) rss_kb: u64,
pub(crate) state: String,
}
pub(crate) fn gather(pids: &[i64]) -> HashMap<i64, ProcStat> {
if pids.is_empty() {
return HashMap::new();
}
let list = pids
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",");
let Ok(out) = Command::new("ps")
.args(["-o", "pid=,pcpu=,pmem=,rss=,state=", "-p", &list])
.output()
else {
return HashMap::new();
};
let text = String::from_utf8_lossy(&out.stdout);
let mut map = HashMap::new();
for line in text.lines() {
if let Some((pid, stat)) = parse_ps_line(line) {
map.insert(pid, stat);
}
}
map
}
pub(crate) fn parse_ps_line(line: &str) -> Option<(i64, ProcStat)> {
let mut it = line.split_whitespace();
let pid: i64 = it.next()?.parse().ok()?;
let cpu: f32 = it.next()?.parse().unwrap_or(0.0);
let mem: f32 = it.next()?.parse().unwrap_or(0.0);
let rss_kb: u64 = it.next()?.parse().unwrap_or(0);
let state = it.next().unwrap_or("").to_string();
Some((
pid,
ProcStat {
cpu,
mem,
rss_kb,
state,
},
))
}
pub(crate) fn fmt_rss(kb: u64) -> String {
if kb >= 1_048_576 {
format!("{:.1}G", kb as f64 / 1_048_576.0)
} else if kb >= 1024 {
format!("{:.1}M", kb as f64 / 1024.0)
} else {
format!("{kb}K")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_a_ps_line() {
let (pid, s) = parse_ps_line(" 1234 12.5 0.8 40960 R").unwrap();
assert_eq!(pid, 1234);
assert!((s.cpu - 12.5).abs() < 0.01);
assert!((s.mem - 0.8).abs() < 0.01);
assert_eq!(s.rss_kb, 40960);
assert_eq!(s.state, "R");
}
#[test]
fn rejects_a_non_numeric_line() {
assert!(parse_ps_line("PID CPU MEM").is_none());
}
#[test]
fn fmt_rss_scales() {
assert_eq!(fmt_rss(512), "512K");
assert_eq!(fmt_rss(2048), "2.0M");
}
#[test]
fn fmt_rss_boundaries_and_gigabytes() {
assert_eq!(fmt_rss(1023), "1023K");
assert_eq!(fmt_rss(1024), "1.0M");
assert_eq!(fmt_rss(1_048_575), "1024.0M");
assert_eq!(fmt_rss(1_048_576), "1.0G");
assert_eq!(fmt_rss(3_145_728), "3.0G");
}
#[test]
fn parse_ps_line_missing_state_is_empty() {
let (pid, s) = parse_ps_line("42 0.0 0.0 100").unwrap();
assert_eq!(pid, 42);
assert_eq!(s.rss_kb, 100);
assert_eq!(s.state, "");
}
#[test]
fn parse_ps_line_bad_numbers_default_to_zero() {
let (pid, s) = parse_ps_line("7 x y z S").unwrap();
assert_eq!(pid, 7);
assert_eq!(s.cpu, 0.0);
assert_eq!(s.mem, 0.0);
assert_eq!(s.rss_kb, 0);
assert_eq!(s.state, "S");
}
#[test]
fn parse_ps_line_requires_pid_cpu_mem_rss() {
assert!(parse_ps_line("").is_none());
assert!(parse_ps_line("123").is_none());
assert!(parse_ps_line("123 1.0 2.0").is_none());
}
#[test]
fn gather_empty_pids_is_empty() {
assert!(gather(&[]).is_empty());
}
}