#![allow(unsafe_code)]
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
static ALLOC_COUNT: AtomicUsize = AtomicUsize::new(0);
static DEALLOC_COUNT: AtomicUsize = AtomicUsize::new(0);
static ALLOC_BYTES: AtomicUsize = AtomicUsize::new(0);
static DEALLOC_BYTES: AtomicUsize = AtomicUsize::new(0);
pub struct AllocCounter<A: GlobalAlloc + Send + Sync + Default = System> {
_inner: A,
}
impl<A: GlobalAlloc + Send + Sync + Default> AllocCounter<A> {
pub fn new() -> Self {
Self {
_inner: A::default(),
}
}
}
impl AllocCounter<System> {
pub fn count() -> usize {
ALLOC_COUNT.load(Ordering::Relaxed)
}
pub fn dealloc_count() -> usize {
DEALLOC_COUNT.load(Ordering::Relaxed)
}
pub fn alloc_bytes() -> usize {
ALLOC_BYTES.load(Ordering::Relaxed)
}
pub fn dealloc_bytes() -> usize {
DEALLOC_BYTES.load(Ordering::Relaxed)
}
pub fn net_count() -> isize {
let alloc = ALLOC_COUNT.load(Ordering::Relaxed) as isize;
let dealloc = DEALLOC_COUNT.load(Ordering::Relaxed) as isize;
alloc - dealloc
}
pub fn net_bytes() -> isize {
let alloc = ALLOC_BYTES.load(Ordering::Relaxed) as isize;
let dealloc = DEALLOC_BYTES.load(Ordering::Relaxed) as isize;
alloc - dealloc
}
pub fn reset() {
ALLOC_COUNT.store(0, Ordering::Relaxed);
DEALLOC_COUNT.store(0, Ordering::Relaxed);
ALLOC_BYTES.store(0, Ordering::Relaxed);
DEALLOC_BYTES.store(0, Ordering::Relaxed);
}
pub fn measure<F, R>(f: F) -> (R, usize)
where
F: FnOnce() -> R,
{
let before = ALLOC_COUNT.load(Ordering::Relaxed);
let result = f();
let after = ALLOC_COUNT.load(Ordering::Relaxed);
(result, after.saturating_sub(before))
}
pub fn measure_detailed<F, R>(f: F) -> (R, AllocStats)
where
F: FnOnce() -> R,
{
let before_alloc = ALLOC_COUNT.load(Ordering::Relaxed);
let before_dealloc = DEALLOC_COUNT.load(Ordering::Relaxed);
let before_alloc_bytes = ALLOC_BYTES.load(Ordering::Relaxed);
let before_dealloc_bytes = DEALLOC_BYTES.load(Ordering::Relaxed);
let result = f();
let stats = AllocStats {
alloc_count: ALLOC_COUNT
.load(Ordering::Relaxed)
.saturating_sub(before_alloc),
dealloc_count: DEALLOC_COUNT
.load(Ordering::Relaxed)
.saturating_sub(before_dealloc),
alloc_bytes: ALLOC_BYTES
.load(Ordering::Relaxed)
.saturating_sub(before_alloc_bytes),
dealloc_bytes: DEALLOC_BYTES
.load(Ordering::Relaxed)
.saturating_sub(before_dealloc_bytes),
};
(result, stats)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AllocStats {
pub alloc_count: usize,
pub dealloc_count: usize,
pub alloc_bytes: usize,
pub dealloc_bytes: usize,
}
impl AllocStats {
pub fn net_count(&self) -> isize {
self.alloc_count as isize - self.dealloc_count as isize
}
pub fn net_bytes(&self) -> isize {
self.alloc_bytes as isize - self.dealloc_bytes as isize
}
pub fn is_zero_alloc(&self) -> bool {
self.alloc_count == 0
}
}
impl std::fmt::Display for AllocStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"alloc={}/{}, dealloc={}/{}, net={}/{}",
self.alloc_count,
self.alloc_bytes,
self.dealloc_count,
self.dealloc_bytes,
self.net_count(),
self.net_bytes()
)
}
}
unsafe impl<A: GlobalAlloc + Send + Sync + Default> GlobalAlloc for AllocCounter<A> {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
ALLOC_BYTES.fetch_add(layout.size(), Ordering::Relaxed);
A::default().alloc(layout)
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
DEALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
DEALLOC_BYTES.fetch_add(layout.size(), Ordering::Relaxed);
A::default().dealloc(ptr, layout);
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
ALLOC_BYTES.fetch_add(layout.size(), Ordering::Relaxed);
A::default().alloc_zeroed(layout)
}
unsafe fn realloc(&self, ptr: *mut u8, old_layout: Layout, new_size: usize) -> *mut u8 {
DEALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
DEALLOC_BYTES.fetch_add(old_layout.size(), Ordering::Relaxed);
ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
ALLOC_BYTES.fetch_add(new_size, Ordering::Relaxed);
A::default().realloc(ptr, old_layout, new_size)
}
}
#[macro_export]
macro_rules! register_alloc_counter {
() => {
#[global_allocator]
static GLOBAL_ALLOC: $crate::alloc_counter::AllocCounter<std::alloc::System> =
$crate::alloc_counter::AllocCounter {
_inner: std::alloc::System,
};
};
}
#[cfg(test)]
mod tests {
use super::*;
fn inc_alloc(n: usize, bytes: usize) {
ALLOC_COUNT.fetch_add(n, Ordering::Relaxed);
ALLOC_BYTES.fetch_add(bytes, Ordering::Relaxed);
}
fn inc_dealloc(n: usize, bytes: usize) {
DEALLOC_COUNT.fetch_add(n, Ordering::Relaxed);
DEALLOC_BYTES.fetch_add(bytes, Ordering::Relaxed);
}
#[test]
fn test_alloc_counter_measure_and_stats() {
AllocCounter::reset();
let (result, count) = AllocCounter::measure(|| {
inc_alloc(2, 64);
let s = "hello".to_string();
s + " world"
});
assert_eq!(result, "hello world");
assert_eq!(count, 2, "measure 应检测到 2 次 alloc");
AllocCounter::reset();
let (result, count) = AllocCounter::measure(|| {
let a: i32 = 1;
let b: i32 = 2;
a + b
});
assert_eq!(result, 3);
assert_eq!(count, 0, "纯栈运算不应触发堆 alloc");
AllocCounter::reset();
let (result, count) = AllocCounter::measure(|| {
inc_alloc(3, 400);
let v: Vec<i32> = (0..100).collect();
v.len()
});
assert_eq!(result, 100);
assert_eq!(count, 3, "measure 应检测到 3 次 alloc");
AllocCounter::reset();
let (result, stats) = AllocCounter::measure_detailed(|| {
inc_alloc(10, 1000);
inc_dealloc(2, 200);
let v: Vec<String> = (0..10).map(|i| format!("item_{i}")).collect();
v.len()
});
assert_eq!(result, 10);
assert_eq!(stats.alloc_count, 10);
assert_eq!(stats.dealloc_count, 2);
assert_eq!(stats.alloc_bytes, 1000);
assert_eq!(stats.dealloc_bytes, 200);
inc_alloc(5, 500);
inc_dealloc(3, 300);
AllocCounter::reset();
assert_eq!(AllocCounter::count(), 0);
assert_eq!(AllocCounter::dealloc_count(), 0);
assert_eq!(AllocCounter::alloc_bytes(), 0);
assert_eq!(AllocCounter::dealloc_bytes(), 0);
inc_alloc(10, 1000);
inc_dealloc(4, 400);
assert_eq!(AllocCounter::net_count(), 6);
assert_eq!(AllocCounter::net_bytes(), 600);
}
#[test]
fn test_alloc_stats_net_count() {
let stats = AllocStats {
alloc_count: 10,
dealloc_count: 3,
alloc_bytes: 1000,
dealloc_bytes: 300,
};
assert_eq!(stats.net_count(), 7);
assert_eq!(stats.net_bytes(), 700);
assert!(!stats.is_zero_alloc());
}
#[test]
fn test_alloc_stats_zero_alloc() {
let stats = AllocStats {
alloc_count: 0,
dealloc_count: 0,
alloc_bytes: 0,
dealloc_bytes: 0,
};
assert!(stats.is_zero_alloc());
assert_eq!(stats.net_count(), 0);
}
#[test]
fn test_alloc_stats_display() {
let stats = AllocStats {
alloc_count: 5,
dealloc_count: 2,
alloc_bytes: 500,
dealloc_bytes: 200,
};
let s = format!("{stats}");
assert!(s.contains("alloc=5/500"));
assert!(s.contains("dealloc=2/200"));
assert!(s.contains("net=3/300"));
}
}