pub fn peak_working_set_bytes() -> Option<u64> {
peak_working_set_impl().ok()
}
#[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
pub struct MemorySample {
pub working_set_kb: u64,
pub peak_working_set_kb: u64,
pub private_bytes_kb: u64,
}
pub fn sample_memory() -> Option<MemorySample> {
let c = counters_impl().ok()?;
Some(MemorySample {
working_set_kb: c.working_set_size as u64 / 1024,
peak_working_set_kb: c.peak_working_set_size as u64 / 1024,
private_bytes_kb: c.pagefile_usage as u64 / 1024,
})
}
#[derive(Debug, Clone)]
pub struct StageMemRecord {
pub stage: &'static str,
pub sample: MemorySample,
}
thread_local! {
static TL_PROBE_BUFFER: std::cell::RefCell<Vec<StageMemRecord>> =
const { std::cell::RefCell::new(Vec::new()) };
static TL_PROBE_ENABLED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub fn tl_probe_begin() {
TL_PROBE_ENABLED.with(|c| c.set(true));
TL_PROBE_BUFFER.with(|b| b.borrow_mut().clear());
}
pub fn tl_probe_end() -> Vec<StageMemRecord> {
TL_PROBE_ENABLED.with(|c| c.set(false));
TL_PROBE_BUFFER.with(|b| std::mem::take(&mut *b.borrow_mut()))
}
pub fn tl_probe(stage: &'static str) {
TL_PROBE_ENABLED.with(|enabled| {
if enabled.get()
&& let Some(sample) = sample_memory()
{
TL_PROBE_BUFFER.with(|b| {
b.borrow_mut().push(StageMemRecord { stage, sample });
});
}
});
}
pub fn probe_stage(enabled: bool, stage: &'static str, buffer: &mut Vec<StageMemRecord>) {
if enabled && let Some(sample) = sample_memory() {
buffer.push(StageMemRecord { stage, sample });
}
}
fn counters_impl() -> Result<ProcessMemoryCounters, u32> {
let mut counters = ProcessMemoryCounters {
cb: std::mem::size_of::<ProcessMemoryCounters>() as u32,
..Default::default()
};
let ok = unsafe { GetProcessMemoryInfo(GetCurrentProcess(), &mut counters, counters.cb) };
if ok == 0 {
return Err(unsafe { GetLastError() });
}
Ok(counters)
}
pub const PEAK_MEMORY_SOURCE: &str = "windows:GetProcessMemoryInfo.PeakWorkingSetSize";
pub fn peak_memory_source() -> &'static str {
PEAK_MEMORY_SOURCE
}
pub fn peak_memory_failure_reason() -> Option<String> {
peak_working_set_impl()
.err()
.map(|code| format!("GetProcessMemoryInfo failed with Win32 error {code}"))
}
#[derive(Default)]
#[repr(C)]
struct ProcessMemoryCounters {
cb: u32,
page_fault_count: u32,
peak_working_set_size: usize,
working_set_size: usize,
quota_peak_paged_pool_usage: usize,
quota_paged_pool_usage: usize,
quota_peak_non_paged_pool_usage: usize,
quota_non_paged_pool_usage: usize,
pagefile_usage: usize,
peak_pagefile_usage: usize,
}
#[link(name = "psapi")]
unsafe extern "system" {
fn GetCurrentProcess() -> *mut core::ffi::c_void;
fn GetProcessMemoryInfo(
process: *mut core::ffi::c_void,
counters: *mut ProcessMemoryCounters,
size: u32,
) -> i32;
fn GetLastError() -> u32;
}
fn peak_working_set_impl() -> Result<u64, u32> {
let mut counters = ProcessMemoryCounters {
cb: std::mem::size_of::<ProcessMemoryCounters>() as u32,
..Default::default()
};
let ok = unsafe { GetProcessMemoryInfo(GetCurrentProcess(), &mut counters, counters.cb) };
if ok == 0 {
return Err(unsafe { GetLastError() });
}
Ok(counters.peak_working_set_size as u64)
}
#[cfg(test)]
mod tests {
use super::{
PEAK_MEMORY_SOURCE, peak_memory_failure_reason, peak_memory_source, peak_working_set_bytes,
sample_memory,
};
#[test]
fn peak_memory_is_always_available_on_windows() {
match peak_working_set_bytes() {
Some(bytes) => {
assert!(bytes > 0, "peak working set must be positive");
assert!(
peak_memory_failure_reason().is_none(),
"a successful sample must not report a failure reason"
);
}
None => {
let reason = peak_memory_failure_reason()
.expect("a failed sample must report a Win32 failure reason");
panic!("GetProcessMemoryInfo failed: {reason}");
}
}
assert_eq!(peak_memory_source(), PEAK_MEMORY_SOURCE);
}
#[test]
fn sample_memory_returns_valid_snapshot() {
let s = sample_memory().expect("sample_memory must succeed on Windows");
assert!(s.working_set_kb > 0);
assert!(s.peak_working_set_kb >= s.working_set_kb);
assert!(s.private_bytes_kb > 0);
}
}