dataprof_core/
memory_sampler.rs1use std::time::{Duration, Instant};
2
3use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, System};
4
5const MIN_SAMPLE_INTERVAL: Duration = Duration::from_millis(250);
15
16pub struct PeakMemorySampler {
26 system: System,
27 pid: Option<Pid>,
28 peak_bytes: u64,
29 last_sample: Option<Instant>,
30 refreshes: u64,
31}
32
33impl PeakMemorySampler {
34 pub fn new() -> Self {
36 let mut sampler = Self {
37 system: System::new(),
38 pid: sysinfo::get_current_pid().ok(),
39 peak_bytes: 0,
40 last_sample: None,
41 refreshes: 0,
42 };
43 sampler.sample_now();
44 sampler
45 }
46
47 pub fn sample(&mut self) {
50 if self
51 .last_sample
52 .is_some_and(|last| last.elapsed() < MIN_SAMPLE_INTERVAL)
53 {
54 return;
55 }
56 self.sample_now();
57 }
58
59 pub fn sample_now(&mut self) {
63 self.last_sample = Some(Instant::now());
64 let Some(pid) = self.pid else { return };
65 self.refreshes += 1;
66 self.system.refresh_processes_specifics(
67 ProcessesToUpdate::Some(&[pid]),
68 false,
69 ProcessRefreshKind::nothing().with_memory(),
70 );
71 if let Some(process) = self.system.process(pid) {
72 self.peak_bytes = self.peak_bytes.max(process.memory());
73 }
74 }
75
76 pub fn peak_mb(&self) -> Option<f64> {
81 (self.peak_bytes > 0).then(|| self.peak_bytes as f64 / (1024.0 * 1024.0))
82 }
83}
84
85impl Default for PeakMemorySampler {
86 fn default() -> Self {
87 Self::new()
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn repeated_samples_within_the_interval_do_not_refresh() {
97 let mut sampler = PeakMemorySampler::new();
98 let before = sampler.refreshes;
99 for _ in 0..1_000 {
100 sampler.sample();
101 }
102 assert_eq!(sampler.refreshes, before, "a tight loop must not refresh");
103 sampler.sample_now();
104 assert_eq!(sampler.refreshes, before + 1, "sample_now always refreshes");
105 }
106
107 #[test]
108 fn test_sampler_reports_positive_peak() {
109 let sampler = PeakMemorySampler::new();
110 let peak = sampler
111 .peak_mb()
112 .expect("current process RSS should be readable on supported platforms");
113 assert!(peak > 0.0, "peak RSS must be positive, got {peak}");
114 }
115
116 #[test]
117 fn test_peak_is_monotonic_across_samples() {
118 let mut sampler = PeakMemorySampler::new();
119 let first = sampler.peak_mb().expect("initial reading");
120 let ballast = vec![1u8; 4 * 1024 * 1024];
122 sampler.sample_now();
123 let second = sampler.peak_mb().expect("second reading");
124 assert!(
125 second >= first,
126 "peak must never decrease: {first} -> {second}"
127 );
128 drop(ballast);
129 }
130}