use std::{
fs,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
thread::{self, JoinHandle},
time::Duration,
};
pub fn purge_allocator() {
#[cfg(all(not(miri), feature = "mimalloc"))]
{
unsafe { libmimalloc_sys::mi_collect(true) };
}
}
const DEFAULT_INTERVAL: Duration = Duration::from_millis(50);
const KIB_TO_BYTES: u64 = 1024;
const RSS_MEDIAN_PERCENTILE: usize = 50;
const RSS_P90_PERCENTILE: usize = 90;
const PERCENT_SCALE: f64 = 100.0;
const PROC_SELF_STATUS: &str = "/proc/self/status";
const PROC_SELF_SMAPS_ROLLUP: &str = "/proc/self/smaps_rollup";
pub fn current_rss_bytes() -> Option<u64> {
let s = fs::read_to_string(PROC_SELF_STATUS).ok()?;
for line in s.lines() {
if let Some(rest) = line.strip_prefix("VmRSS:") {
let kb: u64 = rest.split_whitespace().next()?.parse().ok()?;
return Some(kb * KIB_TO_BYTES);
}
}
None
}
pub fn current_anon_rss_bytes() -> Option<u64> {
purge_allocator();
anon_rss_bytes_fast()
}
fn anon_rss_bytes_fast() -> Option<u64> {
let rollup = fs::read_to_string(PROC_SELF_SMAPS_ROLLUP).ok()?;
rollup
.lines()
.find(|l| l.starts_with("Anonymous:"))
.and_then(|l| l.split_whitespace().nth(1))
.and_then(|v| v.parse::<u64>().ok())
.map(|kb| kb * KIB_TO_BYTES)
}
pub struct PeakSampler {
stop: Arc<AtomicBool>,
handle: Option<JoinHandle<Vec<(u64, u64)>>>,
initial: (u64, u64),
}
#[derive(Debug, Clone, Copy)]
pub struct RssStats {
pub peak_rss_bytes: u64,
pub median_rss_bytes: u64,
pub p90_rss_bytes: u64,
pub peak_anon_rss_bytes: u64,
pub peak_file_rss_bytes: u64,
}
impl RssStats {
fn from_samples(mut samples: Vec<(u64, u64)>) -> Self {
if samples.is_empty() {
samples.push((
current_rss_bytes().unwrap_or(0),
anon_rss_bytes_fast().unwrap_or(0),
));
}
let peak_anon = samples.iter().map(|(_, a)| *a).max().unwrap_or(0);
let peak_file = samples
.iter()
.map(|(t, a)| t.saturating_sub(*a))
.max()
.unwrap_or(0);
let mut totals: Vec<u64> = samples.iter().map(|(t, _)| *t).collect();
totals.sort_unstable();
Self {
peak_rss_bytes: *totals.last().expect("rss samples is non-empty"),
median_rss_bytes: percentile_nearest_rank(&totals, RSS_MEDIAN_PERCENTILE),
p90_rss_bytes: percentile_nearest_rank(&totals, RSS_P90_PERCENTILE),
peak_anon_rss_bytes: peak_anon,
peak_file_rss_bytes: peak_file,
}
}
}
fn percentile_nearest_rank(sorted: &[u64], percentile: usize) -> u64 {
debug_assert!(!sorted.is_empty());
let rank = ((percentile as f64 / PERCENT_SCALE) * sorted.len() as f64).ceil() as usize;
sorted[rank.saturating_sub(1).min(sorted.len() - 1)]
}
impl PeakSampler {
pub fn start_default() -> Self {
Self::start(DEFAULT_INTERVAL)
}
pub fn start(interval: Duration) -> Self {
purge_allocator();
let stop = Arc::new(AtomicBool::new(false));
let initial = (
current_rss_bytes().unwrap_or(0),
anon_rss_bytes_fast().unwrap_or(0),
);
let stop_t = Arc::clone(&stop);
let handle = thread::Builder::new()
.name("rss-sampler".into())
.spawn(move || {
let mut samples = vec![initial];
while !stop_t.load(Ordering::Acquire) {
if let Some(rss) = current_rss_bytes() {
samples.push((rss, anon_rss_bytes_fast().unwrap_or(0)));
}
thread::park_timeout(interval);
}
if let Some(rss) = current_rss_bytes() {
samples.push((rss, anon_rss_bytes_fast().unwrap_or(0)));
}
samples
})
.ok();
Self {
stop,
handle,
initial,
}
}
pub fn stop(self) -> u64 {
self.stop_stats().peak_rss_bytes
}
pub fn stop_stats(mut self) -> RssStats {
self.stop.store(true, Ordering::Release);
if let Some(handle) = self.handle.as_ref() {
handle.thread().unpark();
}
let samples = self
.handle
.take()
.and_then(|h| h.join().ok())
.unwrap_or_else(|| vec![self.initial]);
RssStats::from_samples(samples)
}
}
pub fn settled_rss_breakdown() -> Option<(u64, u64, u64, u64)> {
purge_allocator();
let rollup = fs::read_to_string(PROC_SELF_SMAPS_ROLLUP).ok()?;
let kb = |key: &str| -> u64 {
rollup
.lines()
.find(|l| l.starts_with(key))
.and_then(|l| l.split_whitespace().nth(1))
.and_then(|v| v.parse().ok())
.unwrap_or(0)
};
let rss = kb("Rss:") * KIB_TO_BYTES;
let anon = kb("Anonymous:") * KIB_TO_BYTES;
let shmem = kb("Shmem:") * KIB_TO_BYTES;
let file_backed = rss.saturating_sub(anon).saturating_sub(shmem);
Some((rss, anon, file_backed, shmem))
}
pub fn log_rss_breakdown(label: &str) {
let Some((rss, anon, file_backed, shmem)) = settled_rss_breakdown() else {
return;
};
eprintln!(
"[rss-breakdown] {label}: rss={} anonymous={} file_backed={} shmem={}",
fmt_bytes(rss),
fmt_bytes(anon),
fmt_bytes(file_backed),
fmt_bytes(shmem),
);
}
pub fn fmt_bytes(b: u64) -> String {
const KIB: u64 = 1 << 10;
const MIB: u64 = 1 << 20;
const GIB: u64 = 1 << 30;
if b >= GIB {
format!("{:.2} GiB", b as f64 / GIB as f64)
} else if b >= MIB {
format!("{:.2} MiB", b as f64 / MIB as f64)
} else if b >= KIB {
format!("{:.1} KiB", b as f64 / KIB as f64)
} else {
format!("{b} B")
}
}
#[cfg(test)]
mod tests {
use std::hint::black_box;
use super::*;
const TEST_SAMPLER_INTERVAL_MS: u64 = 1_000;
const TEST_ALLOC_SIZE_BYTES: usize = 32 * 1024 * 1024;
const TEST_PAGE_STRIDE_BYTES: usize = 4096;
const TEST_MIN_RSS_GROWTH_BYTES: u64 = 16 * 1024 * 1024;
const TEST_GROWTH_SAMPLER_INTERVAL: Duration = Duration::from_millis(5);
const TEST_GROWTH_HOLD: Duration = Duration::from_millis(50);
const TEST_GROWTH_ATTEMPTS: usize = 5;
#[test]
fn current_rss_is_nonzero_on_linux() {
if let Some(rss) = current_rss_bytes() {
assert!(rss > 0, "VmRSS reported as zero — parse error?");
}
}
#[test]
fn sampler_returns_at_least_start_rss() {
purge_allocator();
let before = current_rss_bytes();
let s = PeakSampler::start(Duration::from_millis(TEST_SAMPLER_INTERVAL_MS));
let after_start = current_rss_bytes();
let peak = s.stop();
if let (Some(before), Some(after)) = (before, after_start) {
let floor = before.min(after);
assert!(peak >= floor, "peak {peak} < floor {floor} — seed missing");
}
}
fn fault_alloc_and_sample_peak() -> Option<(u64, u64)> {
purge_allocator();
let baseline = current_rss_bytes()?;
let s = PeakSampler::start(TEST_GROWTH_SAMPLER_INTERVAL);
let mut v: Vec<u8> = vec![0; TEST_ALLOC_SIZE_BYTES];
for chunk in v.chunks_mut(TEST_PAGE_STRIDE_BYTES) {
chunk[0] = 1;
}
thread::sleep(TEST_GROWTH_HOLD);
black_box(&v);
Some((baseline, s.stop()))
}
#[test]
fn sampler_observes_allocation_growth() {
let mut last = (0, 0);
for _ in 0..TEST_GROWTH_ATTEMPTS {
let Some((baseline, peak)) = fault_alloc_and_sample_peak() else {
return;
};
if peak >= baseline + TEST_MIN_RSS_GROWTH_BYTES {
return;
}
last = (baseline, peak);
}
let (baseline, peak) = last;
panic!(
"sampler missed the 32 MiB faulted allocation in \
{TEST_GROWTH_ATTEMPTS} attempts: last baseline={baseline}, \
last peak={peak}"
);
}
#[test]
fn rss_stats_use_nearest_rank_percentiles() {
let stats = RssStats::from_samples(vec![(50, 5), (10, 1), (40, 30), (20, 2), (30, 3)]);
assert_eq!(stats.peak_rss_bytes, 50);
assert_eq!(stats.median_rss_bytes, 30);
assert_eq!(stats.p90_rss_bytes, 50);
assert_eq!(stats.peak_anon_rss_bytes, 30);
assert_eq!(stats.peak_file_rss_bytes, 45);
}
}