#![cfg(not(target_arch = "wasm32"))]
use std::{
alloc::{GlobalAlloc, Layout, System},
cell::Cell,
time::{Duration, Instant},
};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Measurement {
pub allocations: usize,
pub reallocations: usize,
pub net_bytes: i128,
pub peak_bytes: usize,
pub elapsed: Duration,
}
#[derive(Clone, Copy, Default)]
struct State {
active: bool,
counts: Measurement,
}
thread_local! { static STATE: Cell<State> = const { Cell::new(State {
active: false,
counts: Measurement { allocations: 0, reallocations: 0, net_bytes: 0, peak_bytes: 0, elapsed: Duration::ZERO },
}) }; }
fn account(old: usize, new: usize, allocation: bool, reallocation: bool) {
let _ = STATE.try_with(|cell| {
let mut state = cell.get();
if state.active {
state.counts.net_bytes += new as i128 - old as i128;
state.counts.peak_bytes = state
.counts
.peak_bytes
.max(usize::try_from(state.counts.net_bytes).unwrap_or(0));
state.counts.allocations += usize::from(allocation);
state.counts.reallocations += usize::from(reallocation);
cell.set(state);
}
});
}
pub struct MeasuredAllocator<A = System> {
allocator: A,
}
impl<A> MeasuredAllocator<A> {
#[must_use]
pub const fn new(allocator: A) -> Self {
Self { allocator }
}
}
#[expect(
unsafe_code,
reason = "forward the GlobalAlloc contract to the caller-selected allocator"
)]
unsafe impl<A: GlobalAlloc> GlobalAlloc for MeasuredAllocator<A> {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let pointer = unsafe { self.allocator.alloc(layout) };
if !pointer.is_null() {
account(0, layout.size(), true, false);
}
pointer
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
let pointer = unsafe { self.allocator.alloc_zeroed(layout) };
if !pointer.is_null() {
account(0, layout.size(), true, false);
}
pointer
}
unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
account(layout.size(), 0, false, false);
unsafe { self.allocator.dealloc(pointer, layout) };
}
unsafe fn realloc(&self, pointer: *mut u8, layout: Layout, size: usize) -> *mut u8 {
let result = unsafe { self.allocator.realloc(pointer, layout, size) };
if !result.is_null() {
account(layout.size(), size, false, true);
}
result
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AlreadyMeasuring;
impl std::fmt::Display for AlreadyMeasuring {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("allocation measurement already active on this thread")
}
}
impl std::error::Error for AlreadyMeasuring {}
struct Guard;
impl Drop for Guard {
fn drop(&mut self) {
STATE.with(|cell| cell.set(State::default()));
}
}
pub fn measure(
iterations: usize,
mut operation: impl FnMut(),
) -> Result<Measurement, AlreadyMeasuring> {
STATE.with(|cell| {
if cell.get().active {
return Err(AlreadyMeasuring);
}
cell.set(State {
active: true,
..State::default()
});
Ok(())
})?;
let guard = Guard;
let started = Instant::now();
for _ in 0..iterations {
operation();
}
let elapsed = started.elapsed();
let mut counts = STATE.with(|cell| cell.get().counts);
drop(guard);
counts.elapsed = elapsed;
Ok(counts)
}