1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
//! Process-level resource reads (ADR-103 Stage 1).
//!
//! Cumulative CPU time and RSS via `getrusage(2)`, used by the daemon's
//! phase-span logging (background-task start/completion/cancellation) and
//! the `comm.health` resource self-report. `libc` is already a workspace
//! dependency (used elsewhere in this crate for `flock`/`kill`), so this
//! reuses it rather than adding a new one, per ADR-103 Stage 1's preference
//! for a small std/libc-based read over a dedicated sysinfo-style crate.
/// A point-in-time snapshot of this process's cumulative resource usage.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProcessResourceUsage {
/// Cumulative user+system CPU time consumed by this process since start,
/// in microseconds.
pub cpu_us: i64,
/// Resident set size, in bytes.
pub rss_bytes: i64,
}
/// Read this process's cumulative CPU time and RSS via `getrusage(RUSAGE_SELF)`.
///
/// Returns `None` if the underlying syscall fails (never expected in
/// practice on a supported platform) or on a non-Unix target, where no
/// portable equivalent is wired up. Callers treat this as a best-effort
/// diagnostic read, never load-bearing.
#[cfg(unix)]
pub fn process_resource_usage() -> Option<ProcessResourceUsage> {
// SAFETY: `getrusage` is a POSIX syscall with no memory side effects
// beyond writing into the `rusage` out-param, which is a plain
// `#[repr(C)]` struct zero-initialized immediately before the call.
let usage: libc::rusage = unsafe {
let mut usage: libc::rusage = std::mem::zeroed();
if libc::getrusage(libc::RUSAGE_SELF, &mut usage) != 0 {
return None;
}
usage
};
let user_us = usage.ru_utime.tv_sec * 1_000_000 + i64::from(usage.ru_utime.tv_usec as i32);
let sys_us = usage.ru_stime.tv_sec * 1_000_000 + i64::from(usage.ru_stime.tv_usec as i32);
// `ru_maxrss` is bytes on macOS/Darwin and KiB on Linux.
let rss_bytes: i64 = if cfg!(target_os = "macos") {
usage.ru_maxrss as i64
} else {
usage.ru_maxrss as i64 * 1024
};
Some(ProcessResourceUsage {
cpu_us: user_us + sys_us,
rss_bytes,
})
}
/// Non-Unix fallback: no portable `getrusage` equivalent is wired up.
#[cfg(not(unix))]
pub fn process_resource_usage() -> Option<ProcessResourceUsage> {
None
}
/// CPU time consumed strictly between two [`ProcessResourceUsage`] snapshots,
/// in microseconds.
///
/// `cpu_us` on a snapshot is cumulative process CPU time since process
/// start, not CPU consumed by any one phase, so per-phase attribution needs
/// the delta between an entry and exit snapshot. The result is floored at
/// zero so a spurious (should-never-happen) decrease in cumulative CPU
/// between two reads never surfaces as a negative delta.
pub fn cpu_delta_us(
start: Option<ProcessResourceUsage>,
end: Option<ProcessResourceUsage>,
) -> Option<i64> {
match (start, end) {
(Some(start), Some(end)) => Some(end.cpu_us.saturating_sub(start.cpu_us).max(0)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn process_resource_usage_returns_positive_values() {
// Do a small amount of work first so ru_utime/ru_maxrss are
// guaranteed non-zero on every platform this runs on.
let mut acc: u64 = 0;
for i in 0..1_000_000u64 {
acc = acc.wrapping_add(i);
}
std::hint::black_box(acc);
let usage = process_resource_usage().expect("getrusage must succeed on unix CI runners");
assert!(usage.cpu_us >= 0, "cpu_us must be non-negative");
assert!(
usage.rss_bytes > 0,
"rss_bytes must be positive for a live process"
);
}
// `cpu_delta_us` must report the difference between two snapshots, not
// either snapshot's raw cumulative value. Synthetic snapshots (rather
// than real `getrusage` reads) make this fail deterministically if the
// caller regresses to reporting `end.cpu_us` directly.
#[test]
fn cpu_delta_us_reports_the_difference_not_the_cumulative_end_value() {
let start = ProcessResourceUsage {
cpu_us: 500 * 60 * 1_000_000, // 500 CPU-minutes already burned
rss_bytes: 1_000_000,
};
let end = ProcessResourceUsage {
cpu_us: 500 * 60 * 1_000_000 + 1_500, // +1.5ms of CPU during the phase
rss_bytes: 1_100_000,
};
let delta = cpu_delta_us(Some(start), Some(end)).expect("both snapshots present");
assert_eq!(
delta, 1_500,
"delta must be end-minus-start, not the cumulative end value"
);
assert!(
delta < start.cpu_us,
"a correct delta must be far smaller than the pre-existing cumulative total"
);
}
#[test]
fn cpu_delta_us_saturates_instead_of_underflowing_on_a_spurious_decrease() {
let start = ProcessResourceUsage {
cpu_us: 1_000,
rss_bytes: 0,
};
let end = ProcessResourceUsage {
cpu_us: 500,
rss_bytes: 0,
};
assert_eq!(cpu_delta_us(Some(start), Some(end)), Some(0));
}
#[test]
fn cpu_delta_us_is_none_when_either_snapshot_is_unavailable() {
let snap = ProcessResourceUsage {
cpu_us: 10,
rss_bytes: 0,
};
assert_eq!(cpu_delta_us(None, Some(snap)), None);
assert_eq!(cpu_delta_us(Some(snap), None), None);
assert_eq!(cpu_delta_us(None, None), None);
}
}