use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
use std::sync::atomic::{AtomicI64, Ordering};
static GLOBAL_LIVE: AtomicI64 = AtomicI64::new(0);
thread_local! {
static THREAD_ALLOCATED: Cell<u64> = const { Cell::new(0) };
static THREAD_LIVE: Cell<i64> = const { Cell::new(0) };
static THREAD_ALLOC_COUNT: Cell<u64> = const { Cell::new(0) };
}
pub struct TrackingAllocator<A = System> {
inner: A,
}
impl TrackingAllocator<System> {
pub const fn new() -> Self {
Self { inner: System }
}
}
impl Default for TrackingAllocator<System> {
fn default() -> Self {
Self::new()
}
}
impl<A> TrackingAllocator<A> {
pub const fn wrapping(inner: A) -> Self {
Self { inner }
}
}
unsafe impl<A: GlobalAlloc> GlobalAlloc for TrackingAllocator<A> {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
unsafe {
THREAD_ALLOCATED.with(|c| c.set(c.get() + layout.size() as u64));
THREAD_LIVE.with(|c| c.set(c.get() + layout.size() as i64));
THREAD_ALLOC_COUNT.with(|c| c.set(c.get() + 1));
GLOBAL_LIVE.fetch_add(layout.size() as i64, Ordering::Relaxed);
self.inner.alloc(layout)
}
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe {
THREAD_LIVE.with(|c| c.set(c.get() - layout.size() as i64));
GLOBAL_LIVE.fetch_sub(layout.size() as i64, Ordering::Relaxed);
self.inner.dealloc(ptr, layout)
}
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
unsafe {
THREAD_ALLOCATED.with(|c| c.set(c.get() + layout.size() as u64));
THREAD_LIVE.with(|c| c.set(c.get() + layout.size() as i64));
THREAD_ALLOC_COUNT.with(|c| c.set(c.get() + 1));
GLOBAL_LIVE.fetch_add(layout.size() as i64, Ordering::Relaxed);
self.inner.alloc_zeroed(layout)
}
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
unsafe {
if new_size > layout.size() {
let grown = (new_size - layout.size()) as u64;
THREAD_ALLOCATED.with(|c| c.set(c.get() + grown));
THREAD_LIVE.with(|c| c.set(c.get() + grown as i64));
THREAD_ALLOC_COUNT.with(|c| c.set(c.get() + 1));
GLOBAL_LIVE.fetch_add(grown as i64, Ordering::Relaxed);
} else if new_size < layout.size() {
let shrunk = (layout.size() - new_size) as i64;
THREAD_LIVE.with(|c| c.set(c.get() - shrunk));
GLOBAL_LIVE.fetch_sub(shrunk, Ordering::Relaxed);
}
self.inner.realloc(ptr, layout, new_size)
}
}
}
pub fn thread_allocated_bytes() -> u64 {
THREAD_ALLOCATED.with(Cell::get)
}
pub fn thread_live_bytes() -> i64 {
THREAD_LIVE.with(Cell::get)
}
pub fn global_live_bytes() -> u64 {
GLOBAL_LIVE.load(Ordering::Relaxed).max(0) as u64
}
pub fn thread_alloc_count() -> u64 {
THREAD_ALLOC_COUNT.with(Cell::get)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counter_is_monotonic_and_thread_local() {
let before = thread_allocated_bytes();
THREAD_ALLOCATED.with(|c| c.set(c.get() + 128));
let after = thread_allocated_bytes();
assert_eq!(after, before + 128);
let handle = std::thread::spawn(thread_allocated_bytes);
let other_thread = handle.join().unwrap();
assert_eq!(
other_thread, 0,
"a fresh thread starts its own counter at 0"
);
}
#[test]
fn live_and_alloc_count_counters_are_independent_and_thread_local() {
let live_before = thread_live_bytes();
let count_before = thread_alloc_count();
THREAD_LIVE.with(|c| c.set(c.get() + 256));
THREAD_ALLOC_COUNT.with(|c| c.set(c.get() + 1));
assert_eq!(thread_live_bytes(), live_before + 256);
assert_eq!(thread_alloc_count(), count_before + 1);
THREAD_LIVE.with(|c| c.set(c.get() - 300));
assert!(
thread_live_bytes() < live_before,
"net live bytes can go negative relative to an earlier snapshot"
);
let handle = std::thread::spawn(|| (thread_live_bytes(), thread_alloc_count()));
let (other_live, other_count) = handle.join().unwrap();
assert_eq!(
other_live, 0,
"a fresh thread starts its own live counter at 0"
);
assert_eq!(
other_count, 0,
"a fresh thread starts its own alloc-count counter at 0"
);
}
}