use std::collections::HashMap;
use std::time::Instant;
use crate::darwin::{parse, scan, units, DarwinPlatform};
use crate::row::{Row, RowFlags};
use crate::system::{CpuTimes, MemInfo, SystemSnapshot};
use crate::table::{Cache, Stable};
use crate::Pid;
use crate::PlatformSampler;
#[derive(Copy, Clone)]
struct PrevRow {
start_usec: u64,
cpu_ns: u64,
}
#[derive(Copy, Clone)]
struct PrevThread {
cpu_ns: u64,
}
pub fn thread_identity(pid: Pid, handle: u64) -> u64 {
let mixed = handle ^ ((pid as u64) << 32);
((pid as u64) << 48) | (mixed & 0x0000_FFFF_FFFF_FFFF)
}
const CACHE_SLACK: usize = 4;
pub struct Sampler {
cache: Cache,
prev_rows: HashMap<Pid, PrevRow>,
prev_threads: HashMap<(Pid, u64), PrevThread>,
prev_cpu: Vec<CpuTimes>,
prev_at: Option<Instant>,
prev_net: Option<(crate::system::NetCounters, Instant)>,
page_size: u64,
cpu_count: usize,
show_threads: bool,
}
impl Default for Sampler {
fn default() -> Self {
Self::new()
}
}
impl Sampler {
pub fn new() -> Self {
let cpu_count = DarwinPlatform::cpu_count();
Self {
cache: Cache::new(),
prev_rows: HashMap::new(),
prev_threads: HashMap::new(),
prev_cpu: Vec::new(),
prev_at: None,
prev_net: None,
page_size: scan::page_size(),
cpu_count,
show_threads: false,
}
}
}
impl PlatformSampler for Sampler {
fn backend_name() -> &'static str {
"darwin/libproc"
}
fn cache_len(&self) -> usize {
self.cache.len()
}
fn sample(&mut self) -> (SystemSnapshot, Vec<Row>) {
let at = Instant::now();
let cpu = scan::cpu_ticks();
let cpu_delta: Vec<CpuTimes> = if self.prev_cpu.len() == cpu.len() && !cpu.is_empty() {
cpu.iter()
.zip(&self.prev_cpu)
.map(|(n, p)| n.delta(p))
.collect()
} else {
vec![CpuTimes::default(); cpu.len()]
};
let vm = scan::vm_stats();
let total = scan::total_memory();
let mut mem = MemInfo {
total,
free: vm.free,
active: vm.active,
inactive: vm.inactive,
wired: vm.wired,
compressed: vm.compressed,
purgeable: vm.purgeable,
speculative: vm.speculative,
cached: vm.external,
internal: vm.internal,
..Default::default()
};
mem.finish_darwin();
let swap = scan::swap_usage();
let load = scan::loadavg();
let uptime = scan::uptime_secs();
let now_secs = scan::now_secs();
let net = scan::net_counters();
let net_rate = match self.prev_net {
Some((prev, at_prev)) => net.per_sec(&prev, at.duration_since(at_prev).as_secs_f64()),
None => crate::system::Rate::default(),
};
self.prev_net = Some((net, at));
let procs = match crate::darwin::kinfo::list_processes() {
Ok(list) if !list.is_empty() => list,
_ => Vec::new(),
};
let interval = self
.prev_at
.map(|p| at.duration_since(p).as_secs_f64())
.unwrap_or(0.0);
let mut rows = self.read_rows(&procs, interval, now_secs, mem.total);
if self.show_threads {
let threads = self.read_threads(&procs, &rows, interval, mem.total);
rows.extend(threads);
}
self.prev_cpu = cpu.clone();
self.prev_at = Some(at);
self.cache.retain_live(
&rows
.iter()
.map(|r| (r.pid, r.starttime))
.collect::<Vec<_>>(),
);
if self.cache.len() > rows.len().saturating_mul(CACHE_SLACK) + 64 {
self.cache.clear();
}
let snapshot = SystemSnapshot {
at,
cpu,
cpu_delta,
mem,
swap,
load,
uptime,
clk_tck: 100,
page_size: self.page_size,
net,
net_rate,
disk: crate::system::DiskCounters::default(),
disk_rate: crate::system::Rate::default(),
tasks: task_stats(&rows),
psi: None,
zram: crate::system::ZramInfo::default(),
};
(snapshot, rows)
}
fn set_scan_options(&mut self, opts: crate::ScanOptions) {
self.show_threads = opts.threads;
}
fn supports_threads() -> bool {
true
}
}
impl Sampler {
fn read_threads(
&mut self,
procs: &[crate::darwin::kinfo::RawProc],
proc_rows: &[Row],
interval_secs: f64,
mem_total: u64,
) -> Vec<Row> {
let mut out = Vec::new();
let mut next_prev: HashMap<(Pid, u64), PrevThread> =
HashMap::with_capacity(self.prev_threads.len());
let mem_of: HashMap<Pid, (u64, u64, u64)> = proc_rows
.iter()
.map(|r| (r.pid, (r.res, r.virt, r.elapsed)))
.collect();
for kp in procs {
let pid = kp.pid;
let Some(handles) = scan::list_threads(pid) else {
continue;
};
if handles.is_empty() {
continue;
}
let start_usec = kp
.start_sec
.saturating_mul(1_000_000)
.saturating_add(kp.start_usec);
let stable = self.identity(pid, start_usec);
for (index, handle) in handles.into_iter().enumerate() {
if index == 0 {
continue;
}
if handle == 0 {
continue;
}
let Some(ti) = scan::thread_info(pid, handle) else {
continue;
};
let cpu_ns = ti.user_ns.saturating_add(ti.system_ns);
let prev = self.prev_threads.get(&(pid, handle)).copied();
let mut row = Row::default_for_test(pid);
row.thread_id = Some(thread_identity(pid, handle));
row.tgid = pid;
row.is_thread = true;
row.ppid = pid;
row.starttime = start_usec;
row.uid = kp.uid;
row.pgrp = stable.pgrp;
row.tpgid = stable.tpgid;
row.tty_nr = stable.tty_dev;
row.nice = kp.nice.clamp(-128, 127) as i8;
row.priority = row.nice;
row.processor = -1;
row.nlwp = 1;
row.cmdline = stable.cmdline.clone();
row.exe = stable.exe.clone();
row.cwd = stable.cwd.clone();
row.session = stable.session;
row.flags = row
.flags
.insert(RowFlags::USERLAND_THREAD)
.set(RowFlags::KERNEL_THREAD, stable.is_kernel_thread);
if ti.name_str().is_empty() {
let _ = row.comm.try_push_str(&kp.comm_str());
} else {
let _ = row.comm.try_push_str(ti.name_str());
}
row.time_ticks = cpu_ns / 10_000_000; row.state = match ti.run_state {
1 => b'R',
2 => b'T',
_ => b'S',
};
if interval_secs > 0.0 {
if let Some(p) = prev {
if cpu_ns >= p.cpu_ns {
let delta = (cpu_ns - p.cpu_ns) as f64;
let pct = delta / (interval_secs * 1e9) * 100.0;
row.percent_cpu = (pct as f32).min(self.cpu_count as f32 * 100.0);
}
}
}
let (res, virt, elapsed) = mem_of.get(&pid).copied().unwrap_or((0, 0, 0));
row.res = res;
row.virt = virt;
row.elapsed = elapsed;
row.percent_mem = if mem_total > 0 {
row.res as f32 * 100.0 / mem_total as f32
} else {
0.0
};
next_prev.insert((pid, handle), PrevThread { cpu_ns });
out.push(row);
}
}
self.prev_threads = next_prev;
out
}
fn read_rows(
&mut self,
procs: &[crate::darwin::kinfo::RawProc],
interval_secs: f64,
now_secs: u64,
mem_total: u64,
) -> Vec<Row> {
let mut out = Vec::with_capacity(procs.len());
let mut next_prev = HashMap::with_capacity(procs.len());
for kp in procs {
let pid = kp.pid;
let bsd_details = scan::task_all_info(pid);
let ti = bsd_details.map(|all| all.task);
let details = bsd_details.map(|all| all.bsd).unwrap_or_default();
let start_usec = kp
.start_sec
.saturating_mul(1_000_000)
.saturating_add(kp.start_usec);
let prev = self.prev_rows.get(&pid).copied();
let same_process = prev.is_some_and(|p| p.start_usec == start_usec);
let cpu_ns = ti
.map(|t| {
units::mach_ticks_to_nanos(t.user_ns)
.saturating_add(units::mach_ticks_to_nanos(t.system_ns))
})
.unwrap_or(0);
let mut row = Row::default_for_test(pid);
row.starttime = start_usec;
row.ppid = kp.ppid;
row.pgrp = details.pgrp;
row.tpgid = details.tpgid;
row.tty_nr = details.tty_dev;
let sid = unsafe { libc::getsid(pid as libc::pid_t) };
row.session = if sid > 0 { sid as u32 } else { 0 };
row.uid = kp.uid;
row.nice = kp.nice.clamp(-128, 127) as i8;
row.priority = kp.nice.clamp(-128, 127) as i8;
row.state = parse::state_letter(kp.p_stat);
if matches!(row.state, b'T' | b'Z' | b'I') {
} else if let Some(t) = ti {
row.state = if t.running_threads > 0 { b'R' } else { b'S' };
} else {
row.state = b'?';
}
let name = kp.comm_str();
let _ = row.comm.try_push_str(&name);
row.time_ticks = cpu_ns / 10_000_000; if let Some(t) = ti {
row.virt = t.virtual_size;
row.res = t.resident_size;
row.nlwp = t.threads as i32;
row.min_flt = t.faults as u64;
}
row.processor = -1;
if same_process && interval_secs > 0.0 {
if let Some(p) = prev {
if cpu_ns >= p.cpu_ns {
let delta = (cpu_ns - p.cpu_ns) as f64;
let pct = delta / (interval_secs * 1e9) * 100.0;
row.percent_cpu = (pct as f32).min(self.cpu_count as f32 * 100.0);
}
}
}
next_prev.insert(pid, PrevRow { start_usec, cpu_ns });
row.percent_mem = if mem_total > 0 {
row.res as f32 * 100.0 / mem_total as f32
} else {
0.0
};
row.elapsed = now_secs.saturating_sub(kp.start_sec);
if ti.is_some() {
let stable = self.identity(pid, start_usec);
row.cmdline = stable.cmdline;
row.exe = stable.exe;
row.cwd = stable.cwd;
row.session = stable.session;
row.flags = row
.flags
.set(RowFlags::KERNEL_THREAD, stable.is_kernel_thread);
if stable.is_kernel_thread {
row.flags = row.flags.insert(RowFlags::KERNEL_THREAD);
}
}
out.push(row);
}
self.prev_rows = next_prev;
out
}
fn identity(&mut self, pid: Pid, key: u64) -> Stable {
if let Some(hit) = self.cache.get(pid, key) {
return hit.clone();
}
let mut stable = Stable {
exe: scan::exe_path(pid).map(String::into_boxed_str),
cwd: scan::cwd_path(pid).map(String::into_boxed_str),
..Stable::default()
};
let sid = unsafe { libc::getsid(pid as libc::pid_t) };
stable.session = if sid > 0 { sid as u32 } else { 0 };
if let Some(all) = scan::task_all_info(pid) {
stable.pgrp = all.bsd.pgrp;
stable.tpgid = all.bsd.tpgid;
stable.tty_dev = all.bsd.tty_dev;
}
stable.cmdline = scan::procargs2(pid)
.and_then(|raw| parse::parse_procargs2(&raw))
.filter(|s| !s.is_empty())
.map(String::into_boxed_str);
stable.is_kernel_thread = stable.cmdline.is_none() && stable.exe.is_none();
self.cache.insert(pid, key, stable.clone());
stable
}
}
fn task_stats(rows: &[Row]) -> crate::system::TaskStats {
let procs = rows.iter().filter(|r| !r.is_thread);
let mut stats = crate::system::TaskStats::default();
for row in procs {
stats.total += 1;
if row.state == b'R' {
stats.running += 1;
}
stats.threads += row.nlwp.max(1) as usize;
}
stats
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn thread_identity_is_unique_per_pid_and_handle() {
let shared = 8_681_251_360u64;
assert_ne!(thread_identity(1, shared), thread_identity(2, shared));
assert_ne!(thread_identity(1, 100), thread_identity(1, 200));
assert_eq!(thread_identity(42, 7), thread_identity(42, 7));
}
#[test]
fn thread_identity_does_not_collide_with_a_process_pid() {
for pid in [1u32, 2, 100, 99998] {
for handle in [0u64, 1, 8_681_251_360, u64::MAX] {
let id = thread_identity(pid, handle);
assert!(
id > u32::MAX as u64,
"thread id {id} for pid {pid} would collide with a pid"
);
}
}
}
#[test]
fn task_stats_count_processes_not_rows() {
let mut proc = Row::default_for_test(1);
proc.nlwp = 4;
proc.state = b'R';
let mut thread = Row::default_for_test(1);
thread.is_thread = true;
thread.thread_id = Some(thread_identity(1, 99));
thread.nlwp = 1;
thread.state = b'R';
let stats = task_stats(&[proc, thread]);
assert_eq!(stats.total, 1, "one process, not two rows");
assert_eq!(stats.running, 1, "the thread must not be counted again");
assert_eq!(stats.threads, 4, "nlwp already counts the threads");
}
#[test]
fn live_sample_produces_rows() {
let mut s = Sampler::new();
let (snap, rows) = s.sample();
assert!(!rows.is_empty(), "there are always processes");
assert!(snap.mem.total > 0);
assert!(snap.cpu.len() >= 2);
assert_eq!(snap.page_size, scan::page_size());
}
#[test]
fn self_appears_with_a_command_line() {
let me = std::process::id();
let mut s = Sampler::new();
let (_snap, rows) = s.sample();
let mine = rows
.iter()
.find(|r| r.pid == me)
.expect("own process must appear");
assert!(mine.res > 0, "resident size must be known");
assert!(mine.cmdline.is_some(), "own cmdline is readable");
}
#[test]
fn second_sample_yields_finite_cpu() {
let mut s = Sampler::new();
s.sample();
std::thread::sleep(std::time::Duration::from_millis(60));
let (snap, rows) = s.sample();
for row in &rows {
assert!(row.percent_cpu.is_finite(), "cpu% must be finite");
assert!(row.percent_cpu >= 0.0);
}
assert!(snap.cpu_usage().is_finite());
}
#[test]
fn identity_cache_is_stable_across_ticks() {
let mut s = Sampler::new();
s.sample();
let first = s.cache_len();
assert!(first > 0);
s.sample();
assert!(
s.cache_len() <= first * 2,
"cache must not grow unboundedly"
);
}
}