1use std::collections::HashMap;
23use std::hash::Hash;
24use std::time::Instant;
25
26#[cfg(target_os = "linux")]
27mod linux;
28#[cfg(target_os = "linux")]
29use linux as os;
30
31#[cfg(target_os = "macos")]
32mod macos;
33#[cfg(target_os = "macos")]
34use macos as os;
35
36#[cfg(windows)]
37mod windows;
38#[cfg(windows)]
39use windows as os;
40
41#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
42mod os {
43 pub fn snapshot() -> std::io::Result<Vec<super::ProcInfo>> {
44 Err(std::io::Error::new(
45 std::io::ErrorKind::Unsupported,
46 "no process reader for this OS",
47 ))
48 }
49 pub fn command_line(_pid: u32) -> Option<String> {
50 None
51 }
52 pub fn start_key(_pid: u32) -> Option<u64> {
53 None
54 }
55 pub fn cpu_count() -> usize {
56 std::thread::available_parallelism()
57 .map(|n| n.get())
58 .unwrap_or(1)
59 }
60}
61
62#[cfg(windows)]
65pub const MEMORY_METRIC: &str = "private working set";
66#[cfg(target_os = "macos")]
67pub const MEMORY_METRIC: &str = "memory footprint";
68#[cfg(not(any(windows, target_os = "macos")))]
69pub const MEMORY_METRIC: &str = "anonymous resident + swap";
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct ProcInfo {
74 pub pid: u32,
75 pub ppid: Option<u32>,
79 pub start_key: u64,
84 pub started_at_ms: Option<u64>,
86 pub name: String,
88 pub cpu_ns: Option<u64>,
90 pub mem_private: Option<u64>,
92 pub mem_resident: Option<u64>,
94 pub mem_commit: Option<u64>,
96}
97
98impl ProcInfo {
99 pub fn key(&self) -> ProcKey {
101 ProcKey {
102 pid: self.pid,
103 start_key: self.start_key,
104 }
105 }
106
107 pub fn measured(&self) -> bool {
110 self.cpu_ns.is_some() || self.mem_private.is_some()
111 }
112
113 pub fn is_child_of(&self, parent: &ProcInfo) -> bool {
117 if self.ppid != Some(parent.pid) || parent.pid == self.pid {
118 return false;
119 }
120 match (parent.started_at_ms, self.started_at_ms) {
121 (Some(p), Some(c)) => p <= c,
122 _ => true,
123 }
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
129pub struct ProcKey {
130 pub pid: u32,
131 pub start_key: u64,
132}
133
134pub fn snapshot() -> std::io::Result<Vec<ProcInfo>> {
136 os::snapshot()
137}
138
139pub fn command_line(pid: u32) -> Option<String> {
143 os::command_line(pid)
144}
145
146pub fn is_running(key: ProcKey) -> bool {
150 os::start_key(key.pid) == Some(key.start_key)
151}
152
153pub fn command_line_of(key: ProcKey) -> Option<String> {
157 if os::start_key(key.pid)? != key.start_key {
158 return None;
159 }
160 let line = os::command_line(key.pid)?;
161 (os::start_key(key.pid)? == key.start_key).then_some(line)
162}
163
164pub fn cpu_count() -> usize {
167 os::cpu_count().max(1)
168}
169
170#[derive(Debug)]
177pub struct RateMeter<K> {
178 prev: HashMap<K, (u64, Instant)>,
179 next: HashMap<K, (u64, Instant)>,
180}
181
182impl<K> Default for RateMeter<K> {
183 fn default() -> Self {
184 Self {
185 prev: HashMap::new(),
186 next: HashMap::new(),
187 }
188 }
189}
190
191impl<K: Eq + Hash + Clone> RateMeter<K> {
192 pub fn new() -> Self {
193 Self::default()
194 }
195
196 pub fn sample(&mut self, key: K, total_ns: u64, now: Instant) -> Option<f64> {
200 let rate = self.prev.get(&key).and_then(|&(was, at)| {
201 let wall = now.checked_duration_since(at)?.as_nanos();
202 (wall > 0 && total_ns >= was).then(|| (total_ns - was) as f64 / wall as f64)
203 });
204 self.next.insert(key, (total_ns, now));
205 rate
206 }
207
208 pub fn finish(&mut self) {
210 self.prev = std::mem::take(&mut self.next);
211 }
212
213 pub fn len(&self) -> usize {
215 self.prev.len()
216 }
217
218 pub fn is_empty(&self) -> bool {
219 self.prev.is_empty()
220 }
221}
222
223pub fn base_name(path: &str) -> &str {
225 path.rsplit(['/', '\\']).next().unwrap_or(path)
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use std::time::Duration;
232
233 fn proc(pid: u32, ppid: Option<u32>, started: Option<u64>) -> ProcInfo {
234 ProcInfo {
235 pid,
236 ppid,
237 start_key: started.unwrap_or(0),
238 started_at_ms: started,
239 name: String::new(),
240 cpu_ns: None,
241 mem_private: None,
242 mem_resident: None,
243 mem_commit: None,
244 }
245 }
246
247 #[test]
248 fn rate_needs_two_rounds_and_is_a_fraction_of_one_core() {
249 let mut m = RateMeter::new();
250 let t0 = Instant::now();
251 assert_eq!(
252 m.sample("a", 1_000_000_000, t0),
253 None,
254 "first round has no rate"
255 );
256 m.finish();
257 let r = m
259 .sample("a", 2_500_000_000, t0 + Duration::from_secs(1))
260 .unwrap();
261 assert!((r - 1.5).abs() < 1e-9, "{r}");
262 }
263
264 #[test]
265 fn a_key_missing_from_a_round_starts_over() {
266 let mut m = RateMeter::new();
267 let t0 = Instant::now();
268 m.sample(1, 100, t0);
269 m.finish();
270 m.finish(); assert_eq!(m.sample(1, 200, t0 + Duration::from_secs(2)), None);
272 assert!(m.is_empty(), "nothing finished yet this round");
273 }
274
275 #[test]
276 fn a_counter_that_goes_backwards_has_no_rate() {
277 let mut m = RateMeter::new();
278 let t0 = Instant::now();
279 m.sample(1, 500, t0);
280 m.finish();
281 assert_eq!(m.sample(1, 100, t0 + Duration::from_secs(1)), None);
282 }
283
284 #[test]
285 fn a_parent_that_started_after_its_child_is_a_reused_pid() {
286 let parent = proc(10, None, Some(2_000));
287 let child = proc(11, Some(10), Some(1_000));
288 assert!(!child.is_child_of(&parent));
289 let real_parent = proc(10, None, Some(500));
290 assert!(child.is_child_of(&real_parent));
291 assert!(!child.is_child_of(&proc(12, None, Some(0))), "wrong pid");
292 assert!(
293 proc(11, Some(10), None).is_child_of(&parent),
294 "unknown start times trust the pid"
295 );
296 }
297
298 #[test]
299 fn base_name_strips_either_separator() {
300 assert_eq!(base_name(r"C:\Program Files\nodejs\node.exe"), "node.exe");
301 assert_eq!(base_name("/usr/bin/node"), "node");
302 assert_eq!(base_name("node"), "node");
303 }
304
305 #[test]
308 fn snapshot_sees_this_process_with_cpu_and_memory() {
309 let me = std::process::id();
310 let all = snapshot().expect("snapshot");
311 assert!(all.len() > 1, "only {} processes", all.len());
312 let p = all
313 .iter()
314 .find(|p| p.pid == me)
315 .expect("this process is listed");
316 assert!(p.cpu_ns.is_some(), "{p:?}");
317 assert!(p.mem_private.unwrap_or(0) > 0, "{p:?}");
318 assert!(
319 p.mem_resident.unwrap_or(0) >= p.mem_private.unwrap_or(0) / 4,
320 "{p:?}"
321 );
322 assert!(p.ppid.is_some(), "{p:?}");
323 assert!(!p.name.is_empty(), "{p:?}");
324 let now_ms = std::time::SystemTime::now()
325 .duration_since(std::time::UNIX_EPOCH)
326 .unwrap()
327 .as_millis() as u64;
328 let started = p.started_at_ms.expect("start time");
329 assert!(
330 started <= now_ms + 5_000 && started > now_ms - 24 * 3600 * 1000,
331 "started {started}, now {now_ms}"
332 );
333 let again = snapshot().unwrap();
335 assert_eq!(again.iter().find(|q| q.pid == me).unwrap().key(), p.key());
336 }
337
338 #[test]
339 fn cpu_time_grows_while_this_process_spins() {
340 let me = std::process::id();
341 let cpu = || {
342 snapshot()
343 .unwrap()
344 .into_iter()
345 .find(|p| p.pid == me)
346 .and_then(|p| p.cpu_ns)
347 .unwrap()
348 };
349 let before = cpu();
350 let spin_until = Instant::now() + Duration::from_millis(300);
351 let mut x = 0u64;
352 while Instant::now() < spin_until {
353 x = x.wrapping_mul(6364136223846793005).wrapping_add(1);
354 }
355 std::hint::black_box(x);
356 let after = cpu();
357 assert!(
359 after >= before + 100_000_000,
360 "before {before} after {after}"
361 );
362 assert!(
364 after < before + 30_000_000_000,
365 "before {before} after {after}"
366 );
367 }
368
369 #[test]
370 fn command_line_of_this_process_names_its_executable() {
371 let line = command_line(std::process::id()).expect("own command line");
372 let exe = std::env::current_exe().unwrap();
373 let stem = exe.file_stem().unwrap().to_string_lossy().to_string();
374 assert!(line.contains(&stem), "{line:?} lacks {stem:?}");
375 assert_eq!(command_line(u32::MAX - 7), None);
376 }
377
378 #[test]
379 fn a_command_line_only_for_the_exact_process() {
380 let me = snapshot()
381 .unwrap()
382 .into_iter()
383 .find(|p| p.pid == std::process::id())
384 .unwrap();
385 assert!(command_line_of(me.key()).is_some(), "{me:?}");
386 let newer = ProcKey {
387 pid: me.pid,
388 start_key: me.start_key.wrapping_add(1),
389 };
390 assert_eq!(command_line_of(newer), None, "same PID, another process");
391 }
392
393 #[test]
394 fn cpu_count_is_at_least_what_this_process_may_use() {
395 let usable = std::thread::available_parallelism()
396 .map(|n| n.get())
397 .unwrap_or(1);
398 assert!(cpu_count() >= usable, "{} < {usable}", cpu_count());
399 }
400}