Skip to main content

ferrum_types/
process_memory.rs

1use crate::MemorySnapshot;
2
3#[derive(Clone, Debug, PartialEq, Eq)]
4pub struct ProcessMemorySample {
5    pub current_bytes: u64,
6    pub high_water_bytes: u64,
7    pub source: &'static str,
8}
9
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct ProcessMemoryObservation {
12    pub before_bytes: u64,
13    pub after_bytes: u64,
14    pub current_bytes: u64,
15    pub high_water_bytes: u64,
16    pub source: &'static str,
17}
18
19#[derive(Clone, Copy, Debug, Default)]
20pub struct ProcessMemorySampler;
21
22impl ProcessMemorySampler {
23    pub fn sample(&self) -> Option<ProcessMemorySample> {
24        sample_process_memory()
25    }
26
27    pub fn observe(&self, before: Option<ProcessMemorySample>) -> Option<ProcessMemoryObservation> {
28        let after = self.sample()?;
29        Some(ProcessMemoryObservation::from_samples(before, after))
30    }
31}
32
33impl ProcessMemoryObservation {
34    pub fn from_samples(before: Option<ProcessMemorySample>, after: ProcessMemorySample) -> Self {
35        let before_bytes = before
36            .as_ref()
37            .map(|sample| sample.current_bytes)
38            .unwrap_or(after.current_bytes);
39        let high_water_bytes = before
40            .as_ref()
41            .map(|sample| sample.high_water_bytes)
42            .unwrap_or(0)
43            .max(after.high_water_bytes)
44            .max(before_bytes)
45            .max(after.current_bytes);
46        Self {
47            before_bytes,
48            after_bytes: after.current_bytes,
49            current_bytes: after.current_bytes,
50            high_water_bytes,
51            source: after.source,
52        }
53    }
54
55    pub fn from_sample(sample: ProcessMemorySample) -> Self {
56        Self::from_samples(Some(sample.clone()), sample)
57    }
58
59    pub fn to_snapshot(&self, scope: impl Into<String>, backend: Option<&str>) -> MemorySnapshot {
60        MemorySnapshot {
61            scope: scope.into(),
62            backend: backend.map(str::to_string),
63            before_bytes: Some(self.before_bytes),
64            after_bytes: Some(self.after_bytes),
65            current_bytes: Some(self.current_bytes),
66            high_water_bytes: Some(self.high_water_bytes),
67            available_bytes: None,
68        }
69    }
70}
71
72#[cfg(target_os = "linux")]
73fn current_resident_bytes() -> Option<u64> {
74    let statm = std::fs::read_to_string("/proc/self/statm").ok()?;
75    let resident_pages = statm.split_whitespace().nth(1)?.parse::<u64>().ok()?;
76    let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
77    if page_size <= 0 {
78        return None;
79    }
80    resident_pages.checked_mul(page_size as u64)
81}
82
83#[cfg(all(not(windows), not(target_os = "linux")))]
84fn current_resident_bytes() -> Option<u64> {
85    None
86}
87
88#[cfg(unix)]
89fn high_water_bytes() -> Option<u64> {
90    let mut usage = std::mem::MaybeUninit::<libc::rusage>::zeroed();
91    let rc = unsafe { libc::getrusage(libc::RUSAGE_SELF, usage.as_mut_ptr()) };
92    if rc != 0 {
93        return None;
94    }
95    let max_rss = unsafe { usage.assume_init() }.ru_maxrss;
96    if max_rss <= 0 {
97        return None;
98    }
99    #[cfg(target_os = "macos")]
100    {
101        Some(max_rss as u64)
102    }
103    #[cfg(not(target_os = "macos"))]
104    {
105        (max_rss as u64).checked_mul(1024)
106    }
107}
108
109#[cfg(not(any(unix, windows)))]
110fn high_water_bytes() -> Option<u64> {
111    None
112}
113
114#[cfg(not(windows))]
115pub fn sample_process_memory() -> Option<ProcessMemorySample> {
116    let high_water = high_water_bytes()?;
117    let current = current_resident_bytes().unwrap_or(high_water);
118    Some(ProcessMemorySample {
119        current_bytes: current,
120        high_water_bytes: high_water.max(current),
121        source: process_memory_source(),
122    })
123}
124
125#[cfg(windows)]
126pub fn sample_process_memory() -> Option<ProcessMemorySample> {
127    use windows_sys::Win32::System::{
128        ProcessStatus::{K32GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS},
129        Threading::GetCurrentProcess,
130    };
131
132    let mut counters = PROCESS_MEMORY_COUNTERS {
133        cb: std::mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32,
134        ..Default::default()
135    };
136    // SAFETY: the current-process pseudo-handle remains valid without being closed;
137    // counters points to a writable structure with the declared size.
138    let succeeded =
139        unsafe { K32GetProcessMemoryInfo(GetCurrentProcess(), &mut counters, counters.cb) };
140    if succeeded == 0 {
141        return None;
142    }
143
144    // Working-set counters are resident bytes, distinct from page-file commit charge.
145    let current_bytes = counters.WorkingSetSize as u64;
146    Some(ProcessMemorySample {
147        current_bytes,
148        high_water_bytes: (counters.PeakWorkingSetSize as u64).max(current_bytes),
149        source: "windows_process_memory_counters",
150    })
151}
152
153#[cfg(target_os = "linux")]
154fn process_memory_source() -> &'static str {
155    "procfs_statm_plus_getrusage"
156}
157
158#[cfg(all(unix, not(target_os = "linux")))]
159fn process_memory_source() -> &'static str {
160    "getrusage_maxrss"
161}
162
163#[cfg(not(any(unix, windows)))]
164fn process_memory_source() -> &'static str {
165    "unsupported"
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn process_memory_observation_builds_valid_snapshot() {
174        let before = ProcessMemorySample {
175            current_bytes: 100,
176            high_water_bytes: 110,
177            source: "test",
178        };
179        let after = ProcessMemorySample {
180            current_bytes: 150,
181            high_water_bytes: 160,
182            source: "test",
183        };
184        let observation = ProcessMemoryObservation::from_samples(Some(before), after);
185        let snapshot = observation.to_snapshot("process", Some("actual"));
186        assert_eq!(snapshot.before_bytes, Some(100));
187        assert_eq!(snapshot.after_bytes, Some(150));
188        assert_eq!(snapshot.current_bytes, Some(150));
189        assert_eq!(snapshot.high_water_bytes, Some(160));
190        snapshot.validate().unwrap();
191    }
192
193    #[cfg(any(unix, windows))]
194    #[test]
195    fn process_memory_sampler_returns_valid_resident_bytes() {
196        let sample = ProcessMemorySampler
197            .sample()
198            .expect("supported platform samples current-process memory");
199        assert!(sample.current_bytes > 0);
200        assert!(sample.high_water_bytes >= sample.current_bytes);
201        ProcessMemoryObservation::from_sample(sample)
202            .to_snapshot("process", None)
203            .validate()
204            .unwrap();
205    }
206}