xchecker_utils/
process_memory.rs1use anyhow::Result;
7use sysinfo::{Pid, System};
8
9#[derive(Debug, Clone)]
11pub struct ProcessMemory {
12 pub rss_mb: f64,
14 #[cfg(target_os = "windows")]
16 pub commit_mb: f64,
17 #[cfg(target_os = "windows")]
19 pub ffi_fallback: bool,
20}
21
22impl ProcessMemory {
23 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 #[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 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 #[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 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 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 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 let rss_bytes = process.memory();
99 let rss_mb = rss_bytes as f64 / (1024.0 * 1024.0);
100
101 Ok(Self {
103 rss_mb,
104 commit_mb: 0.0,
105 ffi_fallback: true,
106 })
107 }
108 }
109
110 #[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 assert!(
139 mem.rss_mb > 0.0,
140 "RSS should be positive, got {}",
141 mem.rss_mb
142 );
143
144 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 let mem = ProcessMemory::current()?;
158
159 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 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 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 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 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 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 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 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 assert!(
264 mem.rss_mb > 0.0,
265 "RSS should be positive, got {}",
266 mem.rss_mb
267 );
268
269 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}