Skip to main content

alloc_counter/
alloc_counter.rs

1//! Per-thread allocation counting for tests.
2//!
3//! Linking this crate installs a counting [`GlobalAlloc`](std::alloc::GlobalAlloc)
4//! wrapper around [`System`](std::alloc::System). Every test binary that uses it
5//! can then measure how many allocations a piece of code performs:
6//!
7//! ```
8//! use alloc_counter::measure;
9//!
10//! let (value, counters) = measure(|| 1u32 + 1);
11//! assert_eq!(value, 2);
12//! assert_eq!(counters.calls, 0);
13//! ```
14//!
15//! # Per-thread counting
16//!
17//! Counters are always on and scoped to the thread that performs the
18//! allocation. This keeps measurements exact even when the test harness runs
19//! other tests, and their bookkeeping, in parallel: an allocation made by
20//! another thread is never attributed to the measuring one. The trade-off is
21//! that only the calling thread is observed, so work that must be measured has
22//! to run on it.
23//!
24//! [`measure`] returns the delta across a closure; [`counters`] returns a
25//! snapshot for manual before/after comparisons. The `calls` and `bytes`
26//! counters are monotonic totals of allocation requests (allocation, zeroed
27//! allocation and reallocation); `live_bytes` and `live_allocations` track
28//! memory that is still allocated and can be used to detect leaks.
29//!
30//! The `live_*` gauges assume that an allocation is released by the thread
31//! that made it. Freeing on another thread changes that thread's gauge instead,
32//! so it can go negative; scope deltas taken on a single thread are exact.
33//!
34//! # Global allocator constraint
35//!
36//! Rust allows a single `#[global_allocator]` per binary, and this crate
37//! provides one. A test binary that links `alloc_counter` therefore cannot also
38//! install a custom allocator (for example one that verifies zeroization).
39//! Test binaries that do not use this crate are unaffected: the allocator is
40//! only linked in when the crate is referenced.
41//!
42//! Nothing in this crate panics. Allocations made while a thread's counters
43//! are unavailable (during thread-local destruction) are simply not counted.
44
45use core::cell::Cell;
46use std::alloc::{GlobalAlloc, Layout, System};
47
48/// Allocation counters for a single thread.
49#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
50pub struct Counters {
51    /// Number of allocation requests: allocations, zeroed allocations and
52    /// reallocations.
53    pub calls: u64,
54    /// Total number of bytes requested by those allocations.
55    pub bytes: u64,
56    /// Bytes currently allocated and not yet freed.
57    pub live_bytes: i64,
58    /// Number of allocations currently live.
59    pub live_allocations: i64,
60}
61
62impl Counters {
63    /// The all-zero value.
64    const ZERO: Counters = Counters {
65        calls: 0,
66        bytes: 0,
67        live_bytes: 0,
68        live_allocations: 0,
69    };
70
71    /// Returns the counters between `earlier` and `self`.
72    ///
73    /// # Panics
74    ///
75    /// Never panics: `calls` and `bytes` are monotonic, and the `live_*`
76    /// gauges may legitimately decrease (or go negative across threads).
77    #[must_use]
78    pub fn delta(self, earlier: Counters) -> Counters {
79        Counters {
80            calls: self.calls - earlier.calls,
81            bytes: self.bytes - earlier.bytes,
82            live_bytes: self.live_bytes - earlier.live_bytes,
83            live_allocations: self.live_allocations - earlier.live_allocations,
84        }
85    }
86}
87
88thread_local! {
89    // The `const` initialiser is load-bearing: the global allocator reads this
90    // on every allocation, so a lazily-initialised thread-local would allocate
91    // on first access and recurse into the allocator.
92    static COUNTERS: Cell<Counters> = const { Cell::new(Counters::ZERO) };
93}
94
95/// Returns a snapshot of the current thread's allocation counters.
96pub fn counters() -> Counters {
97    COUNTERS.try_with(Cell::get).unwrap_or_default()
98}
99
100/// Runs `f` and returns its result along with the allocation counters the
101/// current thread accumulated during the call.
102///
103/// Allocations made by other threads are not counted.
104pub fn measure<T>(f: impl FnOnce() -> T) -> (T, Counters) {
105    let before = counters();
106    let value = f();
107    (value, counters().delta(before))
108}
109
110/// Applies `f` to the current thread's counters, if they are available.
111///
112/// Safe to call from inside the global allocator: accessing a `const`
113/// thread-local never allocates. Threads whose thread-locals are being torn
114/// down are skipped, since allocation may still happen during destruction.
115fn update(f: impl FnOnce(&mut Counters)) {
116    let _ = COUNTERS.try_with(|cell| {
117        let mut counters = cell.get();
118        f(&mut counters);
119        cell.set(counters);
120    });
121}
122
123fn record_allocation(size: usize) {
124    update(|counters| {
125        counters.calls += 1;
126        counters.bytes += size as u64;
127        counters.live_bytes += size as i64;
128        counters.live_allocations += 1;
129    });
130}
131
132fn record_reallocation(old_size: usize, new_size: usize) {
133    update(|counters| {
134        counters.calls += 1;
135        counters.bytes += new_size as u64;
136        counters.live_bytes += new_size as i64 - old_size as i64;
137    });
138}
139
140fn record_deallocation(size: usize) {
141    update(|counters| {
142        counters.live_bytes -= size as i64;
143        counters.live_allocations -= 1;
144    });
145}
146
147struct CountingAllocator;
148
149unsafe impl GlobalAlloc for CountingAllocator {
150    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
151        record_allocation(layout.size());
152        unsafe { System.alloc(layout) }
153    }
154
155    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
156        record_allocation(layout.size());
157        unsafe { System.alloc_zeroed(layout) }
158    }
159
160    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
161        record_reallocation(layout.size(), new_size);
162        unsafe { System.realloc(ptr, layout, new_size) }
163    }
164
165    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
166        record_deallocation(layout.size());
167        unsafe { System.dealloc(ptr, layout) }
168    }
169}
170
171#[global_allocator]
172static ALLOCATOR: CountingAllocator = CountingAllocator;