1use std::collections::{HashMap, HashSet, VecDeque};
31use std::time::Duration;
32
33#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
35pub struct TreeUsage {
36 pub rss_bytes: u64,
38 pub cpu_time: Duration,
40 pub processes: u32,
42}
43
44type Facts = (u64, Duration);
46
47#[must_use]
49pub fn sample_tree(root: u32) -> Option<TreeUsage> {
50 sample_trees(&[root]).remove(&root)
51}
52
53#[must_use]
56pub fn sample_trees(roots: &[u32]) -> HashMap<u32, TreeUsage> {
57 if roots.is_empty() {
58 return HashMap::new();
59 }
60 sys::sample_trees(roots)
61}
62
63fn trees(
67 roots: &[u32],
68 parents: &[(u32, u32)],
69 mut facts: impl FnMut(u32) -> Option<Facts>,
70 mut is_child: impl FnMut(u32, u32) -> bool,
71) -> HashMap<u32, TreeUsage> {
72 let mut children: HashMap<u32, Vec<u32>> = HashMap::new();
73 for (pid, parent) in parents {
74 if pid != parent {
75 children.entry(*parent).or_default().push(*pid);
76 }
77 }
78 let alive: HashSet<u32> = parents.iter().map(|(pid, _)| *pid).collect();
79 let mut found = HashMap::new();
80 for root in roots.iter().copied().filter(|root| alive.contains(root)) {
81 let mut usage = TreeUsage::default();
82 let mut seen = HashSet::from([root]);
83 let mut queue = VecDeque::from([root]);
84 while let Some(pid) = queue.pop_front() {
85 let (rss, cpu) = facts(pid).unwrap_or_default();
86 usage.rss_bytes = usage.rss_bytes.saturating_add(rss);
87 usage.cpu_time = usage.cpu_time.saturating_add(cpu);
88 usage.processes = usage.processes.saturating_add(1);
89 for child in children.get(&pid).into_iter().flatten() {
90 if !seen.contains(child) && is_child(*child, pid) {
91 seen.insert(*child);
92 queue.push_back(*child);
93 }
94 }
95 }
96 found.insert(root, usage);
97 }
98 found
99}
100
101#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
106fn parse_proc_stat(line: &str) -> Option<(u32, u64, u64)> {
107 let fields: Vec<&str> = crate::process::proc_stat_after_comm(line)?
108 .split_whitespace()
109 .collect();
110 let field = |number: usize| fields.get(number - 3).copied();
113 let parent = field(4)?.parse().ok()?;
114 let ticks = [14, 15, 16, 17]
115 .into_iter()
116 .map(|number| field(number)?.parse::<i64>().ok())
117 .try_fold(0u64, |sum, value| {
118 Some(sum.saturating_add(u64::try_from(value?).unwrap_or(0)))
119 })?;
120 let pages = u64::try_from(field(24)?.parse::<i64>().ok()?).unwrap_or(0);
121 Some((parent, ticks, pages))
122}
123
124#[cfg(target_os = "linux")]
125mod sys {
126 use std::collections::HashMap;
127 use std::time::Duration;
128
129 use super::{Facts, TreeUsage, parse_proc_stat, trees};
130
131 pub(super) fn sample_trees(roots: &[u32]) -> HashMap<u32, TreeUsage> {
132 let ticks_per_second = u64::try_from(unsafe { libc::sysconf(libc::_SC_CLK_TCK) })
135 .ok()
136 .filter(|ticks| *ticks > 0)
137 .unwrap_or(100);
138 let page = u64::try_from(unsafe { libc::sysconf(libc::_SC_PAGESIZE) })
140 .ok()
141 .filter(|page| *page > 0)
142 .unwrap_or(4096);
143 let mut parents = Vec::new();
144 let mut facts: HashMap<u32, Facts> = HashMap::new();
145 let Ok(entries) = std::fs::read_dir("/proc") else {
146 return HashMap::new();
147 };
148 for entry in entries.flatten() {
149 let Some(pid) = entry
150 .file_name()
151 .to_str()
152 .and_then(|n| n.parse::<u32>().ok())
153 else {
154 continue;
155 };
156 let Ok(line) = std::fs::read_to_string(entry.path().join("stat")) else {
158 continue;
159 };
160 if let Some((parent, ticks, pages)) = parse_proc_stat(&line) {
161 parents.push((pid, parent));
162 facts.insert(
163 pid,
164 (
165 pages.saturating_mul(page),
166 Duration::from_nanos(
167 ticks.saturating_mul(1_000_000_000) / ticks_per_second,
168 ),
169 ),
170 );
171 }
172 }
173 trees(roots, &parents, |pid| facts.get(&pid).copied(), |_, _| true)
174 }
175}
176
177#[cfg(target_os = "macos")]
178mod sys {
179 use std::collections::HashMap;
180 use std::time::Duration;
181
182 use super::{TreeUsage, trees};
183
184 fn info<T>(pid: i32, flavor: i32) -> Option<T> {
185 let size = i32::try_from(std::mem::size_of::<T>()).ok()?;
186 let mut value = std::mem::MaybeUninit::<T>::zeroed();
187 let written =
190 unsafe { libc::proc_pidinfo(pid, flavor, 0, value.as_mut_ptr().cast(), size) };
191 (written == size).then(|| unsafe { value.assume_init() })
194 }
195
196 #[allow(
199 deprecated,
200 reason = "libc marks the mach bindings deprecated in favour of a crate this workspace does not take; the call is stable"
201 )]
202 fn ticks_to_nanos(ticks: u64) -> u64 {
203 static BASE: std::sync::OnceLock<(u32, u32)> = std::sync::OnceLock::new();
204 let (numer, denom) = *BASE.get_or_init(|| {
205 let mut base = libc::mach_timebase_info { numer: 0, denom: 0 };
206 let ok = unsafe { libc::mach_timebase_info(&raw mut base) } == 0;
208 if ok && base.denom != 0 {
209 (base.numer, base.denom)
210 } else {
211 (1, 1)
212 }
213 });
214 u64::try_from(u128::from(ticks) * u128::from(numer) / u128::from(denom)).unwrap_or(u64::MAX)
215 }
216
217 fn all_pids() -> Vec<i32> {
218 let count = unsafe { libc::proc_listallpids(std::ptr::null_mut(), 0) };
220 let Ok(count) = usize::try_from(count) else {
221 return Vec::new();
222 };
223 let mut pids = vec![0i32; count + 64];
225 let bytes = i32::try_from(pids.len() * std::mem::size_of::<i32>()).unwrap_or(i32::MAX);
226 let filled = unsafe { libc::proc_listallpids(pids.as_mut_ptr().cast(), bytes) };
228 pids.truncate(usize::try_from(filled).unwrap_or(0).min(pids.len()));
229 pids
230 }
231
232 pub(super) fn sample_trees(roots: &[u32]) -> HashMap<u32, TreeUsage> {
233 let parents: Vec<(u32, u32)> = all_pids()
234 .into_iter()
235 .filter(|pid| *pid > 0)
236 .filter_map(|pid| {
237 let bsd: libc::proc_bsdshortinfo = info(pid, libc::PROC_PIDT_SHORTBSDINFO)?;
238 Some((u32::try_from(pid).ok()?, bsd.pbsi_ppid))
239 })
240 .collect();
241 trees(
242 roots,
243 &parents,
244 |pid| {
245 let task: libc::proc_taskinfo =
246 info(i32::try_from(pid).ok()?, libc::PROC_PIDTASKINFO)?;
247 Some((
248 task.pti_resident_size,
249 Duration::from_nanos(ticks_to_nanos(
250 task.pti_total_user.saturating_add(task.pti_total_system),
251 )),
252 ))
253 },
254 |_, _| true,
255 )
256 }
257}
258
259#[cfg(windows)]
260mod sys {
261 use std::collections::HashMap;
262 use std::time::Duration;
263
264 use windows::Win32::Foundation::{CloseHandle, FILETIME};
265 use windows::Win32::System::Diagnostics::ToolHelp::{
266 CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
267 TH32CS_SNAPPROCESS,
268 };
269 use windows::Win32::System::ProcessStatus::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS};
270 use windows::Win32::System::Threading::{
271 GetProcessTimes, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
272 };
273
274 use super::{Facts, TreeUsage, trees};
275
276 fn parents() -> Vec<(u32, u32)> {
277 let Ok(snapshot) = (unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }) else {
280 return Vec::new();
281 };
282 let mut entry = PROCESSENTRY32W {
283 dwSize: u32::try_from(std::mem::size_of::<PROCESSENTRY32W>()).unwrap_or(0),
284 ..PROCESSENTRY32W::default()
285 };
286 let mut pairs = Vec::new();
287 let mut more = unsafe { Process32FirstW(snapshot, &raw mut entry) }.is_ok();
290 while more {
291 pairs.push((entry.th32ProcessID, entry.th32ParentProcessID));
292 more = unsafe { Process32NextW(snapshot, &raw mut entry) }.is_ok();
294 }
295 let _ = unsafe { CloseHandle(snapshot) };
297 pairs
298 }
299
300 fn hundreds(time: FILETIME) -> u64 {
302 (u64::from(time.dwHighDateTime) << 32) | u64::from(time.dwLowDateTime)
303 }
304
305 fn filetime(time: FILETIME) -> Duration {
306 Duration::from_nanos(hundreds(time).saturating_mul(100))
307 }
308
309 fn facts(pid: u32) -> Option<(Facts, Option<u64>)> {
312 let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) }.ok()?;
315 let mut memory = PROCESS_MEMORY_COUNTERS {
316 cb: u32::try_from(std::mem::size_of::<PROCESS_MEMORY_COUNTERS>()).unwrap_or(0),
317 ..PROCESS_MEMORY_COUNTERS::default()
318 };
319 let rss = if unsafe { GetProcessMemoryInfo(handle, &raw mut memory, memory.cb) }.is_ok() {
322 u64::try_from(memory.WorkingSetSize).unwrap_or(u64::MAX)
323 } else {
324 0
325 };
326 let (mut created, mut exited, mut kernel, mut user) = (
327 FILETIME::default(),
328 FILETIME::default(),
329 FILETIME::default(),
330 FILETIME::default(),
331 );
332 let timed = unsafe {
334 GetProcessTimes(
335 handle,
336 &raw mut created,
337 &raw mut exited,
338 &raw mut kernel,
339 &raw mut user,
340 )
341 };
342 let (cpu, born) = if timed.is_ok() {
343 (
344 filetime(kernel).saturating_add(filetime(user)),
345 Some(hundreds(created)),
346 )
347 } else {
348 (Duration::ZERO, None)
349 };
350 let _ = unsafe { CloseHandle(handle) };
352 Some(((rss, cpu), born))
353 }
354
355 pub(super) fn sample_trees(roots: &[u32]) -> HashMap<u32, TreeUsage> {
356 let known = std::cell::RefCell::new(HashMap::<u32, Option<(Facts, Option<u64>)>>::new());
357 let look = |pid: u32| *known.borrow_mut().entry(pid).or_insert_with(|| facts(pid));
358 trees(
359 roots,
360 &parents(),
361 |pid| look(pid).map(|(facts, _)| facts),
362 |child, parent| match (look(child), look(parent)) {
365 (Some((_, Some(child))), Some((_, Some(parent)))) => child >= parent,
366 _ => true,
367 },
368 )
369 }
370}
371
372#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
373mod sys {
374 use std::collections::HashMap;
375
376 use super::TreeUsage;
377
378 pub(super) fn sample_trees(_: &[u32]) -> HashMap<u32, TreeUsage> {
379 HashMap::new()
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 #[test]
388 fn a_stat_line_parses_whatever_the_command_name_holds() {
389 let line = "4242 (Runner.Worker (x) 1) S 4200 4242 4200 0 -1 4194560 100 0 0 0 \
390 250 50 7 3 20 0 12 0 123456 1048576000 25600 18446744073709551615";
391 assert_eq!(parse_proc_stat(line), Some((4200, 310, 25600)));
392 assert_eq!(parse_proc_stat("garbage"), None);
393 assert_eq!(parse_proc_stat("1 (init) S"), None);
394 }
395
396 #[test]
397 fn a_tree_sums_its_descendants_only_and_survives_a_cycle() {
398 let parents = [
399 (1, 0),
400 (10, 1),
401 (11, 10),
402 (12, 11),
403 (20, 1),
404 (30, 12),
405 (12, 30),
406 ];
407 let facts: HashMap<u32, Facts> = [
408 (1, (1, 1)),
409 (10, (100, 10)),
410 (11, (20, 2)),
411 (12, (30, 3)),
412 (20, (999, 99)),
413 (30, (5, 1)),
414 ]
415 .into_iter()
416 .map(|(pid, (rss, secs))| (pid, (rss, Duration::from_secs(secs))))
417 .collect();
418 let found = trees(
419 &[10, 99],
420 &parents,
421 |pid| facts.get(&pid).copied(),
422 |_, _| true,
423 );
424 assert_eq!(
425 found,
426 HashMap::from([(
427 10,
428 TreeUsage {
429 rss_bytes: 155,
430 cpu_time: Duration::from_secs(16),
431 processes: 4,
432 }
433 )]),
434 "the gone root 99 has no entry"
435 );
436 let unreadable = trees(&[10], &parents, |_| None, |_, _| true);
438 assert_eq!(unreadable[&10].processes, 4);
439 let cut = trees(
441 &[10],
442 &parents,
443 |pid| facts.get(&pid).copied(),
444 |child, _| child != 11,
445 );
446 assert_eq!(cut[&10].processes, 1);
447 }
448
449 #[cfg(unix)]
451 #[test]
452 fn a_spawned_process_tree_is_measured_on_this_os() {
453 let mut child = std::process::Command::new("sh")
456 .args([
457 "-c",
458 "sleep 30 & i=0; while [ $i -lt 200000 ]; do i=$((i+1)); done; wait",
459 ])
460 .spawn()
461 .expect("sh starts");
462 let root = child.id();
463 let mut best = None;
464 for _ in 0..50 {
465 std::thread::sleep(Duration::from_millis(100));
466 if let Some(usage) = sample_tree(root)
467 && usage.processes >= 2
468 && usage.cpu_time > Duration::ZERO
469 {
470 best = Some(usage);
471 break;
472 }
473 }
474 let _ = child.kill();
475 let _ = child.wait();
476 let usage = best.expect("the tree was seen with its child and some CPU");
477 assert!(usage.rss_bytes > 0, "{usage:?}");
478 assert_eq!(sample_tree(root), None, "a reaped root is gone");
479 }
480
481 #[cfg(windows)]
482 #[test]
483 fn a_spawned_process_tree_is_measured_on_this_os() {
484 let mut child = std::process::Command::new("cmd")
485 .args(["/C", "ping -n 6 127.0.0.1 > NUL"])
486 .spawn()
487 .expect("cmd starts");
488 let root = child.id();
489 let mut best = None;
490 for _ in 0..50 {
491 std::thread::sleep(Duration::from_millis(100));
492 if let Some(usage) = sample_tree(root)
493 && usage.processes >= 2
494 {
495 best = Some(usage);
496 break;
497 }
498 }
499 let _ = child.kill();
500 let _ = child.wait();
501 let usage = best.expect("the tree was seen with its child");
502 assert!(usage.rss_bytes > 0, "{usage:?}");
503 }
504}