use core::alloc::{GlobalAlloc, Layout};
use core::cell::Cell;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct AllocationDelta {
pub allocations: usize,
pub deallocations: usize,
pub reallocations: usize,
pub bytes_allocated: usize,
pub bytes_deallocated: usize,
}
impl AllocationDelta {
#[must_use]
pub const fn bytes_retained(&self) -> isize {
self.bytes_allocated
.wrapping_sub(self.bytes_deallocated)
.cast_signed()
}
const fn since(&self, earlier: &Self) -> Self {
Self {
allocations: self.allocations.wrapping_sub(earlier.allocations),
deallocations: self.deallocations.wrapping_sub(earlier.deallocations),
reallocations: self.reallocations.wrapping_sub(earlier.reallocations),
bytes_allocated: self.bytes_allocated.wrapping_sub(earlier.bytes_allocated),
bytes_deallocated: self
.bytes_deallocated
.wrapping_sub(earlier.bytes_deallocated),
}
}
}
struct Counters {
allocations: Cell<usize>,
deallocations: Cell<usize>,
reallocations: Cell<usize>,
bytes_allocated: Cell<usize>,
bytes_deallocated: Cell<usize>,
open_windows: Cell<usize>,
}
impl Counters {
const fn new() -> Self {
Self {
allocations: Cell::new(0),
deallocations: Cell::new(0),
reallocations: Cell::new(0),
bytes_allocated: Cell::new(0),
bytes_deallocated: Cell::new(0),
open_windows: Cell::new(0),
}
}
fn totals(&self) -> AllocationDelta {
AllocationDelta {
allocations: self.allocations.get(),
deallocations: self.deallocations.get(),
reallocations: self.reallocations.get(),
bytes_allocated: self.bytes_allocated.get(),
bytes_deallocated: self.bytes_deallocated.get(),
}
}
}
std::thread_local! {
static COUNTERS: Counters = const { Counters::new() };
}
fn bump(cell: &Cell<usize>, by: usize) {
cell.set(cell.get().wrapping_add(by));
}
fn record_alloc(size: usize) {
COUNTERS.with(|counters| {
if counters.open_windows.get() == 0 {
return;
}
bump(&counters.allocations, 1);
bump(&counters.bytes_allocated, size);
});
}
fn record_dealloc(size: usize) {
COUNTERS.with(|counters| {
if counters.open_windows.get() == 0 {
return;
}
bump(&counters.deallocations, 1);
bump(&counters.bytes_deallocated, size);
});
}
fn record_realloc(old_size: usize, new_size: usize) {
COUNTERS.with(|counters| {
if counters.open_windows.get() == 0 {
return;
}
bump(&counters.reallocations, 1);
if new_size >= old_size {
bump(&counters.bytes_allocated, new_size - old_size);
} else {
bump(&counters.bytes_deallocated, old_size - new_size);
}
});
}
struct OpenWindow;
impl OpenWindow {
fn enter() -> Self {
COUNTERS.with(|counters| bump(&counters.open_windows, 1));
Self
}
}
impl Drop for OpenWindow {
fn drop(&mut self) {
COUNTERS.with(|counters| {
counters
.open_windows
.set(counters.open_windows.get().saturating_sub(1));
});
}
}
#[derive(Debug, Default)]
pub struct CountingAllocator<A> {
inner: A,
}
impl<A> CountingAllocator<A> {
#[must_use]
pub const fn new(inner: A) -> Self {
Self { inner }
}
}
unsafe impl<A: GlobalAlloc> GlobalAlloc for CountingAllocator<A> {
#[inline]
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { self.inner.alloc(layout) };
if !ptr.is_null() {
record_alloc(layout.size());
}
ptr
}
#[inline]
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { self.inner.dealloc(ptr, layout) };
record_dealloc(layout.size());
}
#[inline]
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { self.inner.alloc_zeroed(layout) };
if !ptr.is_null() {
record_alloc(layout.size());
}
ptr
}
#[inline]
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let new_ptr = unsafe { self.inner.realloc(ptr, layout, new_size) };
if !new_ptr.is_null() {
record_realloc(layout.size(), new_size);
}
new_ptr
}
}
pub fn measure<R>(body: impl FnOnce() -> R) -> (R, AllocationDelta) {
let _window = OpenWindow::enter();
let before = COUNTERS.with(Counters::totals);
let value = body();
let after = COUNTERS.with(Counters::totals);
(value, after.since(&before))
}
#[cfg(test)]
mod tests {
use super::{AllocationDelta, COUNTERS, Counters, measure, record_alloc};
fn totals() -> AllocationDelta {
COUNTERS.with(Counters::totals)
}
#[test]
fn recording_is_inert_outside_a_window() {
let before = totals();
record_alloc(24);
assert_eq!(totals(), before);
let ((), delta) = measure(|| record_alloc(24));
assert_eq!(
delta,
AllocationDelta {
allocations: 1,
bytes_allocated: 24,
..AllocationDelta::default()
}
);
record_alloc(24);
assert_eq!(
totals(),
AllocationDelta {
allocations: before.allocations + 1,
bytes_allocated: before.bytes_allocated + 24,
..before
}
);
}
#[test]
fn closing_an_inner_window_keeps_the_outer_one_recording() {
let ((), outer) = measure(|| {
let ((), inner) = measure(|| record_alloc(8));
assert_eq!(inner.allocations, 1);
record_alloc(8);
});
assert_eq!(outer.allocations, 2);
assert_eq!(outer.bytes_allocated, 16);
}
#[test]
fn a_panicking_body_closes_its_window() {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let ((), _) = measure(|| -> () { panic!("invariant: test body") });
}));
let Err(payload) = outcome else {
panic!("invariant: the panicking body unwinds out of measure");
};
assert_eq!(
payload.downcast_ref::<&str>(),
Some(&"invariant: test body")
);
let before = totals();
record_alloc(24);
assert_eq!(totals(), before);
}
}