use serde::Serialize;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProcessMemory {
pub footprint: u64,
pub resident: u64,
pub peak: Option<u64>,
}
pub fn process_memory() -> Option<ProcessMemory> {
imp::process_memory()
}
pub fn thread_count() -> Option<u64> {
imp::thread_count()
}
pub fn release_free_memory() -> u64 {
imp::release_free_memory()
}
pub fn space_efficient_malloc() -> bool {
cfg!(target_os = "macos") && std::env::var_os("MallocSpaceEfficient").is_some_and(|v| v != "0" && !v.is_empty())
}
#[cfg(target_os = "macos")]
#[allow(unsafe_code)]
mod imp {
use super::ProcessMemory;
pub fn process_memory() -> Option<ProcessMemory> {
let mut info = std::mem::MaybeUninit::<libc::rusage_info_v4>::zeroed();
let r = unsafe {
libc::proc_pid_rusage(std::process::id() as libc::c_int, libc::RUSAGE_INFO_V4, info.as_mut_ptr().cast::<libc::rusage_info_t>())
};
if r != 0 {
return None;
}
let info = unsafe { info.assume_init() };
Some(ProcessMemory {
footprint: info.ri_phys_footprint,
resident: info.ri_resident_size,
peak: Some(info.ri_lifetime_max_phys_footprint.max(info.ri_phys_footprint)),
})
}
pub fn thread_count() -> Option<u64> {
let mut info = std::mem::MaybeUninit::<libc::proc_taskinfo>::zeroed();
let size = libc::c_int::try_from(std::mem::size_of::<libc::proc_taskinfo>()).ok()?;
let n = unsafe { libc::proc_pidinfo(std::process::id() as libc::c_int, libc::PROC_PIDTASKINFO, 0, info.as_mut_ptr().cast(), size) };
if n != size {
return None;
}
let info = unsafe { info.assume_init() };
u64::try_from(info.pti_threadnum).ok()
}
pub fn release_free_memory() -> u64 {
0
}
}
#[cfg(target_os = "linux")]
#[allow(unsafe_code)]
mod imp {
use super::ProcessMemory;
pub fn process_memory() -> Option<ProcessMemory> {
parse_status(&std::fs::read_to_string("/proc/self/status").ok()?)
}
pub(super) fn parse_status(text: &str) -> Option<ProcessMemory> {
let field = |name: &str| -> Option<u64> {
let line = text.lines().find(|l| l.starts_with(name) && l.as_bytes().get(name.len()) == Some(&b':'))?;
let kb: u64 = line.get(name.len() + 1..)?.trim().trim_end_matches("kB").trim().parse().ok()?;
Some(kb.saturating_mul(1024))
};
let resident = field("VmRSS")?;
let footprint = field("RssAnon").map_or(resident, |anon| anon.saturating_add(field("VmSwap").unwrap_or(0)));
Some(ProcessMemory { footprint, resident, peak: None })
}
pub fn thread_count() -> Option<u64> {
super::threads_in_status(&std::fs::read_to_string("/proc/self/status").ok()?)
}
pub fn release_free_memory() -> u64 {
#[cfg(target_env = "gnu")]
{
unsafe { libc::malloc_trim(0) };
}
0
}
}
#[cfg(target_os = "windows")]
#[allow(unsafe_code)]
mod imp {
use super::ProcessMemory;
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW, TH32CS_SNAPPROCESS,
};
use windows::Win32::System::ProcessStatus::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS, PROCESS_MEMORY_COUNTERS_EX};
use windows::Win32::System::Threading::GetCurrentProcess;
pub fn process_memory() -> Option<ProcessMemory> {
let mut c = PROCESS_MEMORY_COUNTERS_EX { cb: std::mem::size_of::<PROCESS_MEMORY_COUNTERS_EX>() as u32, ..Default::default() };
let ok = unsafe { GetProcessMemoryInfo(GetCurrentProcess(), (&raw mut c).cast::<PROCESS_MEMORY_COUNTERS>(), c.cb) };
ok.ok()?;
let footprint = c.PrivateUsage as u64;
Some(ProcessMemory { footprint, resident: c.WorkingSetSize as u64, peak: Some((c.PeakPagefileUsage as u64).max(footprint)) })
}
pub fn thread_count() -> Option<u64> {
let snap = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }.ok()?;
let me = std::process::id();
let mut entry = PROCESSENTRY32W { dwSize: std::mem::size_of::<PROCESSENTRY32W>() as u32, ..Default::default() };
let mut more = unsafe { Process32FirstW(snap, &raw mut entry) }.is_ok();
let mut found = None;
for _ in 0..1_000_000 {
if !more {
break;
}
if entry.th32ProcessID == me {
found = Some(u64::from(entry.cntThreads));
break;
}
more = unsafe { Process32NextW(snap, &raw mut entry) }.is_ok();
}
let _ = unsafe { CloseHandle(snap) };
found
}
pub fn release_free_memory() -> u64 {
0
}
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
mod imp {
use super::ProcessMemory;
pub fn process_memory() -> Option<ProcessMemory> {
None
}
pub fn thread_count() -> Option<u64> {
None
}
pub fn release_free_memory() -> u64 {
0
}
}
#[cfg_attr(not(any(target_os = "linux", test)), allow(dead_code))]
fn threads_in_status(text: &str) -> Option<u64> {
text.lines().find_map(|l| l.strip_prefix("Threads:")).and_then(|v| v.trim().parse().ok())
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
#[test]
fn the_footprint_counts_touched_memory() {
let before = process_memory().expect("process memory");
assert!(before.footprint > 0 && before.resident > 0, "{before:?}");
let blocks: Vec<Vec<u8>> = (0..256).map(|_| vec![1u8; 1 << 20]).collect();
let full = process_memory().expect("process memory");
assert!(full.footprint >= before.footprint + (200 << 20), "{before:?} → {full:?}");
drop(blocks);
release_free_memory();
assert!(process_memory().is_some());
}
#[cfg(all(target_os = "linux", target_env = "gnu"))]
#[test]
fn malloc_trim_hands_freed_heap_back() {
let blocks: Vec<Vec<u8>> = (0..2048).map(|_| vec![1u8; 100 << 10]).collect();
let full = process_memory().expect("process memory");
drop(blocks);
release_free_memory();
let after = process_memory().expect("process memory");
assert!(after.footprint + (150 << 20) <= full.footprint, "{full:?} → {after:?}");
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
#[test]
fn the_os_peak_outlives_a_freed_spike() {
let before = process_memory().expect("process memory");
let peak_before = before.peak.expect("peak");
assert!(peak_before >= before.footprint, "{before:?}");
let blocks: Vec<Vec<u8>> = (0..128).map(|_| vec![1u8; 1 << 20]).collect();
let during = process_memory().expect("process memory");
drop(blocks);
let after = process_memory().expect("process memory");
let peak = after.peak.expect("peak");
assert!(peak >= during.footprint.max(peak_before), "{before:?} {during:?} {after:?}");
assert!(peak >= before.footprint + (100 << 20), "{before:?} → {after:?}");
}
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
#[test]
fn threads_are_counted() {
let before = thread_count().expect("thread count");
assert!(before >= 1);
let (tx, rx) = std::sync::mpsc::channel::<()>();
let (ready_tx, ready_rx) = std::sync::mpsc::channel::<()>();
let t = std::thread::spawn(move || {
let _ = ready_tx.send(());
let _ = rx.recv();
});
let _ = ready_rx.recv();
let during = thread_count().expect("thread count");
assert!(during >= 2, "{before} → {during}");
let _ = tx.send(());
let _ = t.join();
}
#[test]
fn proc_status_threads_parse() {
assert_eq!(threads_in_status("Name:\tx\nThreads:\t7\nVmRSS: 1 kB\n"), Some(7));
assert_eq!(threads_in_status("Name: x\n"), None);
}
#[test]
fn the_space_efficient_mode_follows_the_environment() {
let set = std::env::var_os("MallocSpaceEfficient").is_some_and(|v| v != "0" && !v.is_empty());
assert_eq!(space_efficient_malloc(), set && cfg!(target_os = "macos"));
}
#[cfg(target_os = "linux")]
#[test]
fn linux_status_is_parsed() {
let text = "Name:\tx\nVmRSS:\t 2048 kB\nRssAnon:\t 1024 kB\nVmSwap:\t 4 kB\n";
assert_eq!(imp::parse_status(text), Some(ProcessMemory { footprint: 1028 * 1024, resident: 2048 * 1024, peak: None }));
assert_eq!(imp::parse_status("VmRSS: x kB"), None);
}
}