use std::collections::HashMap;
use std::sync::OnceLock;
use libproc::libproc::{
bsd_info::BSDInfo,
pid_rusage::{RUsageInfoV4, pidrusage},
proc_pid::pidinfo,
task_info::TaskInfo,
};
use libproc::processes::{ProcFilter, pids_by_type};
use super::ProcessSampler;
use crate::sampler::ProcessSnapshot;
const NANOS_PER_SEC: f64 = 1_000_000_000.0;
#[repr(C)]
struct MachTimebaseInfo {
numer: u32,
denom: u32,
}
unsafe extern "C" {
fn mach_timebase_info(info: *mut MachTimebaseInfo) -> i32;
}
fn mach_timebase() -> (u64, u64) {
static TIMEBASE: OnceLock<(u64, u64)> = OnceLock::new();
*TIMEBASE.get_or_init(|| {
let mut info = MachTimebaseInfo { numer: 0, denom: 0 };
unsafe { mach_timebase_info(&raw mut info) };
let denom = if info.denom == 0 { 1 } else { info.denom };
(u64::from(info.numer), u64::from(denom))
})
}
#[allow(clippy::cast_precision_loss)]
fn mach_time_to_seconds(mach_time: u64) -> f64 {
let (numer, denom) = mach_timebase();
(mach_time as f64) * (numer as f64) / (denom as f64) / NANOS_PER_SEC
}
pub struct MacosSampler;
impl MacosSampler {
#[must_use]
pub fn new() -> Self {
Self
}
}
impl Default for MacosSampler {
fn default() -> Self {
Self::new()
}
}
impl ProcessSampler for MacosSampler {
fn sample_tree(&mut self, root_pid: i32) -> Vec<ProcessSnapshot> {
let pids = collect_descendants(root_pid);
pids.into_iter().filter_map(sample_process).collect()
}
}
fn collect_descendants(root: i32) -> Vec<i32> {
let mut by_parent: HashMap<i32, Vec<i32>> = HashMap::new();
if let Ok(pids) = pids_by_type(ProcFilter::All) {
for pid in pids {
#[allow(clippy::cast_possible_wrap)]
let pid_i32 = pid as i32;
if let Ok(info) = pidinfo::<BSDInfo>(pid_i32, 0) {
#[allow(clippy::cast_possible_wrap)]
let ppid = info.pbi_ppid as i32;
#[allow(clippy::cast_possible_wrap)]
let proc_pid = info.pbi_pid as i32;
by_parent.entry(ppid).or_default().push(proc_pid);
}
}
}
let mut out = vec![root];
let mut stack = vec![root];
while let Some(parent) = stack.pop() {
if let Some(children) = by_parent.get(&parent) {
for &child in children {
out.push(child);
stack.push(child);
}
}
}
out
}
fn sample_process(pid: i32) -> Option<ProcessSnapshot> {
let task: TaskInfo = pidinfo(pid, 0).ok()?;
let rss_bytes = task.pti_resident_size;
let vms_bytes = task.pti_virtual_size;
let cpu_time_seconds =
mach_time_to_seconds(task.pti_total_user) + mach_time_to_seconds(task.pti_total_system);
let rusage_result: Result<RUsageInfoV4, _> = pidrusage(pid);
let (uss_bytes, pss_bytes, io_read_bytes, io_write_bytes) = match rusage_result {
Ok(rusage) => {
let footprint = rusage.ri_phys_footprint;
(
Some(footprint),
Some(footprint),
Some(rusage.ri_diskio_bytesread),
Some(rusage.ri_diskio_byteswritten),
)
}
Err(_) => (None, None, None, None),
};
Some(ProcessSnapshot {
pid,
rss_bytes,
vms_bytes,
uss_bytes,
pss_bytes,
io_read_bytes,
io_write_bytes,
cpu_time_seconds,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn self_pid_produces_a_snapshot() {
let mut sampler = MacosSampler::new();
#[allow(clippy::cast_possible_wrap)]
let pid = std::process::id() as i32;
let snaps = sampler.sample_tree(pid);
let me = snaps.iter().find(|s| s.pid == pid).expect("self snapshot");
assert!(me.rss_bytes > 0, "expected non-zero RSS");
assert!(me.vms_bytes > 0, "expected non-zero VMS");
}
}