use core::alloc::{GlobalAlloc, Layout};
use crate::memory::counters::Counters;
pub(crate) static COUNTERS: Counters = Counters::new();
pub struct TrackingAlloc<A> {
inner: A,
counters: &'static Counters,
}
impl<A> TrackingAlloc<A> {
pub const fn new(inner: A) -> Self {
Self {
inner,
counters: &COUNTERS,
}
}
#[cfg(test)]
pub(crate) const fn with_counters(inner: A, counters: &'static Counters) -> Self {
Self { inner, counters }
}
}
unsafe impl<A: GlobalAlloc> GlobalAlloc for TrackingAlloc<A> {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { self.inner.alloc(layout) };
if !ptr.is_null() {
self.counters.record_alloc(layout.size());
crate::memory::detail::record_alloc(layout.size());
}
ptr
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { self.inner.alloc_zeroed(layout) };
if !ptr.is_null() {
self.counters.record_alloc(layout.size());
crate::memory::detail::record_alloc(layout.size());
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { self.inner.dealloc(ptr, layout) };
self.counters.record_free(layout.size());
crate::memory::detail::record_free(layout.size());
}
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() {
self.counters.record_realloc(layout.size(), new_size);
crate::memory::detail::record_realloc(layout.size(), new_size);
}
new_ptr
}
}
#[macro_export]
macro_rules! install_global_allocator {
() => {
#[global_allocator]
static CN_GLOBAL_ALLOC: $crate::memory::TrackingAlloc<std::alloc::System> =
$crate::memory::TrackingAlloc::new(std::alloc::System);
};
}
#[cfg(test)]
mod tests {
use super::*;
use core::ptr;
use std::alloc::System;
const SIZE: usize = 4096;
const GROWN: usize = 64 * 1024;
const SHRUNK: usize = 1024;
fn layout(size: usize) -> Layout {
Layout::from_size_align(size, 8).expect("valid layout")
}
struct Failing;
unsafe impl GlobalAlloc for Failing {
unsafe fn alloc(&self, _layout: Layout) -> *mut u8 {
ptr::null_mut()
}
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
unreachable!("this allocator never hands out a block to free")
}
}
#[test]
fn tracking_alloc_forwards_and_counts() {
static BLOCK: Counters = Counters::new();
let alloc = TrackingAlloc::with_counters(System, &BLOCK);
let layout = layout(SIZE);
assert_eq!(BLOCK.snapshot(), None, "nothing has allocated yet");
let ptr = unsafe { alloc.alloc(layout) };
assert!(!ptr.is_null(), "system allocator returned null for 4 KiB");
unsafe { ptr.write_bytes(0xAB, SIZE) };
let during = BLOCK.snapshot().expect("the wrapper just allocated");
assert_eq!(during.live_bytes, SIZE as u64);
assert_eq!(during.alloc_count, 1);
assert_eq!(during.free_count, 0);
unsafe { alloc.dealloc(ptr, layout) };
let after = BLOCK.snapshot().expect("counters are live");
assert_eq!(after.live_bytes, 0);
assert_eq!(after.alloc_count, 1);
assert_eq!(after.free_count, 1);
assert_eq!(after.peak_bytes, SIZE as u64);
}
#[test]
fn alloc_zeroed_forwards_the_zeroing_and_counts() {
static BLOCK: Counters = Counters::new();
let alloc = TrackingAlloc::with_counters(System, &BLOCK);
let layout = layout(SIZE);
let ptr = unsafe { alloc.alloc_zeroed(layout) };
assert!(!ptr.is_null(), "system allocator returned null for 4 KiB");
let bytes = unsafe { core::slice::from_raw_parts(ptr, SIZE) };
assert!(bytes.iter().all(|&b| b == 0), "the block was not zeroed");
let during = BLOCK.snapshot().expect("the wrapper just allocated");
assert_eq!(during.live_bytes, SIZE as u64);
assert_eq!(during.alloc_count, 1);
unsafe { alloc.dealloc(ptr, layout) };
assert_eq!(BLOCK.snapshot().expect("counters are live").live_bytes, 0);
}
#[test]
fn realloc_counts_the_resize_only() {
static BLOCK: Counters = Counters::new();
let alloc = TrackingAlloc::with_counters(System, &BLOCK);
let ptr = unsafe { alloc.alloc(layout(SIZE)) };
assert!(!ptr.is_null(), "system allocator returned null for 4 KiB");
let ptr = unsafe { alloc.realloc(ptr, layout(SIZE), GROWN) };
assert!(!ptr.is_null(), "system allocator returned null for 64 KiB");
let grown = BLOCK.snapshot().expect("the wrapper just allocated");
assert_eq!(grown.live_bytes, GROWN as u64);
assert_eq!(grown.alloc_count, 1, "a resize is not an allocation");
assert_eq!(grown.free_count, 0, "a resize is not a free");
let ptr = unsafe { alloc.realloc(ptr, layout(GROWN), SHRUNK) };
assert!(!ptr.is_null(), "system allocator returned null for 1 KiB");
let shrunk = BLOCK.snapshot().expect("counters are live");
assert_eq!(shrunk.live_bytes, SHRUNK as u64);
assert_eq!(shrunk.peak_bytes, GROWN as u64);
unsafe { alloc.dealloc(ptr, layout(SHRUNK)) };
let after = BLOCK.snapshot().expect("counters are live");
assert_eq!(after.live_bytes, 0);
assert_eq!(after.alloc_count, 1);
assert_eq!(after.free_count, 1);
}
#[test]
fn a_failed_allocation_counts_nothing() {
static BLOCK: Counters = Counters::new();
let alloc = TrackingAlloc::with_counters(Failing, &BLOCK);
assert!(unsafe { alloc.alloc(layout(SIZE)) }.is_null());
assert!(unsafe { alloc.alloc_zeroed(layout(SIZE)) }.is_null());
assert_eq!(BLOCK.snapshot(), None, "a failed allocation was counted");
}
}