1#![deny(unsafe_code)] mod platform;
14
15use std::collections::HashMap;
16use std::time::{Duration, Instant};
17
18pub use platform::{
19 AllStats, CpuTime, DiskIo, ProcessInfo, build_parent_map, get_all_stats, get_children,
20 get_cpu_time, get_cpu_time_with_children, get_disk_io, get_memory, get_memory_virtual,
21 get_ppid, get_process_comm, get_process_environ, get_process_path, get_start_time, get_tty,
22 list_all_pids, scan_all_processes,
23};
24
25pub type CpuPercent = f32;
27
28#[derive(Debug, Clone, Default)]
30pub struct ProcessStats {
31 pub cpu_time: CpuTime,
32 pub memory_bytes: u64,
33}
34
35#[derive(Debug, Clone, Default)]
37pub struct TreeStats {
38 pub cpu_time: CpuTime,
40 pub memory_bytes: u64,
42 pub process_count: u32,
44 pub pids: Vec<i32>,
46}
47
48pub fn snapshot_process(pid: i32) -> Option<ProcessStats> {
53 let cpu_time = get_cpu_time(pid)?;
54 let memory_bytes = platform::get_memory(pid).unwrap_or(0);
55 Some(ProcessStats {
56 cpu_time,
57 memory_bytes,
58 })
59}
60
61pub fn snapshot_tree(pid: i32) -> Option<TreeStats> {
63 let pids = collect_tree_pids(pid);
64 if pids.is_empty() {
65 return None;
66 }
67
68 let mut total_cpu = CpuTime::default();
69 let mut total_memory = 0u64;
70
71 for &p in &pids {
72 if let Some(cpu) = get_cpu_time(p) {
73 total_cpu = total_cpu + cpu;
74 }
75 total_memory += platform::get_memory(p).unwrap_or(0);
76 }
77
78 Some(TreeStats {
79 cpu_time: total_cpu,
80 memory_bytes: total_memory,
81 process_count: pids.len() as u32,
82 pids,
83 })
84}
85
86#[derive(Debug)]
91pub struct DeltaTracker {
92 snapshots: HashMap<i32, (CpuTime, Instant)>,
94}
95
96impl DeltaTracker {
97 pub fn new() -> Self {
98 Self {
99 snapshots: HashMap::new(),
100 }
101 }
102
103 pub fn get_cpu_percent(&mut self, pid: i32) -> Option<CpuPercent> {
106 let now = Instant::now();
107 let current = get_cpu_time(pid)?;
108
109 let result = if let Some((prev_cpu, prev_time)) = self.snapshots.get(&pid) {
110 let elapsed = now.duration_since(*prev_time);
111 if elapsed.as_nanos() == 0 {
112 return Some(0.0);
113 }
114 Some(current.cpu_percent_since(prev_cpu, elapsed))
115 } else {
116 None };
118
119 self.snapshots.insert(pid, (current, now));
120 result
121 }
122
123 pub fn get_tree_cpu_percent(&mut self, root_pid: i32) -> Option<(CpuPercent, u64, u32)> {
128 let pids = collect_tree_pids(root_pid);
129 if pids.is_empty() {
130 return None;
131 }
132
133 let mut total_percent = 0.0f32;
134 let mut total_memory = 0u64;
135 let mut all_have_baseline = true;
136
137 for &pid in &pids {
138 match self.get_cpu_percent(pid) {
139 Some(pct) => total_percent += pct,
140 None => all_have_baseline = false,
141 }
142 total_memory += platform::get_memory(pid).unwrap_or(0);
143 }
144
145 if all_have_baseline {
146 Some((total_percent, total_memory, pids.len() as u32))
147 } else {
148 Some((total_percent, total_memory, pids.len() as u32))
150 }
151 }
152
153 pub fn prune_dead(&mut self) {
155 self.snapshots
156 .retain(|&pid, _| platform::process_exists(pid));
157 }
158}
159
160impl Default for DeltaTracker {
161 fn default() -> Self {
162 Self::new()
163 }
164}
165
166#[derive(Debug)]
172pub struct CumulativeTracker {
173 baselines: HashMap<i32, (CpuTime, Instant)>,
175 start_time: Instant,
177}
178
179impl CumulativeTracker {
180 pub fn new() -> Self {
181 Self {
182 baselines: HashMap::new(),
183 start_time: Instant::now(),
184 }
185 }
186
187 pub fn get_cpu_percent(&mut self, pid: i32) -> Option<CpuPercent> {
191 let now = Instant::now();
192 let current = get_cpu_time(pid)?;
193
194 if let Some((baseline_cpu, baseline_time)) = self.baselines.get(&pid) {
195 let elapsed = now.duration_since(*baseline_time);
196 if elapsed.as_nanos() == 0 {
197 return Some(0.0);
198 }
199 Some(current.cpu_percent_since(baseline_cpu, elapsed))
200 } else {
201 self.baselines.insert(pid, (current, now));
203 Some(0.0) }
205 }
206
207 pub fn get_tree_cpu_percent(&mut self, root_pid: i32) -> Option<(CpuPercent, u64, u32)> {
209 let pids = collect_tree_pids(root_pid);
210 if pids.is_empty() {
211 return None;
212 }
213
214 let mut total_percent = 0.0f32;
215 let mut total_memory = 0u64;
216
217 for &pid in &pids {
218 if let Some(pct) = self.get_cpu_percent(pid) {
219 total_percent += pct;
220 }
221 total_memory += platform::get_memory(pid).unwrap_or(0);
222 }
223
224 Some((total_percent, total_memory, pids.len() as u32))
225 }
226
227 pub fn reset(&mut self) {
229 self.baselines.clear();
230 self.start_time = Instant::now();
231 }
232
233 pub fn elapsed(&self) -> Duration {
235 self.start_time.elapsed()
236 }
237}
238
239impl Default for CumulativeTracker {
240 fn default() -> Self {
241 Self::new()
242 }
243}
244
245pub fn collect_tree_pids(root_pid: i32) -> Vec<i32> {
247 use std::collections::HashSet;
248
249 let mut seen = HashSet::new();
250 let mut all_pids = Vec::new();
251 let mut queue = vec![root_pid];
252
253 if !platform::process_exists(root_pid) {
255 return all_pids;
256 }
257
258 while let Some(pid) = queue.pop() {
259 if seen.insert(pid) {
261 all_pids.push(pid);
262 queue.extend(get_children(pid));
263 }
264 }
265
266 all_pids
267}
268
269pub fn is_descendant_of<S: std::hash::BuildHasher>(
285 child_pid: i32,
286 parent_pid: i32,
287 parent_map: &HashMap<i32, i32, S>,
288) -> bool {
289 if child_pid == parent_pid {
290 return true;
291 }
292
293 let mut current_pid = child_pid;
294 for _ in 0..50 {
295 if let Some(&ppid) = parent_map.get(¤t_pid) {
296 if ppid == parent_pid {
297 return true;
298 }
299 if ppid <= 1 {
300 return false;
301 }
302 current_pid = ppid;
303 } else {
304 return false;
305 }
306 }
307 false
308}
309
310pub fn build_children_map(procs: &[ProcessInfo]) -> HashMap<i32, Vec<i32>> {
315 let mut map: HashMap<i32, Vec<i32>> = HashMap::new();
316
317 for proc in procs {
318 map.entry(proc.ppid).or_default().push(proc.pid);
319 }
320
321 map
322}
323
324pub fn collect_descendants_from_map(
328 root_pid: i32,
329 children_map: &HashMap<i32, Vec<i32>>,
330) -> Vec<i32> {
331 let mut result = vec![root_pid];
332 let mut queue = vec![root_pid];
333
334 while let Some(pid) = queue.pop() {
335 if let Some(children) = children_map.get(&pid) {
336 for &child in children {
337 result.push(child);
338 queue.push(child);
339 }
340 }
341 }
342
343 result
344}
345
346pub fn snapshot_tree_from_scan(root_pid: i32, procs: &[ProcessInfo]) -> Option<TreeStats> {
351 let children_map = build_children_map(procs);
353
354 let pids = collect_descendants_from_map(root_pid, &children_map);
356
357 let proc_map: HashMap<i32, &ProcessInfo> = procs.iter().map(|p| (p.pid, p)).collect();
359
360 if !proc_map.contains_key(&root_pid) {
362 return None;
363 }
364
365 let mut total_cpu = CpuTime::default();
366 let mut total_memory = 0u64;
367 let mut valid_pids = Vec::new();
368
369 for pid in pids {
370 if let Some(proc) = proc_map.get(&pid) {
371 total_cpu = total_cpu + proc.cpu_time;
372 total_memory += proc.memory_bytes;
373 valid_pids.push(pid);
374 }
375 }
376
377 Some(TreeStats {
378 cpu_time: total_cpu,
379 memory_bytes: total_memory,
380 process_count: valid_pids.len() as u32,
381 pids: valid_pids,
382 })
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
390 fn test_snapshot_self() {
391 let pid = std::process::id() as i32;
392 let stats = snapshot_process(pid);
393 assert!(stats.is_some());
394 let stats = stats.unwrap();
395 assert!(stats.memory_bytes > 0);
396 }
397
398 #[test]
399 fn test_delta_tracker() {
400 let pid = std::process::id() as i32;
401 let mut tracker = DeltaTracker::new();
402
403 let first = tracker.get_cpu_percent(pid);
405 assert!(first.is_none());
406
407 let mut sum = 0u64;
409 for i in 0..1_000_000 {
410 sum = sum.wrapping_add(i);
411 }
412 std::hint::black_box(sum);
413
414 let second = tracker.get_cpu_percent(pid);
416 assert!(second.is_some());
417 }
418
419 #[test]
420 fn test_cumulative_tracker() {
421 let pid = std::process::id() as i32;
422 let mut tracker = CumulativeTracker::new();
423
424 let first = tracker.get_cpu_percent(pid);
426 assert_eq!(first, Some(0.0));
427
428 let mut sum = 0u64;
430 for i in 0..1_000_000 {
431 sum = sum.wrapping_add(i);
432 }
433 std::hint::black_box(sum);
434
435 let second = tracker.get_cpu_percent(pid);
437 assert!(second.is_some());
438 }
439
440 #[test]
441 fn test_collect_tree_pids() {
442 let pid = std::process::id() as i32;
443 let pids = collect_tree_pids(pid);
444 assert!(pids.contains(&pid));
445 }
446
447 #[test]
448 fn test_is_descendant_of_self() {
449 let map = build_parent_map();
450 let pid = std::process::id() as i32;
451 assert!(is_descendant_of(pid, pid, &map));
452 }
453
454 #[test]
455 fn test_is_descendant_of_parent() {
456 let map = build_parent_map();
457 let pid = std::process::id() as i32;
458 if let Some(&ppid) = map.get(&pid) {
459 assert!(is_descendant_of(pid, ppid, &map));
460 }
461 }
462
463 #[test]
464 fn test_is_descendant_of_unrelated() {
465 let map = build_parent_map();
466 let our_pid = std::process::id() as i32;
467 assert!(!is_descendant_of(1, our_pid, &map));
469 }
470
471 #[test]
472 fn test_is_descendant_of_empty_map() {
473 let empty_map: HashMap<i32, i32> = HashMap::new();
474 assert!(is_descendant_of(100, 100, &empty_map));
475 assert!(!is_descendant_of(100, 200, &empty_map));
476 }
477}