1use serde::Serialize;
29
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
32#[serde(rename_all = "camelCase")]
33pub struct ProcessMemory {
34 pub footprint: u64,
37 pub resident: u64,
39 pub peak: Option<u64>,
43}
44
45pub fn process_memory() -> Option<ProcessMemory> {
48 imp::process_memory()
49}
50
51pub fn thread_count() -> Option<u64> {
54 imp::thread_count()
55}
56
57pub fn release_free_memory() -> u64 {
61 imp::release_free_memory()
62}
63
64pub fn space_efficient_malloc() -> bool {
68 cfg!(target_os = "macos") && std::env::var_os("MallocSpaceEfficient").is_some_and(|v| v != "0" && !v.is_empty())
69}
70
71#[cfg(target_os = "macos")]
72#[allow(unsafe_code)]
73mod imp {
74 use super::ProcessMemory;
75
76 pub fn process_memory() -> Option<ProcessMemory> {
77 let mut info = std::mem::MaybeUninit::<libc::rusage_info_v4>::zeroed();
78 let r = unsafe {
83 libc::proc_pid_rusage(std::process::id() as libc::c_int, libc::RUSAGE_INFO_V4, info.as_mut_ptr().cast::<libc::rusage_info_t>())
84 };
85 if r != 0 {
86 return None;
87 }
88 let info = unsafe { info.assume_init() };
91 Some(ProcessMemory {
92 footprint: info.ri_phys_footprint,
93 resident: info.ri_resident_size,
94 peak: Some(info.ri_lifetime_max_phys_footprint.max(info.ri_phys_footprint)),
95 })
96 }
97
98 pub fn thread_count() -> Option<u64> {
99 let mut info = std::mem::MaybeUninit::<libc::proc_taskinfo>::zeroed();
100 let size = libc::c_int::try_from(std::mem::size_of::<libc::proc_taskinfo>()).ok()?;
101 let n = unsafe { libc::proc_pidinfo(std::process::id() as libc::c_int, libc::PROC_PIDTASKINFO, 0, info.as_mut_ptr().cast(), size) };
105 if n != size {
106 return None;
107 }
108 let info = unsafe { info.assume_init() };
111 u64::try_from(info.pti_threadnum).ok()
112 }
113
114 pub fn release_free_memory() -> u64 {
117 0
118 }
119}
120
121#[cfg(target_os = "linux")]
122#[allow(unsafe_code)]
123mod imp {
124 use super::ProcessMemory;
125
126 pub fn process_memory() -> Option<ProcessMemory> {
127 parse_status(&std::fs::read_to_string("/proc/self/status").ok()?)
128 }
129
130 pub(super) fn parse_status(text: &str) -> Option<ProcessMemory> {
132 let field = |name: &str| -> Option<u64> {
133 let line = text.lines().find(|l| l.starts_with(name) && l.as_bytes().get(name.len()) == Some(&b':'))?;
134 let kb: u64 = line.get(name.len() + 1..)?.trim().trim_end_matches("kB").trim().parse().ok()?;
135 Some(kb.saturating_mul(1024))
136 };
137 let resident = field("VmRSS")?;
138 let footprint = field("RssAnon").map_or(resident, |anon| anon.saturating_add(field("VmSwap").unwrap_or(0)));
139 Some(ProcessMemory { footprint, resident, peak: None })
140 }
141
142 pub fn thread_count() -> Option<u64> {
143 super::threads_in_status(&std::fs::read_to_string("/proc/self/status").ok()?)
144 }
145
146 pub fn release_free_memory() -> u64 {
147 #[cfg(target_env = "gnu")]
148 {
149 unsafe { libc::malloc_trim(0) };
151 }
152 0
153 }
154}
155
156#[cfg(target_os = "windows")]
157#[allow(unsafe_code)]
158mod imp {
159 use super::ProcessMemory;
160 use windows::Win32::Foundation::CloseHandle;
161 use windows::Win32::System::Diagnostics::ToolHelp::{
162 CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW, TH32CS_SNAPPROCESS,
163 };
164 use windows::Win32::System::ProcessStatus::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS, PROCESS_MEMORY_COUNTERS_EX};
165 use windows::Win32::System::Threading::GetCurrentProcess;
166
167 pub fn process_memory() -> Option<ProcessMemory> {
168 let mut c = PROCESS_MEMORY_COUNTERS_EX { cb: std::mem::size_of::<PROCESS_MEMORY_COUNTERS_EX>() as u32, ..Default::default() };
169 let ok = unsafe { GetProcessMemoryInfo(GetCurrentProcess(), (&raw mut c).cast::<PROCESS_MEMORY_COUNTERS>(), c.cb) };
173 ok.ok()?;
174 let footprint = c.PrivateUsage as u64;
175 Some(ProcessMemory { footprint, resident: c.WorkingSetSize as u64, peak: Some((c.PeakPagefileUsage as u64).max(footprint)) })
176 }
177
178 pub fn thread_count() -> Option<u64> {
180 let snap = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }.ok()?;
182 let me = std::process::id();
183 let mut entry = PROCESSENTRY32W { dwSize: std::mem::size_of::<PROCESSENTRY32W>() as u32, ..Default::default() };
184 let mut more = unsafe { Process32FirstW(snap, &raw mut entry) }.is_ok();
187 let mut found = None;
188 for _ in 0..1_000_000 {
190 if !more {
191 break;
192 }
193 if entry.th32ProcessID == me {
194 found = Some(u64::from(entry.cntThreads));
195 break;
196 }
197 more = unsafe { Process32NextW(snap, &raw mut entry) }.is_ok();
199 }
200 let _ = unsafe { CloseHandle(snap) };
202 found
203 }
204
205 pub fn release_free_memory() -> u64 {
206 0
207 }
208}
209
210#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
211mod imp {
212 use super::ProcessMemory;
213
214 pub fn process_memory() -> Option<ProcessMemory> {
215 None
216 }
217
218 pub fn thread_count() -> Option<u64> {
219 None
220 }
221
222 pub fn release_free_memory() -> u64 {
223 0
224 }
225}
226
227#[cfg_attr(not(any(target_os = "linux", test)), allow(dead_code))]
229fn threads_in_status(text: &str) -> Option<u64> {
230 text.lines().find_map(|l| l.strip_prefix("Threads:")).and_then(|v| v.trim().parse().ok())
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
238 #[test]
239 fn the_footprint_counts_touched_memory() {
240 let before = process_memory().expect("process memory");
241 assert!(before.footprint > 0 && before.resident > 0, "{before:?}");
242 let blocks: Vec<Vec<u8>> = (0..256).map(|_| vec![1u8; 1 << 20]).collect();
244 let full = process_memory().expect("process memory");
245 assert!(full.footprint >= before.footprint + (200 << 20), "{before:?} → {full:?}");
246 drop(blocks);
247 release_free_memory();
250 assert!(process_memory().is_some());
251 }
252
253 #[cfg(all(target_os = "linux", target_env = "gnu"))]
254 #[test]
255 fn malloc_trim_hands_freed_heap_back() {
256 let blocks: Vec<Vec<u8>> = (0..2048).map(|_| vec![1u8; 100 << 10]).collect();
258 let full = process_memory().expect("process memory");
259 drop(blocks);
260 release_free_memory();
261 let after = process_memory().expect("process memory");
262 assert!(after.footprint + (150 << 20) <= full.footprint, "{full:?} → {after:?}");
263 }
264
265 #[cfg(any(target_os = "macos", target_os = "windows"))]
266 #[test]
267 fn the_os_peak_outlives_a_freed_spike() {
268 let before = process_memory().expect("process memory");
269 let peak_before = before.peak.expect("peak");
270 assert!(peak_before >= before.footprint, "{before:?}");
271 let blocks: Vec<Vec<u8>> = (0..128).map(|_| vec![1u8; 1 << 20]).collect();
273 let during = process_memory().expect("process memory");
274 drop(blocks);
275 let after = process_memory().expect("process memory");
276 let peak = after.peak.expect("peak");
277 assert!(peak >= during.footprint.max(peak_before), "{before:?} {during:?} {after:?}");
278 assert!(peak >= before.footprint + (100 << 20), "{before:?} → {after:?}");
279 }
280
281 #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
282 #[test]
283 fn threads_are_counted() {
284 let before = thread_count().expect("thread count");
285 assert!(before >= 1);
286 let (tx, rx) = std::sync::mpsc::channel::<()>();
287 let (ready_tx, ready_rx) = std::sync::mpsc::channel::<()>();
288 let t = std::thread::spawn(move || {
289 let _ = ready_tx.send(());
290 let _ = rx.recv();
291 });
292 let _ = ready_rx.recv();
293 let during = thread_count().expect("thread count");
295 assert!(during >= 2, "{before} → {during}");
296 let _ = tx.send(());
297 let _ = t.join();
298 }
299
300 #[test]
301 fn proc_status_threads_parse() {
302 assert_eq!(threads_in_status("Name:\tx\nThreads:\t7\nVmRSS: 1 kB\n"), Some(7));
303 assert_eq!(threads_in_status("Name: x\n"), None);
304 }
305
306 #[test]
307 fn the_space_efficient_mode_follows_the_environment() {
308 let set = std::env::var_os("MallocSpaceEfficient").is_some_and(|v| v != "0" && !v.is_empty());
309 assert_eq!(space_efficient_malloc(), set && cfg!(target_os = "macos"));
310 }
311
312 #[cfg(target_os = "linux")]
313 #[test]
314 fn linux_status_is_parsed() {
315 let text = "Name:\tx\nVmRSS:\t 2048 kB\nRssAnon:\t 1024 kB\nVmSwap:\t 4 kB\n";
316 assert_eq!(imp::parse_status(text), Some(ProcessMemory { footprint: 1028 * 1024, resident: 2048 * 1024, peak: None }));
317 assert_eq!(imp::parse_status("VmRSS: x kB"), None);
318 }
319}