Skip to main content

rightkit_child_process/
usage.rs

1//! Resource usage of the current process: the one shared source for the figures a runtime budget
2//! names (docs/RUNTIME-BUDGETS.md): resident memory, CPU time, wake-ups and disk operations.
3//!
4//! Computed on call from the operating system; no thread, no polling, no state. A host or a crate's
5//! own `status()` calls [`process_usage`]; two snapshots give rates through [`ProcessUsage::rates_since`].
6//!
7//! | Figure | macOS | Windows | Linux, Android |
8//! |---|---|---|---|
9//! | `resident_bytes` | `proc_pid_rusage` `ri_resident_size` | `WorkingSetSize` | `/proc/self/statm` |
10//! | `footprint_bytes` | `ri_phys_footprint` | `PrivateUsage` | same as resident |
11//! | `cpu_seconds` | `getrusage(RUSAGE_SELF)` user + system | `GetProcessTimes` user + kernel | `getrusage` |
12//! | `wakeups_total` | `ri_interrupt_wkups` (thread wake-ups from timer and other interrupts; `ri_pkg_idle_wkups` stays 0 whenever the machine is busy, so it cannot be used) | sum of `ContextSwitchCount` over the live threads of this process (`NtQuerySystemInformation(SystemProcessInformation)`); a thread is switched in once per wake. A thread that exits takes its count with it, so the sum can dip when threads end; 64-bit only | not available |
13//! | `disk_ops_total` | not available: `ru_inblock`/`ru_oublock` stay 0 on macOS and no per-process operation count exists; bytes are reported instead | `GetProcessIoCounters` read + write operation counts (all I/O, including pipes and sockets) | `ru_inblock + ru_oublock` |
14//!
15//! Other Unix targets compile, and `process_usage` returns `ErrorKind::Unsupported` there.
16//!
17//! [`process_usage_with_threads`] and [`observe_idle_with_threads`] add a per-thread breakdown for
18//! finding which thread causes wake-ups. On Windows the per-thread `ContextSwitchCount` values come
19//! from the very table the total is summed from, so their sum is exactly `wakeups_total`. macOS and
20//! Linux list the threads (id and name) but report no per-thread wake-up counter (`None`): the macOS
21//! figure is a process-level interrupt count that does not decompose by thread.
22//!
23//! A figure the host cannot report is `None`; it is never reported as zero.
24
25use std::io;
26use std::time::Duration;
27
28/// One snapshot of the current process's resource usage.
29#[derive(Clone, Copy, Debug, Default, PartialEq)]
30pub struct ProcessUsage {
31    /// Resident set (working set on Windows) in bytes.
32    pub resident_bytes: u64,
33    /// Private footprint in bytes: macOS `phys_footprint`, Windows `PrivateUsage`.
34    pub footprint_bytes: u64,
35    /// Process CPU time (user plus system) since process start, in seconds.
36    pub cpu_seconds: f64,
37    /// Thread wake-ups (timer and other interrupts) since process start, when the host reports them.
38    pub wakeups_total: Option<u64>,
39    /// Disk (block or I/O) operations since process start, when the host reports them.
40    pub disk_ops_total: Option<u64>,
41    /// Bytes read from storage since process start, when the host reports them.
42    pub disk_read_bytes: Option<u64>,
43    /// Bytes written to storage since process start, when the host reports them.
44    pub disk_write_bytes: Option<u64>,
45}
46
47/// Rates between two snapshots. Each rate is `None` when either snapshot lacks the counter.
48#[derive(Clone, Copy, Debug, Default, PartialEq)]
49pub struct UsageRates {
50    /// Process CPU percent of one core (100 = one full core) over the interval.
51    pub cpu_percent: f64,
52    pub wakeups_per_second: Option<f64>,
53    pub disk_ops_per_second: Option<f64>,
54    pub disk_read_bytes_per_second: Option<f64>,
55    pub disk_write_bytes_per_second: Option<f64>,
56}
57
58impl ProcessUsage {
59    /// Rates of change from `earlier` (an older snapshot) to `self` over `elapsed`.
60    /// A zero `elapsed` yields zero rates rather than dividing by zero.
61    pub fn rates_since(&self, earlier: &ProcessUsage, elapsed: Duration) -> UsageRates {
62        let secs = elapsed.as_secs_f64();
63        let rate = |now: Option<u64>, then: Option<u64>| match (now, then) {
64            (Some(now), Some(then)) if secs > 0.0 => Some(now.saturating_sub(then) as f64 / secs),
65            (Some(_), Some(_)) => Some(0.0),
66            _ => None,
67        };
68        UsageRates {
69            cpu_percent: if secs > 0.0 {
70                (self.cpu_seconds - earlier.cpu_seconds).max(0.0) / secs * 100.0
71            } else {
72                0.0
73            },
74            wakeups_per_second: rate(self.wakeups_total, earlier.wakeups_total),
75            disk_ops_per_second: rate(self.disk_ops_total, earlier.disk_ops_total),
76            disk_read_bytes_per_second: rate(self.disk_read_bytes, earlier.disk_read_bytes),
77            disk_write_bytes_per_second: rate(self.disk_write_bytes, earlier.disk_write_bytes),
78        }
79    }
80}
81
82/// Resource usage of the current process, computed now.
83pub fn process_usage() -> io::Result<ProcessUsage> {
84    imp::sample()
85}
86
87/// Resident memory summed over one remembered process and the descendants found while it
88/// lived. Members are counted only while their pid-plus-start-time identity still verifies
89/// after the table snapshot, so a pid recycled during the scan contributes nothing.
90#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
91pub struct TreeUsage {
92    /// The root plus descendants that were still the remembered processes at recheck.
93    pub processes: usize,
94    /// Summed resident (working-set) bytes of the members the OS reported a figure for.
95    pub resident_bytes: u64,
96    /// Members still present but with no readable resident figure.
97    pub unreadable: usize,
98}
99
100/// [`TreeUsage`] for `root` and its current descendants (see
101/// [`AdoptedIdentity::descendants`](crate::AdoptedIdentity::descendants) for which
102/// descendants can be found). A root that is gone or replaced yields an empty usage, not
103/// an error. Member figures come from one [`process_table`](crate::process_table)
104/// snapshot, then every member is verified again before it is counted.
105pub fn tree_usage(root: &crate::AdoptedIdentity) -> io::Result<TreeUsage> {
106    if root.verify()? != crate::IdentityCheck::Same {
107        return Ok(TreeUsage::default());
108    }
109    let mut members = vec![root.clone()];
110    members.extend(root.descendants()?);
111    let table = crate::process_table()?;
112    let mut usage = TreeUsage::default();
113    for member in &members {
114        if member.verify()? != crate::IdentityCheck::Same {
115            continue;
116        }
117        usage.processes += 1;
118        match table
119            .iter()
120            .find(|entry| entry.pid == member.pid())
121            .and_then(|entry| entry.working_set_bytes)
122        {
123            Some(bytes) => usage.resident_bytes = usage.resident_bytes.saturating_add(bytes),
124            None => usage.unreadable += 1,
125        }
126    }
127    Ok(usage)
128}
129
130/// What a process did over a quiet window: the idle figures a runtime budget names.
131#[derive(Clone, Copy, Debug)]
132pub struct IdleObservation {
133    pub elapsed: Duration,
134    pub start: ProcessUsage,
135    pub end: ProcessUsage,
136    pub rates: UsageRates,
137}
138
139/// Sleeps `duration` on the calling thread and reports the process's usage over it. Run it after
140/// the load has stopped; it is for benches and tests, so a host reads [`process_usage`] instead.
141pub fn observe_idle(duration: Duration) -> io::Result<IdleObservation> {
142    let start = process_usage()?;
143    let began = std::time::Instant::now();
144    std::thread::sleep(duration);
145    let end = process_usage()?;
146    let elapsed = began.elapsed();
147    Ok(IdleObservation {
148        elapsed,
149        start,
150        end,
151        rates: end.rates_since(&start, elapsed),
152    })
153}
154
155/// One thread of the current process at the moment of a snapshot.
156///
157/// A figure the host cannot report for a thread is `None`, never zero.
158#[derive(Clone, Debug, PartialEq)]
159#[non_exhaustive]
160pub struct ThreadSample {
161    /// OS thread id: the Windows thread id, the Linux tid, or the macOS libproc thread handle.
162    pub thread_id: u64,
163    /// Creation time (Windows FILETIME ticks). Thread ids are reused, so `(thread_id, started)` is
164    /// what identifies a thread across snapshots. `None` where the host reports no creation time.
165    pub started: Option<u64>,
166    /// Wake-ups since the thread started: Windows `ContextSwitchCount` (one switch-in per wake).
167    /// `None` on macOS and Linux.
168    pub wakeups: Option<u64>,
169    /// CPU time (user plus kernel) since the thread started, in seconds. Windows only.
170    pub cpu_seconds: Option<f64>,
171    /// Thread name when the thread set one (Windows `SetThreadDescription`, Linux `comm`, macOS
172    /// `pth_name`); `None` when it has none or the thread was gone before it could be read.
173    pub name: Option<String>,
174    /// Windows Win32 start address of the thread (the routine handed to `CreateThread`, which for
175    /// a Rust thread is the std trampoline, so it tells a pool from a one-off but not the closure);
176    /// `None` elsewhere or when unreadable.
177    pub start_address: Option<u64>,
178}
179
180/// A [`ProcessUsage`] together with the threads it was summed from.
181#[derive(Clone, Debug, PartialEq)]
182pub struct ProcessThreads {
183    pub usage: ProcessUsage,
184    /// Live threads of this process. On Windows `usage.wakeups_total` is exactly the sum of their
185    /// `wakeups` (see [`ProcessThreads::thread_wakeups_sum`]).
186    pub threads: Vec<ThreadSample>,
187}
188
189impl ProcessThreads {
190    /// Sum of the per-thread wake-up counters; `None` when any thread lacks one.
191    pub fn thread_wakeups_sum(&self) -> Option<u64> {
192        sum_wakeups(&self.threads)
193    }
194}
195
196fn sum_wakeups(threads: &[ThreadSample]) -> Option<u64> {
197    threads
198        .iter()
199        .try_fold(0u64, |total, thread| total.checked_add(thread.wakeups?))
200}
201
202/// Resource usage of the current process and its per-thread breakdown, from one snapshot.
203///
204/// More expensive than [`process_usage`] (it opens each thread to read its name and start
205/// address), so it is for benches and diagnosis. A thread that exits while the names are read
206/// keeps its counters and has no name. Fails when the host cannot list threads (Windows when the
207/// process table is unreadable, other Unix targets with `ErrorKind::Unsupported`).
208pub fn process_usage_with_threads() -> io::Result<ProcessThreads> {
209    let (usage, mut threads) = imp::sample_threads()?;
210    imp::enrich(&mut threads);
211    Ok(ProcessThreads { usage, threads })
212}
213
214/// What one thread did between two snapshots.
215#[derive(Clone, Debug, PartialEq)]
216pub struct ThreadDelta {
217    pub thread_id: u64,
218    /// Wake-ups in the window; `None` when either snapshot lacks the counter.
219    pub wakeups: Option<u64>,
220    /// CPU seconds in the window; `None` when either snapshot lacks it.
221    pub cpu_seconds: Option<f64>,
222    pub name: Option<String>,
223    pub start_address: Option<u64>,
224    /// The thread was not in the first snapshot: it began inside the window, so its whole count
225    /// (and CPU time) is the window's.
226    pub started_in_window: bool,
227}
228
229/// Per-thread change between two snapshots (see [`thread_deltas`]).
230#[derive(Clone, Debug, PartialEq)]
231pub struct ThreadDeltas {
232    /// Threads present in the second snapshot, most wake-ups first (unknown counts last, ties by
233    /// thread id).
234    pub threads: Vec<ThreadDelta>,
235    /// Threads in the first snapshot that were gone by the second (as they were at the first).
236    /// Their wake-ups inside the window are unknowable; that is a normal outcome, not an error.
237    pub exited: Vec<ThreadSample>,
238}
239
240/// Per-thread deltas from `start` to `end`. Threads are matched by `(thread_id, started)`, so a
241/// reused id is a new thread, not a continuation. A counter that went backwards counts as zero.
242pub fn thread_deltas(start: &[ThreadSample], end: &[ThreadSample]) -> ThreadDeltas {
243    use std::collections::HashMap;
244    let key = |thread: &ThreadSample| (thread.thread_id, thread.started);
245    let mut earlier: HashMap<(u64, Option<u64>), &ThreadSample> =
246        start.iter().map(|thread| (key(thread), thread)).collect();
247    let mut threads: Vec<ThreadDelta> = end
248        .iter()
249        .map(|now| {
250            let before = earlier.remove(&key(now));
251            ThreadDelta {
252                thread_id: now.thread_id,
253                wakeups: match before {
254                    Some(then) => counter_delta(now.wakeups, then.wakeups),
255                    None => now.wakeups,
256                },
257                cpu_seconds: match before {
258                    Some(then) => match (now.cpu_seconds, then.cpu_seconds) {
259                        (Some(now), Some(then)) => Some((now - then).max(0.0)),
260                        _ => None,
261                    },
262                    None => now.cpu_seconds,
263                },
264                name: now
265                    .name
266                    .clone()
267                    .or_else(|| before.and_then(|then| then.name.clone())),
268                start_address: now
269                    .start_address
270                    .or_else(|| before.and_then(|then| then.start_address)),
271                started_in_window: before.is_none(),
272            }
273        })
274        .collect();
275    threads.sort_by(|a, b| {
276        b.wakeups
277            .cmp(&a.wakeups)
278            .then(a.thread_id.cmp(&b.thread_id))
279    });
280    let mut exited: Vec<ThreadSample> = earlier.into_values().cloned().collect();
281    exited.sort_by_key(|thread| thread.thread_id);
282    ThreadDeltas { threads, exited }
283}
284
285fn counter_delta(now: Option<u64>, then: Option<u64>) -> Option<u64> {
286    Some(now?.saturating_sub(then?))
287}
288
289impl ThreadDeltas {
290    /// Sum of the listed threads' wake-ups; `None` when any is unknown. Minus the exited threads'
291    /// counts at the first snapshot ([`ThreadDeltas::exited_wakeups_at_start`]) it equals the
292    /// whole-process wake-up delta between the same two snapshots.
293    pub fn total_wakeups(&self) -> Option<u64> {
294        self.threads
295            .iter()
296            .try_fold(0u64, |total, thread| total.checked_add(thread.wakeups?))
297    }
298
299    /// Sum of the exited threads' wake-up counters at the first snapshot; `None` when unknown.
300    pub fn exited_wakeups_at_start(&self) -> Option<u64> {
301        sum_wakeups(&self.exited)
302    }
303
304    /// The `n` threads with the most wake-ups.
305    pub fn top(&self, n: usize) -> &[ThreadDelta] {
306        &self.threads[..n.min(self.threads.len())]
307    }
308
309    /// Evidence lines for the top `top` threads over a window of `elapsed`, in the
310    /// `RESULT <key> ...` style: one `RESULT thread_wakeups_summary ...` line, then one
311    /// `RESULT thread_wakeups rank=.. tid=.. wakeups=.. per_second=.. cpu_ms=.. born=0|1
312    /// start=0x.. name=".."` line per thread with a non-zero or unknown delta. Neither key is
313    /// `budget`, so the runtime-budget gate (which reads only the `RESULT budget` line) ignores them.
314    pub fn result_lines(&self, elapsed: Duration, top: usize) -> String {
315        let secs = elapsed.as_secs_f64();
316        let count = |v: Option<u64>| v.map_or_else(|| "unavailable".to_string(), |v| v.to_string());
317        let mut out = format!(
318            "RESULT thread_wakeups_summary threads={} born={} exited={} listed_wakeups={} exited_wakeups_at_start={} window_seconds={:.1}\n",
319            self.threads.len(),
320            self.threads.iter().filter(|t| t.started_in_window).count(),
321            self.exited.len(),
322            count(self.total_wakeups()),
323            count(self.exited_wakeups_at_start()),
324            secs,
325        );
326        let listed = self
327            .threads
328            .iter()
329            .filter(|t| t.wakeups != Some(0))
330            .take(top);
331        for (index, thread) in listed.enumerate() {
332            let per_second = match thread.wakeups {
333                Some(w) if secs > 0.0 => format!("{:.3}", w as f64 / secs),
334                Some(_) => "0.000".to_string(),
335                None => "unavailable".to_string(),
336            };
337            out.push_str(&format!(
338                "RESULT thread_wakeups rank={} tid={} wakeups={} per_second={per_second} cpu_ms={} born={} start={} name={}\n",
339                index + 1,
340                thread.thread_id,
341                count(thread.wakeups),
342                thread
343                    .cpu_seconds
344                    .map_or_else(|| "unavailable".to_string(), |s| format!("{:.1}", s * 1e3)),
345                u8::from(thread.started_in_window),
346                thread
347                    .start_address
348                    .map_or_else(|| "unavailable".to_string(), |a| format!("0x{a:x}")),
349                thread
350                    .name
351                    .as_ref()
352                    .map_or_else(|| "unavailable".to_string(), |n| format!("{n:?}")),
353            ));
354        }
355        out
356    }
357}
358
359/// An [`IdleObservation`] plus what each thread did over the same window.
360#[derive(Clone, Debug)]
361pub struct IdleThreadObservation {
362    /// Rates are computed from the same two snapshots the thread deltas come from.
363    pub observation: IdleObservation,
364    pub threads: ThreadDeltas,
365}
366
367/// [`observe_idle`] that also records per-thread deltas, so a wake-up overage can be attributed to
368/// threads. The thread snapshots and the process figures are the same two samples; names and
369/// start addresses are read after each sample, outside the measured counts.
370pub fn observe_idle_with_threads(duration: Duration) -> io::Result<IdleThreadObservation> {
371    let (start, mut start_threads) = imp::sample_threads()?;
372    let began = std::time::Instant::now();
373    imp::enrich(&mut start_threads);
374    std::thread::sleep(duration.saturating_sub(began.elapsed()));
375    let (end, mut end_threads) = imp::sample_threads()?;
376    let elapsed = began.elapsed();
377    imp::enrich(&mut end_threads);
378    Ok(IdleThreadObservation {
379        observation: IdleObservation {
380            elapsed,
381            start,
382            end,
383            rates: end.rates_since(&start, elapsed),
384        },
385        threads: thread_deltas(&start_threads, &end_threads),
386    })
387}
388
389/// Names and scale of the load behind an [`IdleObservation`], for [`IdleObservation::result_lines`].
390#[derive(Clone, Copy, Debug)]
391pub struct BudgetLabels<'a> {
392    /// Crate being measured, e.g. `rightkit-cache`.
393    pub krate: &'a str,
394    pub version: &'a str,
395    /// Unit of `scale`, e.g. `entries`.
396    pub scale_unit: &'a str,
397    /// How much load the crate carried.
398    pub scale: u64,
399    /// Plain-words description of that load.
400    pub what: &'a str,
401    /// `synthetic`, `real` or `installed`.
402    pub scope: &'a str,
403}
404
405impl IdleObservation {
406    /// The stable machine-readable lines the runtime-budget gate records, one figure per line:
407    /// `RESULT <key>=<number> <unit>`, or `RESULT <key>=unavailable <reason>` when the host cannot
408    /// report the figure (never a stand-in zero). `resident_memory_bytes` is the resident set at the
409    /// end of the idle window. `scale` and `what` state the load the crate carried; `scope` is
410    /// `synthetic`, `real` or `installed`.
411    ///
412    /// `resident_memory_bytes` is the larger of the resident set `warm` (taken right after the load)
413    /// and at the end of the idle window: an OS may compress or page out idle memory, and a ceiling
414    /// must cover the loaded state, not only the reclaimed one. The two are also printed separately.
415    ///
416    /// It also prints the single gate line `RESULT budget crate=.. version=.. resident_bytes=..
417    /// idle_cpu_percent=.. wakeups_per_s=.. disk_ops_per_s=.. scale_unit=.. scale_count=..`
418    /// (docs/RUNTIME-BUDGETS.md); a figure the host cannot report prints `unavailable` there, which
419    /// the gate refuses, and is never replaced by zero.
420    pub fn result_lines(&self, warm: &ProcessUsage, labels: &BudgetLabels<'_>) -> String {
421        let BudgetLabels {
422            krate,
423            version,
424            scale_unit,
425            scale,
426            what,
427            scope,
428        } = *labels;
429        let mut out = String::new();
430        let mut line = |text: String| {
431            out.push_str("RESULT ");
432            out.push_str(&text);
433            out.push('\n');
434        };
435        let resident_max = warm.resident_bytes.max(self.end.resident_bytes);
436        line(format!("warm_resident_bytes={} bytes", warm.resident_bytes));
437        line(format!(
438            "idle_resident_bytes={} bytes",
439            self.end.resident_bytes
440        ));
441        if resident_max > 0 {
442            line(format!("resident_memory_bytes={resident_max} bytes"));
443        } else {
444            line("resident_memory_bytes=unavailable host reported a zero resident set".to_string());
445        }
446        line(format!(
447            "footprint_bytes={} bytes",
448            warm.footprint_bytes.max(self.end.footprint_bytes)
449        ));
450        line(format!(
451            "idle_window_seconds={:.1} seconds",
452            self.elapsed.as_secs_f64()
453        ));
454        line(format!(
455            "idle_cpu_percent={:.3} percent",
456            self.rates.cpu_percent
457        ));
458        let opt = |name: &str,
459                   value: Option<f64>,
460                   unit: &str,
461                   reason: &str,
462                   line: &mut dyn FnMut(String)| match value {
463            Some(v) => line(format!("{name}={v:.3} {unit}")),
464            None => line(format!("{name}=unavailable {reason}")),
465        };
466        opt(
467            "wakeups_per_second",
468            self.rates.wakeups_per_second,
469            "per_second",
470            "the host exposes no process wake-up counter",
471            &mut line,
472        );
473        opt(
474            "disk_ops_per_second",
475            self.rates.disk_ops_per_second,
476            "per_second",
477            "the host exposes no process disk-operation counter",
478            &mut line,
479        );
480        opt(
481            "disk_read_bytes_per_second",
482            self.rates.disk_read_bytes_per_second,
483            "bytes_per_second",
484            "the host exposes no process disk-byte counter",
485            &mut line,
486        );
487        opt(
488            "disk_write_bytes_per_second",
489            self.rates.disk_write_bytes_per_second,
490            "bytes_per_second",
491            "the host exposes no process disk-byte counter",
492            &mut line,
493        );
494        line(format!("scale={scale} {what}"));
495        line(format!("scope={scope}"));
496        let num =
497            |v: Option<f64>| v.map_or_else(|| "unavailable".to_string(), |v| format!("{v:.3}"));
498        let resident = if resident_max > 0 {
499            resident_max.to_string()
500        } else {
501            "unavailable".to_string()
502        };
503        line(format!(
504            "budget crate={krate} version={version} resident_bytes={resident} idle_cpu_percent={:.3} wakeups_per_s={} disk_ops_per_s={} scale_unit={scale_unit} scale_count={scale}",
505            self.rates.cpu_percent,
506            num(self.rates.wakeups_per_second),
507            num(self.rates.disk_ops_per_second),
508        ));
509        out
510    }
511}
512
513#[cfg(target_os = "macos")]
514mod imp {
515    use super::{ProcessUsage, ThreadSample};
516    use std::io;
517
518    extern "C" {
519        fn proc_pid_rusage(pid: i32, flavor: i32, buffer: *mut u8) -> i32;
520    }
521
522    /// RUSAGE_INFO_V2 words after the 16-byte uuid (offsets from the SDK's libproc.h
523    /// `struct rusage_info_v2`).
524    const W_INTERRUPT_WKUPS: usize = 2 + 3;
525    const W_RESIDENT: usize = 2 + 6;
526    const W_FOOTPRINT: usize = 2 + 7;
527    const W_DISK_READ: usize = 2 + 16;
528    const W_DISK_WRITE: usize = 2 + 17;
529
530    pub fn sample() -> io::Result<ProcessUsage> {
531        let mut words = [0u64; 64];
532        // RUSAGE_INFO_V2 = 2.
533        let rc = unsafe { proc_pid_rusage(libc::getpid(), 2, words.as_mut_ptr().cast()) };
534        if rc != 0 {
535            return Err(io::Error::last_os_error());
536        }
537        let mut ru: libc::rusage = unsafe { std::mem::zeroed() };
538        if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut ru) } != 0 {
539            return Err(io::Error::last_os_error());
540        }
541        let secs = |t: libc::timeval| t.tv_sec as f64 + t.tv_usec as f64 / 1e6;
542        Ok(ProcessUsage {
543            resident_bytes: words[W_RESIDENT],
544            footprint_bytes: words[W_FOOTPRINT],
545            cpu_seconds: secs(ru.ru_utime) + secs(ru.ru_stime),
546            wakeups_total: Some(words[W_INTERRUPT_WKUPS]),
547            disk_ops_total: None,
548            disk_read_bytes: Some(words[W_DISK_READ]),
549            disk_write_bytes: Some(words[W_DISK_WRITE]),
550        })
551    }
552
553    /// `PROC_PIDLISTTHREADS` from libproc.h: the thread handles of a process (`libc` does not
554    /// export it; `PROC_PIDTHREADINFO` is the flavor that takes one of those handles).
555    const PROC_PIDLISTTHREADS: i32 = 6;
556    /// Upper bound on the thread handles read (a process with more is refused, not truncated).
557    const MAX_THREADS: usize = 1 << 16;
558
559    /// The live threads of this process with their names. libproc offers no per-thread wake-up
560    /// counter, so `wakeups` is `None`. A thread whose info cannot be read (it exited after the
561    /// listing) is left out: it counts as exited, not as an error.
562    fn threads() -> io::Result<Vec<ThreadSample>> {
563        let pid = unsafe { libc::getpid() };
564        let mut capacity = 64usize;
565        let handles = loop {
566            let mut handles = vec![0u64; capacity];
567            let bytes = i32::try_from(capacity * std::mem::size_of::<u64>())
568                .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "thread list too large"))?;
569            // SAFETY: `handles` is a writable buffer of exactly `bytes` bytes.
570            let written = unsafe {
571                libc::proc_pidinfo(
572                    pid,
573                    PROC_PIDLISTTHREADS,
574                    0,
575                    handles.as_mut_ptr().cast(),
576                    bytes,
577                )
578            };
579            if written <= 0 {
580                return Err(io::Error::last_os_error());
581            }
582            let count = written as usize / std::mem::size_of::<u64>();
583            // A full buffer may have been truncated: grow and ask again.
584            if count < capacity {
585                handles.truncate(count);
586                break handles;
587            }
588            capacity *= 2;
589            if capacity > MAX_THREADS {
590                return Err(io::Error::new(
591                    io::ErrorKind::InvalidData,
592                    "too many threads to list",
593                ));
594            }
595        };
596        let size = std::mem::size_of::<libc::proc_threadinfo>() as i32;
597        let mut threads = Vec::with_capacity(handles.len());
598        for handle in handles {
599            let mut info: libc::proc_threadinfo = unsafe { std::mem::zeroed() };
600            // SAFETY: `info` is a writable `proc_threadinfo` of exactly `size` bytes.
601            let read = unsafe {
602                libc::proc_pidinfo(
603                    pid,
604                    libc::PROC_PIDTHREADINFO,
605                    handle,
606                    (&mut info as *mut libc::proc_threadinfo).cast(),
607                    size,
608                )
609            };
610            if read != size {
611                continue;
612            }
613            let bytes: Vec<u8> = info
614                .pth_name
615                .iter()
616                .map(|&c| c as u8)
617                .take_while(|&c| c != 0)
618                .collect();
619            let name = String::from_utf8_lossy(&bytes).into_owned();
620            threads.push(ThreadSample {
621                thread_id: handle,
622                started: None,
623                wakeups: None,
624                cpu_seconds: None,
625                name: (!name.is_empty()).then_some(name),
626                start_address: None,
627            });
628        }
629        Ok(threads)
630    }
631
632    /// The process figures, then the thread list (two reads, not one atomic snapshot: macOS
633    /// reports no per-thread counter for them to reconcile with).
634    pub fn sample_threads() -> io::Result<(ProcessUsage, Vec<ThreadSample>)> {
635        let usage = sample()?;
636        Ok((usage, threads()?))
637    }
638
639    /// Names are read while listing; nothing more is available.
640    pub fn enrich(_threads: &mut [ThreadSample]) {}
641}
642
643#[cfg(all(
644    unix,
645    not(target_os = "macos"),
646    not(any(target_os = "linux", target_os = "android"))
647))]
648mod imp {
649    use super::{ProcessUsage, ThreadSample};
650    use std::io;
651
652    /// Chosen when the crate is compiled for a Unix this module has no sampler for: an
653    /// explicit, typed error rather than a procfs read that may not exist.
654    pub fn sample() -> io::Result<ProcessUsage> {
655        Err(io::Error::new(
656            io::ErrorKind::Unsupported,
657            "process usage sampling is implemented for macOS, Linux, Android and Windows only",
658        ))
659    }
660
661    pub fn sample_threads() -> io::Result<(ProcessUsage, Vec<ThreadSample>)> {
662        sample().map(|usage| (usage, Vec::new()))
663    }
664
665    pub fn enrich(_threads: &mut [ThreadSample]) {}
666}
667
668#[cfg(any(target_os = "linux", target_os = "android"))]
669mod imp {
670    use super::{ProcessUsage, ThreadSample};
671    use std::io;
672
673    pub fn sample() -> io::Result<ProcessUsage> {
674        let statm = std::fs::read_to_string("/proc/self/statm")?;
675        let pages: u64 = statm
676            .split_whitespace()
677            .nth(1)
678            .and_then(|v| v.parse().ok())
679            .ok_or_else(|| {
680                io::Error::new(io::ErrorKind::InvalidData, "unreadable /proc/self/statm")
681            })?;
682        let page = u64::try_from(unsafe { libc::sysconf(libc::_SC_PAGESIZE) }.max(1))
683            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "page size out of range"))?;
684        let overflow = || io::Error::new(io::ErrorKind::InvalidData, "memory size overflows u64");
685        let resident = pages.checked_mul(page).ok_or_else(overflow)?;
686        // Block counters are signed C longs (32-bit on 32-bit targets): widen before adding.
687        let blocks = |count: libc::c_long| u64::try_from(count.max(0)).unwrap_or(0);
688        let mut ru: libc::rusage = unsafe { std::mem::zeroed() };
689        if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut ru) } != 0 {
690            return Err(io::Error::last_os_error());
691        }
692        let secs = |t: libc::timeval| t.tv_sec as f64 + t.tv_usec as f64 / 1e6;
693        Ok(ProcessUsage {
694            resident_bytes: resident,
695            footprint_bytes: resident,
696            cpu_seconds: secs(ru.ru_utime) + secs(ru.ru_stime),
697            wakeups_total: None,
698            disk_ops_total: blocks(ru.ru_inblock).checked_add(blocks(ru.ru_oublock)),
699            disk_read_bytes: None,
700            disk_write_bytes: None,
701        })
702    }
703
704    /// The live threads (`/proc/self/task`) with their `comm` names. No per-thread wake-up
705    /// counter is reported (the process total is `None` here too). A thread that exits while
706    /// the directory is walked is skipped: it counts as exited, not as an error.
707    fn threads() -> io::Result<Vec<ThreadSample>> {
708        let mut threads = Vec::new();
709        for entry in std::fs::read_dir("/proc/self/task")? {
710            let Ok(entry) = entry else { continue };
711            let Some(tid) = entry
712                .file_name()
713                .to_str()
714                .and_then(|n| n.parse::<u64>().ok())
715            else {
716                continue;
717            };
718            let Ok(comm) = std::fs::read_to_string(entry.path().join("comm")) else {
719                continue;
720            };
721            let name = comm.trim_end().to_string();
722            threads.push(ThreadSample {
723                thread_id: tid,
724                started: None,
725                wakeups: None,
726                cpu_seconds: None,
727                name: (!name.is_empty()).then_some(name),
728                start_address: None,
729            });
730        }
731        Ok(threads)
732    }
733
734    pub fn sample_threads() -> io::Result<(ProcessUsage, Vec<ThreadSample>)> {
735        let usage = sample()?;
736        Ok((usage, threads()?))
737    }
738
739    /// Names are read while listing; nothing more is available.
740    pub fn enrich(_threads: &mut [ThreadSample]) {}
741}
742
743#[cfg(windows)]
744mod imp {
745    use super::{ProcessUsage, ThreadSample};
746    use std::io;
747    use windows::Wdk::System::SystemInformation::{
748        NtQuerySystemInformation, SystemProcessInformation,
749    };
750    use windows::Win32::Foundation::{
751        CloseHandle, LocalFree, FILETIME, HANDLE, HLOCAL, STATUS_INFO_LENGTH_MISMATCH,
752    };
753    use windows::Win32::System::ProcessStatus::{
754        GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS, PROCESS_MEMORY_COUNTERS_EX,
755    };
756    use windows::Win32::System::Threading::{
757        GetCurrentProcess, GetCurrentProcessId, GetProcessIoCounters, GetProcessTimes,
758        GetThreadDescription, GetThreadTimes, OpenThread, IO_COUNTERS, THREAD_QUERY_INFORMATION,
759        THREAD_QUERY_LIMITED_INFORMATION,
760    };
761    use windows::Win32::System::WindowsProgramming::{
762        SYSTEM_PROCESS_INFORMATION, SYSTEM_THREAD_INFORMATION,
763    };
764
765    fn os(error: windows::core::Error) -> io::Error {
766        io::Error::from_raw_os_error(error.code().0 & 0xFFFF)
767    }
768
769    fn ticks(t: FILETIME) -> u64 {
770        ((t.dwHighDateTime as u64) << 32) | t.dwLowDateTime as u64
771    }
772
773    /// Bytes from the start of a `SYSTEM_PROCESS_INFORMATION` entry to its first
774    /// `SYSTEM_THREAD_INFORMATION` (the Windows 10/11 x64 layout; the `windows` crate's public
775    /// struct is 8 bytes shorter than the real one, so `size_of` cannot be used).
776    #[cfg(target_pointer_width = "64")]
777    const THREADS_OFFSET: usize = 0x100;
778    /// Upper bound on the process-table buffer.
779    #[cfg(target_pointer_width = "64")]
780    const MAX_QUERY_BYTES: usize = 256 * 1024 * 1024;
781
782    /// Every live thread of this process from one `SystemProcessInformation` snapshot, with the
783    /// counters that snapshot holds. `ContextSwitchCount` is the `u32` the crate names `Reserved3`
784    /// in `SYSTEM_THREAD_INFORMATION` (offset 0x40, right after `BasePriority`); the two times and
785    /// the creation time are the three `i64` the crate names `Reserved1` (kernel, user, create).
786    /// `wakeups` is always `Some` here. `None` on any failure or unexpected layout; never panics.
787    /// The total wake-up figure is the sum over this same vector, so the two can never disagree.
788    #[cfg(target_pointer_width = "64")]
789    fn thread_table() -> Option<Vec<ThreadSample>> {
790        // 8-byte aligned backing store; entry offsets are multiples of 8.
791        let mut words: Vec<u64> = vec![0; (1 << 20) / 8];
792        let mut length: u32 = 0;
793        let mut attempts = 0;
794        loop {
795            let capacity = words.len() * 8;
796            let status = unsafe {
797                NtQuerySystemInformation(
798                    SystemProcessInformation,
799                    words.as_mut_ptr().cast(),
800                    u32::try_from(capacity).ok()?,
801                    &mut length,
802                )
803            };
804            if status.0 >= 0 {
805                break;
806            }
807            attempts += 1;
808            if status != STATUS_INFO_LENGTH_MISMATCH || attempts > 8 {
809                return None;
810            }
811            // The process table can grow between calls: ask for a margin over the reported size.
812            let want = (length as usize).checked_add(256 * 1024)?;
813            if want <= capacity || want > MAX_QUERY_BYTES {
814                return None;
815            }
816            words = vec![0; want.div_ceil(8)];
817        }
818        let valid = (length as usize).min(words.len() * 8);
819        let base = words.as_ptr().cast::<u8>();
820        let pid = unsafe { GetCurrentProcessId() } as usize;
821        let thread_size = std::mem::size_of::<SYSTEM_THREAD_INFORMATION>();
822        let mut offset = 0usize;
823        loop {
824            if offset.checked_add(THREADS_OFFSET)? > valid {
825                return None;
826            }
827            // SAFETY: `offset + THREADS_OFFSET <= valid <= buffer size` covers the header read.
828            let entry = unsafe {
829                std::ptr::read_unaligned(base.add(offset).cast::<SYSTEM_PROCESS_INFORMATION>())
830            };
831            if entry.UniqueProcessId.0 as usize == pid {
832                let count = entry.NumberOfThreads as usize;
833                let first = offset + THREADS_OFFSET;
834                let end = first.checked_add(count.checked_mul(thread_size)?)?;
835                if end > valid {
836                    return None;
837                }
838                let mut threads = Vec::with_capacity(count);
839                for i in 0..count {
840                    // SAFETY: `first + (i + 1) * thread_size <= end <= valid`.
841                    let thread = unsafe {
842                        std::ptr::read_unaligned(
843                            base.add(first + i * thread_size)
844                                .cast::<SYSTEM_THREAD_INFORMATION>(),
845                        )
846                    };
847                    // Layout guard: every thread of this process names this process.
848                    if thread.ClientId.UniqueProcess.0 as usize != pid {
849                        return None;
850                    }
851                    let [kernel, user, created] = thread.Reserved1;
852                    threads.push(ThreadSample {
853                        thread_id: thread.ClientId.UniqueThread.0 as usize as u64,
854                        started: u64::try_from(created).ok(),
855                        wakeups: Some(u64::from(thread.Reserved3)),
856                        // FILETIME-style ticks are 100 ns.
857                        cpu_seconds: Some(
858                            (kernel.max(0) as u64).saturating_add(user.max(0) as u64) as f64 / 1e7,
859                        ),
860                        name: None,
861                        start_address: None,
862                    });
863                }
864                return Some(threads);
865            }
866            let next = entry.NextEntryOffset as usize;
867            if next == 0 {
868                return None;
869            }
870            offset = offset.checked_add(next)?;
871        }
872    }
873
874    #[cfg(not(target_pointer_width = "64"))]
875    fn thread_table() -> Option<Vec<ThreadSample>> {
876        None
877    }
878
879    /// Closes the thread handle it owns, exactly once.
880    struct ThreadHandle(HANDLE);
881
882    impl Drop for ThreadHandle {
883        fn drop(&mut self) {
884            // SAFETY: the handle came from `OpenThread` and is closed only here.
885            let _ = unsafe { CloseHandle(self.0) };
886        }
887    }
888
889    /// Opens a thread for querying. A thread that exited since the snapshot cannot be opened:
890    /// that is a normal outcome (`None`), not an error. Asks for `THREAD_QUERY_INFORMATION`
891    /// (the start-address query needs it) and falls back to the limited right (enough for the
892    /// description) when that is denied.
893    fn open_thread(id: u32) -> Option<ThreadHandle> {
894        let full = THREAD_QUERY_INFORMATION | THREAD_QUERY_LIMITED_INFORMATION;
895        // SAFETY: plain handle-returning calls; a result is wrapped so it is closed.
896        let opened = unsafe { OpenThread(full, false, id) }
897            .or_else(|_| unsafe { OpenThread(THREAD_QUERY_LIMITED_INFORMATION, false, id) });
898        opened.ok().map(ThreadHandle)
899    }
900
901    /// Creation time of an open thread in FILETIME ticks.
902    fn thread_created(thread: HANDLE) -> Option<u64> {
903        let (mut created, mut exited, mut kernel, mut user) = (
904            FILETIME::default(),
905            FILETIME::default(),
906            FILETIME::default(),
907            FILETIME::default(),
908        );
909        // SAFETY: four valid out-pointers; `thread` is an open handle.
910        let read =
911            unsafe { GetThreadTimes(thread, &mut created, &mut exited, &mut kernel, &mut user) };
912        read.ok()?;
913        Some(ticks(created))
914    }
915
916    /// The name a thread gave itself (`SetThreadDescription`), or `None`.
917    fn thread_name(thread: HANDLE) -> Option<String> {
918        // SAFETY: `thread` is open with `THREAD_QUERY_LIMITED_INFORMATION`.
919        let description = unsafe { GetThreadDescription(thread) }.ok()?;
920        if description.is_null() {
921            return None;
922        }
923        // SAFETY: a non-null result is a NUL-terminated UTF-16 string allocated for us.
924        let name = unsafe { description.to_string() }.ok();
925        // SAFETY: the string must be released with `LocalFree`, once.
926        unsafe { LocalFree(Some(HLOCAL(description.0.cast()))) };
927        name.filter(|name| !name.is_empty())
928    }
929
930    /// `ThreadQuerySetWin32StartAddress`, the `NtQueryInformationThread` class that returns the
931    /// routine handed to `CreateThread`. The system process table's own start address is the
932    /// kernel-side one (`RtlUserThreadStart` for every thread) and says nothing.
933    #[cfg(target_pointer_width = "64")]
934    const THREAD_QUERY_SET_WIN32_START_ADDRESS: u32 = 9;
935
936    // Declared here rather than taken from the `windows` crate, whose `Wdk_System_Threading`
937    // feature this crate does not enable (std links `ntdll` already on every Windows target).
938    #[cfg(target_pointer_width = "64")]
939    #[link(name = "ntdll")]
940    extern "system" {
941        fn NtQueryInformationThread(
942            thread: HANDLE,
943            class: u32,
944            information: *mut std::ffi::c_void,
945            length: u32,
946            returned: *mut u32,
947        ) -> i32;
948    }
949
950    #[cfg(target_pointer_width = "64")]
951    fn win32_start_address(thread: HANDLE) -> Option<u64> {
952        let mut address: usize = 0;
953        // SAFETY: `address` is a writable pointer-sized out value and the length says so.
954        let status = unsafe {
955            NtQueryInformationThread(
956                thread,
957                THREAD_QUERY_SET_WIN32_START_ADDRESS,
958                (&mut address as *mut usize).cast(),
959                std::mem::size_of::<usize>() as u32,
960                std::ptr::null_mut(),
961            )
962        };
963        (status >= 0 && address != 0).then_some(address as u64)
964    }
965
966    #[cfg(not(target_pointer_width = "64"))]
967    fn win32_start_address(_thread: HANDLE) -> Option<u64> {
968        None
969    }
970
971    /// The process figures and the thread table from one table read: `wakeups_total` is the sum
972    /// of the returned threads' `wakeups`. Fails when the table cannot be read.
973    pub fn sample_threads() -> io::Result<(ProcessUsage, Vec<ThreadSample>)> {
974        let threads = thread_table().ok_or_else(|| io::Error::other("thread table unavailable"))?;
975        let usage = sample_with(super::sum_wakeups(&threads))?;
976        Ok((usage, threads))
977    }
978
979    /// Fills each thread's name and Win32 start address by opening it. A thread that exited
980    /// since the snapshot, was replaced by a new one with the same id (its creation time no
981    /// longer matches), or cannot be opened keeps `None` there; its counters are untouched.
982    pub fn enrich(threads: &mut [ThreadSample]) {
983        for thread in threads.iter_mut() {
984            let Some(handle) = u32::try_from(thread.thread_id).ok().and_then(open_thread) else {
985                continue;
986            };
987            if thread.started.is_some() && thread_created(handle.0) != thread.started {
988                continue;
989            }
990            thread.name = thread_name(handle.0);
991            thread.start_address = win32_start_address(handle.0);
992        }
993    }
994
995    pub fn sample() -> io::Result<ProcessUsage> {
996        let table = thread_table();
997        sample_with(table.as_deref().and_then(super::sum_wakeups))
998    }
999
1000    fn sample_with(wakeups_total: Option<u64>) -> io::Result<ProcessUsage> {
1001        unsafe {
1002            let process = GetCurrentProcess();
1003            let mut mem = PROCESS_MEMORY_COUNTERS_EX::default();
1004            GetProcessMemoryInfo(
1005                process,
1006                (&mut mem as *mut PROCESS_MEMORY_COUNTERS_EX).cast::<PROCESS_MEMORY_COUNTERS>(),
1007                std::mem::size_of::<PROCESS_MEMORY_COUNTERS_EX>() as u32,
1008            )
1009            .map_err(os)?;
1010            let (mut created, mut exited, mut kernel, mut user) = (
1011                FILETIME::default(),
1012                FILETIME::default(),
1013                FILETIME::default(),
1014                FILETIME::default(),
1015            );
1016            GetProcessTimes(process, &mut created, &mut exited, &mut kernel, &mut user)
1017                .map_err(os)?;
1018            let mut io_counters = IO_COUNTERS::default();
1019            GetProcessIoCounters(process, &mut io_counters).map_err(os)?;
1020            Ok(ProcessUsage {
1021                resident_bytes: mem.WorkingSetSize as u64,
1022                footprint_bytes: mem.PrivateUsage as u64,
1023                // FILETIME ticks are 100 ns.
1024                cpu_seconds: (ticks(kernel) + ticks(user)) as f64 / 1e7,
1025                wakeups_total,
1026                disk_ops_total: Some(
1027                    io_counters.ReadOperationCount + io_counters.WriteOperationCount,
1028                ),
1029                disk_read_bytes: Some(io_counters.ReadTransferCount),
1030                disk_write_bytes: Some(io_counters.WriteTransferCount),
1031            })
1032        }
1033    }
1034}
1035
1036#[cfg(test)]
1037mod tests {
1038    use super::*;
1039
1040    /// Sampling exists where an implementation does: macOS, Linux, Android and Windows.
1041    #[cfg(any(
1042        target_os = "macos",
1043        target_os = "linux",
1044        target_os = "android",
1045        windows
1046    ))]
1047    #[test]
1048    fn sample_reports_resident_memory_and_cpu() {
1049        let first = process_usage().unwrap();
1050        assert!(first.resident_bytes > 0);
1051        assert!(first.footprint_bytes > 0);
1052        let mut sink = 0u64;
1053        for i in 0..20_000_000u64 {
1054            sink = sink.wrapping_add(i * 31);
1055        }
1056        std::hint::black_box(sink);
1057        let second = process_usage().unwrap();
1058        assert!(second.cpu_seconds >= first.cpu_seconds);
1059        let rates = second.rates_since(&first, Duration::from_secs(1));
1060        assert!(rates.cpu_percent >= 0.0);
1061    }
1062
1063    /// Windows reports wake-ups as summed thread context switches: present, and not decreasing
1064    /// while the threads that did the waking are still alive.
1065    #[cfg(all(windows, target_pointer_width = "64"))]
1066    #[test]
1067    fn windows_wakeups_count_thread_context_switches() {
1068        use std::sync::{mpsc, Arc, Mutex};
1069        let (tx, rx) = mpsc::channel::<()>();
1070        let rx = Arc::new(Mutex::new(rx));
1071        let (done_tx, done_rx) = mpsc::channel::<()>();
1072        let workers: Vec<_> = (0..3)
1073            .map(|_| {
1074                let rx = rx.clone();
1075                let done = done_tx.clone();
1076                std::thread::spawn(move || {
1077                    // Each message wakes one worker.
1078                    while rx.lock().map(|r| r.recv().is_ok()).unwrap_or(false) {
1079                        let _ = done.send(());
1080                    }
1081                })
1082            })
1083            .collect();
1084        // Other tests in this process may end threads between samples (their counts leave the
1085        // sum), so allow a few attempts for a non-decreasing pair.
1086        let mut ok = false;
1087        for _ in 0..5 {
1088            let first = process_usage()
1089                .unwrap()
1090                .wakeups_total
1091                .expect("windows reports wake-ups");
1092            for _ in 0..5 {
1093                std::thread::sleep(Duration::from_millis(10));
1094                tx.send(()).unwrap();
1095                done_rx.recv().unwrap();
1096            }
1097            let second = process_usage()
1098                .unwrap()
1099                .wakeups_total
1100                .expect("windows reports wake-ups");
1101            if second >= first {
1102                ok = true;
1103                break;
1104            }
1105        }
1106        drop(tx);
1107        for worker in workers {
1108            let _ = worker.join();
1109        }
1110        assert!(ok, "wake-up total decreased on every attempt");
1111    }
1112
1113    /// Every other Unix target compiles and reports `Unsupported`, never a made-up sample.
1114    #[cfg(all(
1115        unix,
1116        not(target_os = "macos"),
1117        not(any(target_os = "linux", target_os = "android"))
1118    ))]
1119    #[test]
1120    fn unsupported_unix_targets_report_unsupported_not_a_sample() {
1121        let error = process_usage().expect_err("no sampler exists for this target");
1122        assert_eq!(error.kind(), io::ErrorKind::Unsupported);
1123        let error = observe_idle(Duration::from_millis(1)).expect_err("idle needs sampling");
1124        assert_eq!(error.kind(), io::ErrorKind::Unsupported);
1125    }
1126
1127    #[cfg(any(
1128        target_os = "macos",
1129        target_os = "linux",
1130        target_os = "android",
1131        windows
1132    ))]
1133    #[test]
1134    fn tree_usage_counts_the_remembered_root_and_refuses_a_replaced_one() {
1135        let pid = std::num::NonZeroU32::new(std::process::id()).unwrap();
1136        let root = crate::AdoptedIdentity::capture(pid)
1137            .unwrap()
1138            .expect("this process has an identity");
1139        let usage = tree_usage(&root).unwrap();
1140        assert!(usage.processes >= 1, "{usage:?}");
1141        assert!(usage.resident_bytes > 0, "{usage:?}");
1142        // Same pid, a start token that is not this process's: counted as nobody.
1143        let replaced = crate::AdoptedIdentity::from_recorded(
1144            pid,
1145            crate::StartToken {
1146                major: root.start_token().major.wrapping_add(1),
1147                minor: root.start_token().minor,
1148            },
1149            None,
1150        );
1151        assert_eq!(tree_usage(&replaced).unwrap(), TreeUsage::default());
1152    }
1153
1154    #[test]
1155    fn rates_are_none_without_counters_and_zero_for_no_time() {
1156        let a = ProcessUsage {
1157            wakeups_total: None,
1158            ..Default::default()
1159        };
1160        let b = ProcessUsage {
1161            wakeups_total: Some(10),
1162            ..Default::default()
1163        };
1164        assert_eq!(
1165            b.rates_since(&a, Duration::from_secs(1)).wakeups_per_second,
1166            None
1167        );
1168        let c = ProcessUsage {
1169            wakeups_total: Some(30),
1170            ..Default::default()
1171        };
1172        assert_eq!(
1173            c.rates_since(&b, Duration::from_secs(2)).wakeups_per_second,
1174            Some(10.0)
1175        );
1176        assert_eq!(c.rates_since(&b, Duration::ZERO).cpu_percent, 0.0);
1177    }
1178
1179    fn thread(id: u64, started: u64, wakeups: Option<u64>) -> ThreadSample {
1180        ThreadSample {
1181            thread_id: id,
1182            started: Some(started),
1183            wakeups,
1184            cpu_seconds: wakeups.map(|w| w as f64 / 1000.0),
1185            name: None,
1186            start_address: None,
1187        }
1188    }
1189
1190    #[test]
1191    fn deltas_match_by_identity_sort_by_wakeups_and_list_exited_threads() {
1192        let start = [
1193            thread(1, 100, Some(10)),
1194            thread(2, 200, Some(50)),
1195            thread(3, 300, Some(7)),
1196        ];
1197        let end = [
1198            thread(1, 100, Some(14)),
1199            thread(2, 200, Some(51)),
1200            thread(4, 400, Some(30)),
1201        ];
1202        let deltas = thread_deltas(&start, &end);
1203        let listed: Vec<(u64, Option<u64>, bool)> = deltas
1204            .threads
1205            .iter()
1206            .map(|d| (d.thread_id, d.wakeups, d.started_in_window))
1207            .collect();
1208        // Thread 4 began inside the window: its whole count is the window's.
1209        assert_eq!(
1210            listed,
1211            vec![
1212                (4, Some(30), true),
1213                (1, Some(4), false),
1214                (2, Some(1), false),
1215            ]
1216        );
1217        assert_eq!(deltas.exited.len(), 1);
1218        assert_eq!(deltas.exited[0].thread_id, 3);
1219        assert_eq!(deltas.total_wakeups(), Some(35));
1220        assert_eq!(deltas.exited_wakeups_at_start(), Some(7));
1221        // Listed minus exited-at-start is the whole-process delta between the same snapshots.
1222        assert_eq!(
1223            deltas.total_wakeups().unwrap() - deltas.exited_wakeups_at_start().unwrap(),
1224            sum_wakeups(&end).unwrap() - sum_wakeups(&start).unwrap()
1225        );
1226        let cpu = deltas.threads.iter().find(|d| d.thread_id == 1).unwrap();
1227        assert!((cpu.cpu_seconds.unwrap() - 0.004).abs() < 1e-9);
1228        assert_eq!(deltas.top(2).len(), 2);
1229        assert_eq!(deltas.top(99).len(), 3);
1230        assert_eq!(deltas.top(0).len(), 0);
1231    }
1232
1233    #[test]
1234    fn a_reused_thread_id_with_a_new_creation_time_is_a_new_thread() {
1235        let start = [thread(9, 100, Some(500))];
1236        let end = [thread(9, 999, Some(3))];
1237        let deltas = thread_deltas(&start, &end);
1238        assert_eq!(deltas.threads.len(), 1);
1239        assert_eq!(deltas.threads[0].wakeups, Some(3));
1240        assert!(deltas.threads[0].started_in_window);
1241        assert_eq!(deltas.exited.len(), 1);
1242        assert_eq!(deltas.exited_wakeups_at_start(), Some(500));
1243    }
1244
1245    #[test]
1246    fn a_counter_that_went_backwards_counts_as_zero_not_a_wrapped_value() {
1247        let deltas = thread_deltas(&[thread(1, 1, Some(10))], &[thread(1, 1, Some(4))]);
1248        assert_eq!(deltas.threads[0].wakeups, Some(0));
1249        assert_eq!(deltas.total_wakeups(), Some(0));
1250    }
1251
1252    #[test]
1253    fn unknown_counters_stay_unknown_and_sort_last() {
1254        let start = [thread(1, 1, None), thread(2, 2, Some(1))];
1255        let end = [thread(1, 1, None), thread(2, 2, Some(5))];
1256        let deltas = thread_deltas(&start, &end);
1257        assert_eq!(deltas.threads[0].thread_id, 2);
1258        assert_eq!(deltas.threads[0].wakeups, Some(4));
1259        assert_eq!(deltas.threads[1].wakeups, None);
1260        assert_eq!(deltas.threads[1].cpu_seconds, None);
1261        assert_eq!(deltas.total_wakeups(), None);
1262        let none = ThreadDeltas {
1263            threads: Vec::new(),
1264            exited: Vec::new(),
1265        };
1266        assert_eq!(none.total_wakeups(), Some(0));
1267        assert_eq!(
1268            sum_wakeups(&[thread(1, 1, Some(2)), thread(2, 2, None)]),
1269            None
1270        );
1271        assert_eq!(
1272            sum_wakeups(&[thread(1, 1, Some(2)), thread(2, 2, Some(3))]),
1273            Some(5)
1274        );
1275        assert_eq!(
1276            sum_wakeups(&[thread(1, 1, Some(u64::MAX)), thread(2, 2, Some(1))]),
1277            None
1278        );
1279    }
1280
1281    #[test]
1282    fn a_name_or_address_read_only_at_one_end_is_kept() {
1283        let mut first = thread(1, 1, Some(1));
1284        first.name = Some("worker".to_string());
1285        let mut last = thread(1, 1, Some(2));
1286        last.start_address = Some(0x1000);
1287        let deltas = thread_deltas(&[first], &[last]);
1288        assert_eq!(deltas.threads[0].name.as_deref(), Some("worker"));
1289        assert_eq!(deltas.threads[0].start_address, Some(0x1000));
1290    }
1291
1292    #[test]
1293    fn thread_result_lines_are_not_budget_lines_and_list_only_active_threads() {
1294        let mut busy = thread(7, 1, Some(100));
1295        busy.name = Some("tokio-runtime-worker".to_string());
1296        busy.start_address = Some(0x7ff6_1234);
1297        let deltas = thread_deltas(
1298            &[thread(7, 1, Some(0)), thread(8, 2, Some(5))],
1299            &[busy, thread(8, 2, Some(5))],
1300        );
1301        let text = deltas.result_lines(Duration::from_secs(50), 5);
1302        let lines: Vec<&str> = text.lines().collect();
1303        assert_eq!(lines.len(), 2, "{text}");
1304        assert!(lines
1305            .iter()
1306            .all(|line| line.starts_with("RESULT thread_wakeups") && !line.contains("budget")));
1307        assert_eq!(
1308            lines[0],
1309            "RESULT thread_wakeups_summary threads=2 born=0 exited=0 listed_wakeups=100 exited_wakeups_at_start=0 window_seconds=50.0"
1310        );
1311        assert!(lines[1]
1312            .starts_with("RESULT thread_wakeups rank=1 tid=7 wakeups=100 per_second=2.000 "));
1313        assert!(lines[1].ends_with(" born=0 start=0x7ff61234 name=\"tokio-runtime-worker\""));
1314        // A zero window never divides by zero, and `top = 0` lists no thread.
1315        let zero = deltas.result_lines(Duration::ZERO, 5);
1316        assert!(zero.contains("per_second=0.000"), "{zero}");
1317        assert_eq!(
1318            deltas
1319                .result_lines(Duration::from_secs(1), 0)
1320                .lines()
1321                .count(),
1322            1
1323        );
1324    }
1325
1326    #[cfg(any(
1327        target_os = "macos",
1328        target_os = "linux",
1329        target_os = "android",
1330        windows
1331    ))]
1332    #[test]
1333    fn thread_snapshot_lists_unique_live_threads() {
1334        let snapshot = process_usage_with_threads().unwrap();
1335        assert!(!snapshot.threads.is_empty());
1336        let mut ids: Vec<u64> = snapshot.threads.iter().map(|t| t.thread_id).collect();
1337        let listed = ids.len();
1338        ids.sort_unstable();
1339        ids.dedup();
1340        assert_eq!(ids.len(), listed, "a thread was listed twice");
1341    }
1342
1343    /// On Windows the total and the per-thread counters come from one table: they agree exactly,
1344    /// and a named thread shows up with its name, start address and the wake-ups it was given.
1345    #[cfg(all(windows, target_pointer_width = "64"))]
1346    #[test]
1347    fn windows_threads_sum_to_the_total_and_attribute_wakeups_to_the_woken_thread() {
1348        use std::sync::mpsc;
1349        let (id_tx, id_rx) = mpsc::channel::<u32>();
1350        let (wake_tx, wake_rx) = mpsc::channel::<()>();
1351        let (ack_tx, ack_rx) = mpsc::channel::<()>();
1352        let probe = std::thread::Builder::new()
1353            .name("rk-usage-probe".to_string())
1354            .spawn(move || {
1355                // SAFETY: no arguments; returns the calling thread's id.
1356                id_tx
1357                    .send(unsafe { windows::Win32::System::Threading::GetCurrentThreadId() })
1358                    .unwrap();
1359                while wake_rx.recv().is_ok() {
1360                    let _ = ack_tx.send(());
1361                }
1362            })
1363            .unwrap();
1364        let id = u64::from(id_rx.recv().unwrap());
1365        let first = process_usage_with_threads().unwrap();
1366        for _ in 0..5 {
1367            std::thread::sleep(Duration::from_millis(10));
1368            wake_tx.send(()).unwrap();
1369            ack_rx.recv().unwrap();
1370        }
1371        let second = process_usage_with_threads().unwrap();
1372        drop(wake_tx);
1373        probe.join().unwrap();
1374
1375        for snapshot in [&first, &second] {
1376            assert!(snapshot.usage.wakeups_total.is_some());
1377            assert_eq!(snapshot.usage.wakeups_total, snapshot.thread_wakeups_sum());
1378        }
1379        let probe_now = second
1380            .threads
1381            .iter()
1382            .find(|t| t.thread_id == id)
1383            .expect("the probe thread is listed");
1384        assert_eq!(probe_now.name.as_deref(), Some("rk-usage-probe"));
1385        assert!(probe_now.start_address.is_some());
1386        assert!(probe_now.started.is_some());
1387        let deltas = thread_deltas(&first.threads, &second.threads);
1388        let delta = deltas
1389            .threads
1390            .iter()
1391            .find(|d| d.thread_id == id)
1392            .expect("the probe thread has a delta");
1393        assert!(!delta.started_in_window);
1394        assert!(delta.wakeups.unwrap() >= 1, "{delta:?}");
1395    }
1396
1397    /// The idle observation's process delta is what the per-thread deltas add up to.
1398    #[cfg(all(windows, target_pointer_width = "64"))]
1399    #[test]
1400    fn windows_idle_thread_deltas_reconcile_with_the_process_delta() {
1401        let observed = observe_idle_with_threads(Duration::from_millis(50)).unwrap();
1402        assert!(observed.observation.elapsed >= Duration::from_millis(50));
1403        let start = observed.observation.start.wakeups_total.unwrap() as i128;
1404        let end = observed.observation.end.wakeups_total.unwrap() as i128;
1405        let listed = observed.threads.total_wakeups().unwrap() as i128;
1406        let exited = observed.threads.exited_wakeups_at_start().unwrap() as i128;
1407        assert_eq!(listed - exited, end - start);
1408    }
1409}