Skip to main content

xchecker_utils/
process_memory.rs

1//! Process-scoped memory tracking for benchmarking (R3.1, R3.2, R3.3, R3.4, R3.5)
2//!
3//! This module provides platform-specific memory measurement for the current process,
4//! reporting RSS (Resident Set Size) on all platforms and commit memory on Windows.
5
6use anyhow::Result;
7use sysinfo::{Pid, System};
8
9/// Process memory metrics
10#[derive(Debug, Clone)]
11pub struct ProcessMemory {
12    /// Resident Set Size in MB (all platforms)
13    pub rss_mb: f64,
14    /// Commit memory in MB (Windows only - private bytes)
15    #[cfg(target_os = "windows")]
16    pub commit_mb: f64,
17    /// Warning flag indicating FFI fallback was used (Windows only)
18    #[cfg(target_os = "windows")]
19    pub ffi_fallback: bool,
20}
21
22impl ProcessMemory {
23    /// Get current process memory usage
24    pub fn current() -> Result<Self> {
25        #[cfg(target_os = "windows")]
26        {
27            Self::current_windows()
28        }
29
30        #[cfg(not(target_os = "windows"))]
31        {
32            Self::current_unix()
33        }
34    }
35
36    /// Unix implementation using sysinfo for RSS measurement
37    #[cfg(not(target_os = "windows"))]
38    fn current_unix() -> Result<Self> {
39        use sysinfo::ProcessesToUpdate;
40
41        let mut sys = System::new();
42        let pid = Pid::from(std::process::id() as usize);
43        sys.refresh_processes(ProcessesToUpdate::Some(&[pid]), false);
44
45        let process = sys
46            .process(pid)
47            .ok_or_else(|| anyhow::anyhow!("Failed to get process information"))?;
48
49        // sysinfo returns memory in bytes
50        let rss_bytes = process.memory();
51        let rss_mb = rss_bytes as f64 / (1024.0 * 1024.0);
52
53        Ok(Self { rss_mb })
54    }
55
56    /// Windows implementation using `K32GetProcessMemoryInfo` with fallback
57    #[cfg(target_os = "windows")]
58    fn current_windows() -> Result<Self> {
59        use std::mem;
60        use winapi::um::processthreadsapi::GetCurrentProcess;
61        use winapi::um::psapi::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS_EX};
62
63        // Try FFI first
64        let mut pmc: PROCESS_MEMORY_COUNTERS_EX = unsafe { mem::zeroed() };
65        pmc.cb = mem::size_of::<PROCESS_MEMORY_COUNTERS_EX>() as u32;
66
67        let success = unsafe {
68            GetProcessMemoryInfo(
69                GetCurrentProcess(),
70                (&raw mut pmc).cast(),
71                mem::size_of::<PROCESS_MEMORY_COUNTERS_EX>() as u32,
72            )
73        };
74
75        if success != 0 {
76            // FFI succeeded
77            let rss_mb = pmc.WorkingSetSize as f64 / (1024.0 * 1024.0);
78            let commit_mb = pmc.PrivateUsage as f64 / (1024.0 * 1024.0);
79
80            Ok(Self {
81                rss_mb,
82                commit_mb,
83                ffi_fallback: false,
84            })
85        } else {
86            // FFI failed, fall back to sysinfo
87            use sysinfo::ProcessesToUpdate;
88
89            let mut sys = System::new();
90            let pid = Pid::from(std::process::id() as usize);
91            sys.refresh_processes(ProcessesToUpdate::Some(&[pid]), false);
92
93            let process = sys
94                .process(pid)
95                .ok_or_else(|| anyhow::anyhow!("Failed to get process information"))?;
96
97            // sysinfo returns memory in bytes
98            let rss_bytes = process.memory();
99            let rss_mb = rss_bytes as f64 / (1024.0 * 1024.0);
100
101            // Set commit_mb to 0.0 as fallback (we don't have this info from sysinfo)
102            Ok(Self {
103                rss_mb,
104                commit_mb: 0.0,
105                ffi_fallback: true,
106            })
107        }
108    }
109
110    /// Display memory usage with one decimal precision
111    #[must_use]
112    pub fn display(&self) -> String {
113        #[cfg(target_os = "windows")]
114        {
115            if self.ffi_fallback {
116                format!("RSS: {:.1}MB (FFI fallback)", self.rss_mb)
117            } else {
118                format!("RSS: {:.1}MB, Commit: {:.1}MB", self.rss_mb, self.commit_mb)
119            }
120        }
121
122        #[cfg(not(target_os = "windows"))]
123        {
124            format!("RSS: {:.1}MB", self.rss_mb)
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_process_memory_current() -> Result<()> {
135        let mem = ProcessMemory::current()?;
136
137        // RSS should be positive
138        assert!(
139            mem.rss_mb > 0.0,
140            "RSS should be positive, got {}",
141            mem.rss_mb
142        );
143
144        // RSS should be reasonable (less than 10GB for a test process)
145        assert!(
146            mem.rss_mb < 10240.0,
147            "RSS should be reasonable, got {}",
148            mem.rss_mb
149        );
150
151        Ok(())
152    }
153
154    #[test]
155    fn test_process_scoped_not_system_wide() -> Result<()> {
156        // This test verifies that we're measuring process-scoped memory, not system totals
157        let mem = ProcessMemory::current()?;
158
159        // Process RSS should be much smaller than typical system memory
160        // A test process should use less than 1GB
161        assert!(
162            mem.rss_mb < 1024.0,
163            "Process RSS should be < 1GB for a test process, got {:.1}MB. \
164             This suggests we might be measuring system-wide memory instead of process-scoped.",
165            mem.rss_mb
166        );
167
168        // Process RSS should be at least a few MB (reasonable for a Rust test binary)
169        assert!(
170            mem.rss_mb > 1.0,
171            "Process RSS should be > 1MB for a Rust test binary, got {:.1}MB",
172            mem.rss_mb
173        );
174
175        #[cfg(target_os = "windows")]
176        {
177            if !mem.ffi_fallback {
178                // Commit memory should also be reasonable for a process
179                assert!(
180                    mem.commit_mb < 2048.0,
181                    "Process commit should be < 2GB for a test process, got {:.1}MB. \
182                     This suggests we might be measuring system-wide memory instead of process-scoped.",
183                    mem.commit_mb
184                );
185
186                assert!(
187                    mem.commit_mb > 0.0,
188                    "Process commit should be positive when not using fallback, got {:.1}MB",
189                    mem.commit_mb
190                );
191            }
192        }
193
194        Ok(())
195    }
196
197    #[test]
198    fn test_display_format() -> Result<()> {
199        let mem = ProcessMemory::current()?;
200        let display = mem.display();
201
202        // Should contain "RSS:" and "MB"
203        assert!(
204            display.contains("RSS:"),
205            "Display should contain 'RSS:', got: {display}"
206        );
207        assert!(
208            display.contains("MB"),
209            "Display should contain 'MB', got: {display}"
210        );
211
212        // Should have one decimal place (check for pattern like "123.4MB")
213        let has_decimal = display.chars().any(|c| c == '.');
214        assert!(
215            has_decimal,
216            "Display should have decimal point, got: {display}"
217        );
218
219        Ok(())
220    }
221
222    #[cfg(target_os = "windows")]
223    #[test]
224    fn test_windows_memory_fields() -> Result<()> {
225        let mem = ProcessMemory::current()?;
226
227        // Both fields should be non-negative
228        assert!(
229            mem.rss_mb >= 0.0,
230            "RSS should be non-negative, got {}",
231            mem.rss_mb
232        );
233        assert!(
234            mem.commit_mb >= 0.0,
235            "Commit should be non-negative, got {}",
236            mem.commit_mb
237        );
238
239        // If fallback is used, commit should be 0
240        if mem.ffi_fallback {
241            assert_eq!(mem.commit_mb, 0.0, "Commit should be 0 when using fallback");
242            assert!(
243                mem.display().contains("FFI fallback"),
244                "Display should indicate fallback"
245            );
246        } else {
247            // If not fallback, commit should be positive
248            assert!(
249                mem.commit_mb > 0.0,
250                "Commit should be positive when not using fallback"
251            );
252        }
253
254        Ok(())
255    }
256
257    #[cfg(not(target_os = "windows"))]
258    #[test]
259    fn test_unix_memory_fields() -> Result<()> {
260        let mem = ProcessMemory::current()?;
261
262        // RSS should be positive
263        assert!(
264            mem.rss_mb > 0.0,
265            "RSS should be positive, got {}",
266            mem.rss_mb
267        );
268
269        // Display should not contain "Commit"
270        let display = mem.display();
271        assert!(
272            !display.contains("Commit"),
273            "Unix display should not contain 'Commit', got: {}",
274            display
275        );
276
277        Ok(())
278    }
279}