#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs)]
#![warn(rust_2018_idioms)]
#[cfg(feature = "backtraces")]
mod backtrace;
#[cfg(feature = "backtraces")]
pub use backtrace::CallSiteStats;
use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
use std::ptr;
use std::sync::atomic::{AtomicPtr, AtomicU64, Ordering};
static GLOBAL_HANDLE: AtomicPtr<ModAlloc> = AtomicPtr::new(ptr::null_mut());
thread_local! {
static IN_ALLOC: Cell<bool> = const { Cell::new(false) };
}
struct ReentryGuard;
impl ReentryGuard {
fn enter() -> Option<Self> {
IN_ALLOC
.try_with(|flag| {
if flag.get() {
None
} else {
flag.set(true);
Some(ReentryGuard)
}
})
.ok()
.flatten()
}
}
impl Drop for ReentryGuard {
fn drop(&mut self) {
let _ = IN_ALLOC.try_with(|flag| flag.set(false));
}
}
pub struct ModAlloc {
alloc_count: AtomicU64,
total_bytes: AtomicU64,
peak_bytes: AtomicU64,
current_bytes: AtomicU64,
}
impl ModAlloc {
pub const fn new() -> Self {
Self {
alloc_count: AtomicU64::new(0),
total_bytes: AtomicU64::new(0),
peak_bytes: AtomicU64::new(0),
current_bytes: AtomicU64::new(0),
}
}
pub fn snapshot(&self) -> AllocStats {
AllocStats {
alloc_count: self.alloc_count.load(Ordering::Relaxed),
total_bytes: self.total_bytes.load(Ordering::Relaxed),
peak_bytes: self.peak_bytes.load(Ordering::Relaxed),
current_bytes: self.current_bytes.load(Ordering::Relaxed),
}
}
pub fn reset(&self) {
self.alloc_count.store(0, Ordering::Relaxed);
self.total_bytes.store(0, Ordering::Relaxed);
self.peak_bytes.store(0, Ordering::Relaxed);
self.current_bytes.store(0, Ordering::Relaxed);
}
#[inline]
fn record_alloc(&self, size: u64) {
self.alloc_count.fetch_add(1, Ordering::Relaxed);
self.total_bytes.fetch_add(size, Ordering::Relaxed);
let new_current = self.current_bytes.fetch_add(size, Ordering::Relaxed) + size;
self.peak_bytes.fetch_max(new_current, Ordering::Relaxed);
}
#[inline]
fn record_dealloc(&self, size: u64) {
self.current_bytes.fetch_sub(size, Ordering::Relaxed);
}
#[inline]
fn record_realloc(&self, old_size: u64, new_size: u64) {
self.alloc_count.fetch_add(1, Ordering::Relaxed);
if new_size > old_size {
let delta = new_size - old_size;
self.total_bytes.fetch_add(delta, Ordering::Relaxed);
let new_current = self.current_bytes.fetch_add(delta, Ordering::Relaxed) + delta;
self.peak_bytes.fetch_max(new_current, Ordering::Relaxed);
} else if new_size < old_size {
self.current_bytes
.fetch_sub(old_size - new_size, Ordering::Relaxed);
}
}
#[inline]
fn register_self(&self) {
if GLOBAL_HANDLE.load(Ordering::Relaxed).is_null() {
let _ = GLOBAL_HANDLE.compare_exchange(
ptr::null_mut(),
self as *const ModAlloc as *mut ModAlloc,
Ordering::Release,
Ordering::Relaxed,
);
}
}
#[cfg(feature = "backtraces")]
pub fn call_sites(&self) -> Vec<CallSiteStats> {
backtrace::call_sites_report()
}
}
impl Default for ModAlloc {
fn default() -> Self {
Self::new()
}
}
unsafe impl GlobalAlloc for ModAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { System.alloc(layout) };
if !ptr.is_null() {
if let Some(_g) = ReentryGuard::enter() {
let size = layout.size() as u64;
self.record_alloc(size);
self.register_self();
#[cfg(feature = "backtraces")]
backtrace::record_event(size);
}
}
ptr
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { System.alloc_zeroed(layout) };
if !ptr.is_null() {
if let Some(_g) = ReentryGuard::enter() {
let size = layout.size() as u64;
self.record_alloc(size);
self.register_self();
#[cfg(feature = "backtraces")]
backtrace::record_event(size);
}
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) };
if let Some(_g) = ReentryGuard::enter() {
self.record_dealloc(layout.size() as u64);
}
}
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 let Some(_g) = ReentryGuard::enter() {
self.record_realloc(layout.size() as u64, new_size as u64);
self.register_self();
#[cfg(feature = "backtraces")]
backtrace::record_event(new_size as u64);
}
}
new_ptr
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AllocStats {
pub alloc_count: u64,
pub total_bytes: u64,
pub peak_bytes: u64,
pub current_bytes: u64,
}
pub struct Profiler {
baseline: AllocStats,
}
impl Profiler {
pub fn start() -> Self {
Self {
baseline: current_snapshot_or_zeros(),
}
}
pub fn stop(self) -> AllocStats {
let now = current_snapshot_or_zeros();
AllocStats {
alloc_count: now.alloc_count.saturating_sub(self.baseline.alloc_count),
total_bytes: now.total_bytes.saturating_sub(self.baseline.total_bytes),
current_bytes: now
.current_bytes
.saturating_sub(self.baseline.current_bytes),
peak_bytes: now.peak_bytes,
}
}
}
fn current_snapshot_or_zeros() -> AllocStats {
let p = GLOBAL_HANDLE.load(Ordering::Acquire);
if p.is_null() {
AllocStats {
alloc_count: 0,
total_bytes: 0,
peak_bytes: 0,
current_bytes: 0,
}
} else {
unsafe { (*p).snapshot() }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn allocator_constructs() {
let _ = ModAlloc::new();
}
#[test]
fn snapshot_returns_zeros_initially() {
let a = ModAlloc::new();
let s = a.snapshot();
assert_eq!(s.alloc_count, 0);
assert_eq!(s.total_bytes, 0);
assert_eq!(s.peak_bytes, 0);
assert_eq!(s.current_bytes, 0);
}
#[test]
fn reset_works() {
let a = ModAlloc::new();
a.reset();
let s = a.snapshot();
assert_eq!(s.alloc_count, 0);
}
#[test]
fn record_alloc_updates_counters() {
let a = ModAlloc::new();
a.record_alloc(128);
a.record_alloc(256);
let s = a.snapshot();
assert_eq!(s.alloc_count, 2);
assert_eq!(s.total_bytes, 384);
assert_eq!(s.current_bytes, 384);
assert_eq!(s.peak_bytes, 384);
}
#[test]
fn record_dealloc_decreases_current_only() {
let a = ModAlloc::new();
a.record_alloc(1000);
a.record_dealloc(400);
let s = a.snapshot();
assert_eq!(s.alloc_count, 1);
assert_eq!(s.total_bytes, 1000);
assert_eq!(s.current_bytes, 600);
assert_eq!(s.peak_bytes, 1000);
}
#[test]
fn record_realloc_growth_updates_total_and_peak() {
let a = ModAlloc::new();
a.record_alloc(100);
a.record_realloc(100, 250);
let s = a.snapshot();
assert_eq!(s.alloc_count, 2);
assert_eq!(s.total_bytes, 250);
assert_eq!(s.current_bytes, 250);
assert_eq!(s.peak_bytes, 250);
}
#[test]
fn record_realloc_shrink_only_adjusts_current() {
let a = ModAlloc::new();
a.record_alloc(500);
a.record_realloc(500, 200);
let s = a.snapshot();
assert_eq!(s.alloc_count, 2);
assert_eq!(s.total_bytes, 500);
assert_eq!(s.current_bytes, 200);
assert_eq!(s.peak_bytes, 500);
}
#[test]
fn peak_holds_high_water_mark() {
let a = ModAlloc::new();
a.record_alloc(1000);
a.record_dealloc(1000);
a.record_alloc(500);
let s = a.snapshot();
assert_eq!(s.peak_bytes, 1000);
assert_eq!(s.current_bytes, 500);
}
#[test]
fn reentry_guard_blocks_nested_entry() {
let outer = ReentryGuard::enter();
assert!(outer.is_some());
let inner = ReentryGuard::enter();
assert!(inner.is_none(), "nested entry must be denied");
drop(outer);
let after = ReentryGuard::enter();
assert!(after.is_some(), "entry must be allowed after outer drops");
}
#[test]
fn profiler_start_stop_with_no_handle() {
let p = Profiler::start();
let s = p.stop();
assert_eq!(s.alloc_count, 0);
}
}