#[cfg(all(feature = "mem-probe", feature = "dhat-heap"))]
compile_error!(
"mem-probe and dhat-heap both install a #[global_allocator]; enable exactly one \
(mem-probe for the gate rows, dhat-heap for allocation-site attribution)"
);
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::thread;
use std::time::Duration;
pub const SAMPLE_INTERVAL: Duration = Duration::from_millis(200);
#[cfg(feature = "mem-probe")]
mod counting {
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicU64, Ordering};
pub(super) static LIVE: AtomicU64 = AtomicU64::new(0);
pub(super) static PEAK: AtomicU64 = AtomicU64::new(0);
pub(super) static TOTAL: AtomicU64 = AtomicU64::new(0);
fn record_alloc(bytes: u64) {
TOTAL.fetch_add(bytes, Ordering::Relaxed);
let live = LIVE.fetch_add(bytes, Ordering::Relaxed) + bytes;
PEAK.fetch_max(live, Ordering::Relaxed);
}
pub(super) struct CountingAlloc;
#[allow(unsafe_code)]
unsafe impl GlobalAlloc for CountingAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { System.alloc(layout) };
if !ptr.is_null() {
record_alloc(layout.size() as u64);
}
ptr
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { System.alloc_zeroed(layout) };
if !ptr.is_null() {
record_alloc(layout.size() as u64);
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
LIVE.fetch_sub(layout.size() as u64, Ordering::Relaxed);
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let new_ptr = unsafe { System.realloc(ptr, layout, new_size) };
if !new_ptr.is_null() {
if new_size >= layout.size() {
record_alloc((new_size - layout.size()) as u64);
} else {
LIVE.fetch_sub((layout.size() - new_size) as u64, Ordering::Relaxed);
}
}
new_ptr
}
}
}
#[cfg(feature = "mem-probe")]
#[global_allocator]
static GLOBAL: counting::CountingAlloc = counting::CountingAlloc;
#[cfg(feature = "dhat-heap")]
#[global_allocator]
static GLOBAL: dhat::Alloc = dhat::Alloc;
#[cfg(feature = "dhat-heap")]
#[must_use]
pub fn start_dhat_profiler() -> dhat::Profiler {
dhat::Profiler::new_heap()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HeapStats {
pub live_bytes: u64,
pub peak_bytes: u64,
pub total_alloc_bytes: u64,
}
#[must_use]
pub fn heap_stats() -> Option<HeapStats> {
#[cfg(feature = "mem-probe")]
{
Some(HeapStats {
live_bytes: counting::LIVE.load(Ordering::Relaxed),
peak_bytes: counting::PEAK.load(Ordering::Relaxed),
total_alloc_bytes: counting::TOTAL.load(Ordering::Relaxed),
})
}
#[cfg(not(feature = "mem-probe"))]
{
None
}
}
pub fn reset_heap_peak() {
#[cfg(feature = "mem-probe")]
{
let live = counting::LIVE.load(Ordering::Relaxed);
counting::PEAK.store(live, Ordering::Relaxed);
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct RssStats {
pub vm_rss_kb: u64,
pub vm_hwm_kb: u64,
pub rss_anon_kb: u64,
}
#[must_use]
pub fn rss() -> Option<RssStats> {
#[cfg(target_os = "linux")]
{
let status = std::fs::read_to_string("/proc/self/status").ok()?;
let field = |name: &str| -> u64 {
status
.lines()
.find_map(|line| {
line.strip_prefix(name)?
.split_whitespace()
.next()?
.parse::<u64>()
.ok()
})
.unwrap_or(0)
};
Some(RssStats {
vm_rss_kb: field("VmRSS:"),
vm_hwm_kb: field("VmHWM:"),
rss_anon_kb: field("RssAnon:"),
})
}
#[cfg(not(target_os = "linux"))]
{
None
}
}
#[must_use]
pub fn current_rss_kb() -> Option<u64> {
rss().map(|r| r.vm_rss_kb)
}
pub fn reset_peak_rss() -> bool {
#[cfg(target_os = "linux")]
{
std::fs::write("/proc/self/clear_refs", "5").is_ok()
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
#[cfg(unix)]
#[must_use]
pub fn ru_maxrss_kb() -> Option<u64> {
#[allow(unsafe_code)]
let usage = {
let mut usage = std::mem::MaybeUninit::<libc::rusage>::uninit();
if unsafe { libc::getrusage(libc::RUSAGE_SELF, usage.as_mut_ptr()) } != 0 {
return None;
}
unsafe { usage.assume_init() }
};
let raw = u64::try_from(usage.ru_maxrss).ok()?;
Some(if cfg!(target_os = "macos") {
raw / 1024
} else {
raw
})
}
#[cfg(not(unix))]
#[must_use]
pub fn ru_maxrss_kb() -> Option<u64> {
None
}
pub struct RssSampler {
peak_kb: Arc<AtomicU64>,
stop: Arc<AtomicBool>,
handle: Option<thread::JoinHandle<()>>,
}
impl RssSampler {
#[must_use]
pub fn start(interval: Duration) -> Self {
let peak_kb = Arc::new(AtomicU64::new(0));
let stop = Arc::new(AtomicBool::new(false));
let handle = {
let (peak, stop) = (Arc::clone(&peak_kb), Arc::clone(&stop));
thread::spawn(move || {
while !stop.load(Ordering::Relaxed) {
if let Some(kb) = current_rss_kb() {
peak.fetch_max(kb, Ordering::Relaxed);
}
thread::sleep(interval);
}
})
};
Self {
peak_kb,
stop,
handle: Some(handle),
}
}
pub fn finish(mut self) -> u64 {
self.stop.store(true, Ordering::Relaxed);
if let Some(handle) = self.handle.take() {
handle.join().ok();
}
self.peak_kb.load(Ordering::Relaxed)
}
}
impl Drop for RssSampler {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(handle) = self.handle.take() {
handle.join().ok();
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use super::*;
#[test]
fn rss_probe_matches_platform_support() {
if cfg!(target_os = "linux") {
let stats = rss().expect("/proc/self/status readable on linux");
assert!(stats.vm_rss_kb > 0);
assert!(stats.vm_hwm_kb >= stats.vm_rss_kb);
} else {
assert!(rss().is_none());
}
}
#[cfg(feature = "mem-probe")]
#[test]
fn counting_allocator_tracks_live_and_peak() {
let before = heap_stats().expect("mem-probe enabled");
let big: Vec<u8> = vec![7; 8 << 20];
let during = heap_stats().expect("mem-probe enabled");
assert!(during.live_bytes >= before.live_bytes + (8 << 20));
drop(big);
let after = heap_stats().expect("mem-probe enabled");
assert!(after.live_bytes < during.live_bytes);
assert!(after.peak_bytes >= during.live_bytes);
}
}