accounting_allocator/
lib.rs

1//! `accounting-allocator` is a global memory allocator wrapper which counts allocated and deallocated bytes.
2//!
3//! # Usage
4//!
5//! ```
6//! use accounting_allocator::{AccountingAlloc, AllTimeAllocStats};
7//!
8//! #[global_allocator]
9//! static GLOBAL_ALLOCATOR: AccountingAlloc = AccountingAlloc::new();
10//!
11//! fn main() {
12//!     let AllTimeAllocStats { alloc, dealloc, largest_alloc } = GLOBAL_ALLOCATOR.count().all_time;
13//!     println!("alloc {alloc} dealloc {dealloc} largest_alloc {largest_alloc}");
14//! }
15//! ```
16
17use std::alloc::{GlobalAlloc, Layout, System};
18use std::cell::Cell;
19use std::fmt;
20use std::fmt::{Debug, Display};
21use std::mem;
22use std::panic::catch_unwind;
23use std::sync::atomic::AtomicUsize;
24use std::sync::atomic::Ordering::{AcqRel, Relaxed};
25use std::sync::{Arc, Mutex};
26
27use crossbeam_channel::{unbounded, Receiver, Sender};
28use once_cell::race::OnceBox;
29use once_cell::unsync::Lazy;
30
31#[derive(Default)]
32/// A global memory allocator wrapper which counts allocated and deallocated bytes.
33pub struct AccountingAlloc<A = System> {
34    thread_counters: OnceBox<ThreadCounters>,
35    allocator: A,
36}
37
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
39/// Statistics for allocations and deallocations made with an [`AccountingAlloc`], across all threads.
40pub struct AllocStats {
41    /// Allocator statistics over all time.
42    pub all_time: AllTimeAllocStats,
43
44    /// Allocator statistics since the last call to [`AccountingAlloc::count`].
45    pub since_last: IncrementalAllocStats,
46}
47
48#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
49/// Statistics for allocations and deallocations made with an [`AccountingAlloc`] for all time, across all threads.
50pub struct AllTimeAllocStats {
51    /// Count of allocated bytes.
52    pub alloc: usize,
53    /// Count of deallocated bytes.
54    pub dealloc: usize,
55    /// Largest allocation size in bytes.
56    pub largest_alloc: usize,
57}
58
59#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
60/// Statistics for allocations and deallocations made with an [`AccountingAlloc`] since the last call to
61/// [`AccountingAlloc::count`], across all threads.
62pub struct IncrementalAllocStats {
63    /// Count of allocated bytes.
64    pub alloc: usize,
65    /// Count of deallocated bytes.
66    pub dealloc: usize,
67    /// Largest allocation size in bytes.
68    pub largest_alloc: usize,
69}
70
71#[derive(Debug)]
72struct ThreadCounters {
73    tx: Sender<Arc<ThreadCounter>>,
74    shared: Mutex<ThreadCountersShared>,
75}
76
77#[derive(Debug)]
78struct ThreadCountersShared {
79    rx: Receiver<Arc<ThreadCounter>>,
80    counters: Vec<Arc<ThreadCounter>>,
81    dead_alloc: usize,
82    dead_dealloc: usize,
83    all_time: AllTimeAllocStats,
84}
85
86#[derive(Debug, Default)]
87struct ThreadCounter {
88    alloc: AtomicUsize,
89    dealloc: AtomicUsize,
90    largest_alloc: AtomicUsize,
91}
92
93#[derive(Clone, Copy, Debug)]
94enum ThreadCounterState {
95    Uninitialized,
96    Initializing(AllTimeAllocStats),
97    Initialized,
98}
99
100impl AccountingAlloc<System> {
101    /// Create a new [`AccountingAlloc`] using the [`System`] allocator.
102    pub const fn new() -> Self {
103        Self::with_allocator(System)
104    }
105}
106
107impl<A> AccountingAlloc<A> {
108    /// Create a new [`AccountingAlloc`] using the given allocator `A`.
109    ///
110    /// Note that in order for `AccountingAlloc<A>` to implement [`GlobalAlloc`], `A` must implement [`GlobalAlloc`].
111    pub const fn with_allocator(allocator: A) -> Self {
112        Self { thread_counters: OnceBox::new(), allocator }
113    }
114
115    /// Return the latest statistics for this allocator.
116    pub fn count(&self) -> AllocStats {
117        let thread_counters = self.thread_counters.get_or_init(Default::default);
118        thread_counters.shared.lock().unwrap().count()
119    }
120
121    /// Increment the current thread's (de)allocated bytes count by `alloc` and `dealloc`.
122    pub fn inc(&self, mut alloc: usize, mut dealloc: usize) {
123        use ThreadCounterState::{Initialized, Initializing, Uninitialized};
124
125        thread_local! {
126            static COUNTER: Lazy<Arc<ThreadCounter>> = Default::default();
127            static STATE: Cell<ThreadCounterState> = Cell::new(Uninitialized);
128        }
129
130        let thread_counters = &self.thread_counters;
131
132        // As of rust 1.65.0, panicking from GlobalAlloc methods is UB, so catch anything here. NB: catch_unwind
133        // allocates internally in the unwinding case, so any panic here will likely recurse and cause a double-panic,
134        // resulting in a process abort.
135        let _ignore = catch_unwind(move || {
136            match STATE.try_with(|state| state.get())? {
137                Uninitialized => {
138                    // Transition to an "initializing" state, to prevent infinite recursion when we allocate below.
139                    STATE.try_with(|state| state.set(Initializing(AllTimeAllocStats::default())))?;
140
141                    // NB: LocalKey::<T>::try_with also allocates internally when T has a destructor.
142                    let counter = COUNTER.try_with(|counter| Arc::clone(counter))?;
143
144                    // Transition to "initialized" state.
145                    let mut largest_alloc = alloc;
146                    if let Initializing(init_counter) = STATE.try_with(|state| state.replace(Initialized))? {
147                        alloc += init_counter.alloc;
148                        dealloc += init_counter.dealloc;
149                        largest_alloc = largest_alloc.max(init_counter.largest_alloc);
150                    }
151
152                    counter.alloc.fetch_add(alloc, Relaxed);
153                    counter.dealloc.fetch_add(dealloc, Relaxed);
154                    counter.largest_alloc.fetch_max(largest_alloc, AcqRel);
155
156                    let thread_counters = thread_counters.get_or_init(Default::default);
157                    let _ignore = thread_counters.tx.send(counter);
158
159                    Ok(())
160                }
161
162                Initializing(init_counts) => STATE.try_with(|state| {
163                    state.set(Initializing(AllTimeAllocStats {
164                        alloc: init_counts.alloc + alloc,
165                        dealloc: init_counts.dealloc + dealloc,
166                        largest_alloc: init_counts.largest_alloc.max(alloc),
167                    }))
168                }),
169
170                // This function is called from dealloc() in the destructor for the thread-local `COUNTER`. We use
171                // LocalKey::try_with here so we don't panic when that's the case.
172                Initialized => COUNTER.try_with(|counter| {
173                    counter.alloc.fetch_add(alloc, Relaxed);
174                    counter.dealloc.fetch_add(dealloc, Relaxed);
175                    counter.largest_alloc.fetch_max(alloc, AcqRel);
176                }),
177            }
178        });
179    }
180}
181
182impl<A: Debug> Debug for AccountingAlloc<A> {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        f.debug_struct("AccountingAlloc")
185            .field("thread_counters", &self.thread_counters.get())
186            .field("allocator", &self.allocator)
187            .finish()
188    }
189}
190
191/// Display the number of bytes each live thread has allocated and deallocated over all time, along with the global
192/// total.
193impl<A> Display for AccountingAlloc<A> {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        let thread_counters = self.thread_counters.get_or_init(Default::default);
196        let mut shared = thread_counters.shared.lock().unwrap();
197
198        let AllTimeAllocStats { alloc, dealloc, largest_alloc } = shared.count().all_time;
199
200        for (thread_idx, thread_counter) in shared.counters.iter().enumerate() {
201            let thread_alloc = thread_counter.alloc.load(Relaxed);
202            let thread_dealloc = thread_counter.dealloc.load(Relaxed);
203            writeln!(f, "Thread {thread_idx}: alloc {thread_alloc} dealloc {thread_dealloc}")?;
204        }
205        let total = alloc - dealloc;
206        writeln!(
207            f,
208            "Total: {total} (alloc {alloc} dealloc {dealloc} largest_alloc {largest_alloc})"
209        )
210    }
211}
212
213unsafe impl<A: GlobalAlloc> GlobalAlloc for AccountingAlloc<A> {
214    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
215        self.inc(layout.size(), 0);
216        self.allocator.alloc(layout)
217    }
218
219    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
220        self.inc(0, layout.size());
221        self.allocator.dealloc(ptr, layout);
222    }
223
224    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
225        self.inc(layout.size(), 0);
226        self.allocator.alloc_zeroed(layout)
227    }
228
229    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
230        self.inc(new_size, layout.size());
231        self.allocator.realloc(ptr, layout, new_size)
232    }
233}
234
235impl Default for ThreadCounters {
236    fn default() -> Self {
237        let (tx, rx) = unbounded();
238        Self {
239            tx,
240            shared: Mutex::new(ThreadCountersShared {
241                rx,
242                counters: Vec::with_capacity(64),
243                dead_alloc: Default::default(),
244                dead_dealloc: Default::default(),
245                all_time: Default::default(),
246            }),
247        }
248    }
249}
250
251impl ThreadCountersShared {
252    fn count(&mut self) -> AllocStats {
253        let mut alloc = 0;
254        let mut dealloc = 0;
255        let mut largest_alloc = 0;
256
257        self.counters.retain_mut(|counter| match Arc::get_mut(counter) {
258            Some(counter) => {
259                self.dead_alloc += *counter.alloc.get_mut();
260                self.dead_dealloc += *counter.dealloc.get_mut();
261                largest_alloc = largest_alloc.max(*counter.largest_alloc.get_mut());
262                false
263            }
264            None => {
265                alloc += counter.alloc.load(Relaxed);
266                dealloc += counter.dealloc.load(Relaxed);
267                largest_alloc = largest_alloc.max(counter.largest_alloc.swap(0, AcqRel));
268                true
269            }
270        });
271
272        for counter in self.rx.try_iter() {
273            match Arc::try_unwrap(counter) {
274                Ok(mut counter) => {
275                    self.dead_alloc += *counter.alloc.get_mut();
276                    self.dead_dealloc += *counter.dealloc.get_mut();
277                    largest_alloc = largest_alloc.max(*counter.largest_alloc.get_mut());
278                }
279                Err(counter) => {
280                    alloc += counter.alloc.load(Relaxed);
281                    dealloc += counter.dealloc.load(Relaxed);
282                    largest_alloc = largest_alloc.max(counter.largest_alloc.swap(0, AcqRel));
283                    self.counters.push(counter);
284                }
285            }
286        }
287
288        alloc += self.dead_alloc;
289        dealloc += self.dead_dealloc;
290
291        let all_time =
292            AllTimeAllocStats { alloc, dealloc, largest_alloc: self.all_time.largest_alloc.max(largest_alloc) };
293        let last_all_time = mem::replace(&mut self.all_time, all_time);
294        let since_last = IncrementalAllocStats {
295            alloc: alloc - last_all_time.alloc,
296            dealloc: dealloc - last_all_time.dealloc,
297            largest_alloc,
298        };
299
300        AllocStats { all_time, since_last }
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use std::convert::identity;
307
308    use crossbeam_utils::thread::scope;
309
310    use super::*;
311
312    #[derive(Default)]
313    /// A fake [`GlobalAlloc`] memory allocator for testing, which doesn't actually allocate the memory requested.
314    ///
315    /// This allocator must not actually be registered as the global allocator using `#[global_allocator]`. The pointers
316    /// returned by this allocator should be treated as opaque and only used in subsequent calls to `<TestAlloc as
317    /// GlobalAlloc>` methods.
318    ///
319    /// Though technically panicking from [`GlobalAlloc`] methods is currently considered UB, this is only relevant if
320    /// the [`GlobalAlloc`] is actually registered as the global allocator. Thus, to verify correctness during tests,
321    /// `dealloc` and `realloc` will panic if `layout` is not equal to the `layout` provided to `alloc`.
322    struct TestAlloc;
323
324    /// Metadata for an allocation made by [`TestAlloc`].
325    struct Allocation {
326        layout: Layout,
327    }
328
329    /// A handle to an allocation made by an `AccountingAlloc<TestAlloc>`.
330    ///
331    /// The allocation will be deallocated when this structure is dropped.
332    struct AllocationHandle<'a> {
333        allocator: &'a AccountingAlloc<TestAlloc>,
334        ptr: *mut u8,
335        layout: Layout,
336    }
337
338    /// Make some variously sized test allocations in separate threads using the provided `allocate` function and call
339    /// `callback`.
340    ///
341    /// The `AllocationHandle`s returned by `allocate` are passed to `callback`. After `callback` returns, all spawned
342    /// threads are waited on to terminate, and the return value of `callback` is returned.
343    fn test_allocations<'a, T>(
344        allocator: &'a AccountingAlloc<TestAlloc>,
345        allocate: fn(&'a AccountingAlloc<TestAlloc>, Layout) -> AllocationHandle<'a>,
346        callback: impl FnOnce(Vec<AllocationHandle<'a>>) -> T,
347    ) -> T {
348        let layouts: Vec<_> = (1..10).map(|idx| Layout::array::<u8>(10000 * idx).unwrap()).collect();
349        let (allocations_tx, allocations_rx) = unbounded();
350        scope(|scope| {
351            for layout in layouts.clone() {
352                let allocations_tx = allocations_tx.clone();
353                scope.spawn(move |_scope| allocations_tx.send(allocate(allocator, layout)).unwrap());
354            }
355            drop(allocations_tx);
356            callback(allocations_rx.into_iter().collect())
357        })
358        .unwrap()
359    }
360
361    /// Calculate the [`AllocStats`] expected from the next call to [`AccountingAlloc::count`] assuming the (only)
362    /// allocations made on the allocator are in `allocations`.
363    fn expected_counts<'a>(allocations: &[AllocationHandle<'a>]) -> AllocStats {
364        let allocation_sizes = allocations.iter().map(|allocation| allocation.layout.size());
365        let since_last = IncrementalAllocStats {
366            alloc: allocation_sizes.clone().sum::<usize>(),
367            dealloc: 0,
368            largest_alloc: allocation_sizes.max().unwrap(),
369        };
370        AllocStats {
371            all_time: AllTimeAllocStats {
372                alloc: since_last.alloc,
373                dealloc: since_last.dealloc,
374                largest_alloc: since_last.largest_alloc,
375            },
376            since_last,
377        }
378    }
379
380    #[test]
381    fn alloc() {
382        let allocator = Default::default();
383        let (_allocations, expected) = test_allocations(&allocator, AllocationHandle::new, |allocations| {
384            // test `allocator.count()` while threads are still alive.
385            let expected = expected_counts(&allocations);
386            assert_eq!(allocator.count(), expected);
387            (allocations, expected)
388        });
389
390        // test `allocator.count()` again after the threads are dead.
391        assert_eq!(
392            allocator.count(),
393            AllocStats { since_last: Default::default(), ..expected }
394        );
395    }
396
397    #[test]
398    fn dealloc() {
399        let allocator = &Default::default();
400        let allocations = test_allocations(&allocator, AllocationHandle::new, identity);
401        let expected = expected_counts(&allocations);
402
403        assert_eq!(allocator.count(), expected);
404
405        scope(|scope| {
406            for allocation in allocations {
407                scope.spawn(move |_scope| drop(allocation));
408            }
409        })
410        .unwrap();
411
412        assert_eq!(
413            allocator.count(),
414            AllocStats {
415                all_time: AllTimeAllocStats { dealloc: expected.all_time.alloc, ..expected.all_time },
416                since_last: IncrementalAllocStats { dealloc: expected.all_time.alloc, ..Default::default() },
417            }
418        );
419    }
420
421    #[test]
422    fn alloc_zeroed() {
423        let allocator = &Default::default();
424        let allocations = test_allocations(&allocator, AllocationHandle::new_zeroed, identity);
425        let expected = expected_counts(&allocations);
426
427        assert_eq!(allocator.count(), expected);
428    }
429
430    #[test]
431    fn realloc() {
432        let allocator = &Default::default();
433        let mut allocations = test_allocations(&allocator, AllocationHandle::new, identity);
434        let expected = expected_counts(&allocations);
435
436        assert_eq!(allocator.count(), expected);
437
438        scope(|scope| {
439            for allocation in &mut allocations {
440                scope.spawn(move |_scope| allocation.realloc(allocation.layout.size() * 2));
441            }
442        })
443        .unwrap();
444
445        let expected_2 = expected_counts(&allocations);
446
447        assert_eq!(
448            allocator.count(),
449            AllocStats {
450                all_time: AllTimeAllocStats {
451                    alloc: expected.since_last.alloc + expected_2.since_last.alloc,
452                    dealloc: expected.since_last.alloc,
453                    largest_alloc: expected_2.since_last.largest_alloc,
454                },
455                since_last: IncrementalAllocStats { dealloc: expected.since_last.alloc, ..expected_2.since_last }
456            }
457        );
458    }
459
460    unsafe impl GlobalAlloc for TestAlloc {
461        unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
462            // Since we're not actually accessing this allocated memory in tests, we don't actually need to allocate
463            // anything, but allocating something using the system allocator lets us leverage it for double-free and
464            // leak detection. We save the Layout in a Box, however, to be able to later assert that the Layout upon
465            // dealloc/realloc matches, for correctness verification.
466            Box::into_raw(Box::new(Allocation { layout })) as *mut u8
467        }
468
469        unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
470            // Claim ownership of the allocation and free it.
471            let allocation = Box::from_raw(ptr as *mut Allocation);
472            assert_eq!(layout, allocation.layout);
473            drop(allocation);
474        }
475
476        unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
477            self.alloc(layout)
478        }
479
480        unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
481            self.dealloc(ptr, layout);
482            self.alloc(Layout::from_size_align_unchecked(new_size, layout.align()))
483        }
484    }
485
486    impl<'a> AllocationHandle<'a> {
487        /// Make a new allocation on `allocator` with the given `layout`.
488        fn new(allocator: &'a AccountingAlloc<TestAlloc>, layout: Layout) -> Self {
489            Self { allocator, ptr: unsafe { allocator.alloc(layout) }, layout }
490        }
491
492        /// Make a new zeroed allocation on `allocator` with the given `layout`.
493        fn new_zeroed(allocator: &'a AccountingAlloc<TestAlloc>, layout: Layout) -> Self {
494            Self { allocator, ptr: unsafe { allocator.alloc_zeroed(layout) }, layout }
495        }
496
497        /// Resize an allocation.
498        fn realloc(&mut self, new_size: usize) {
499            unsafe {
500                self.ptr = self.allocator.realloc(self.ptr, self.layout, new_size);
501                self.layout = Layout::from_size_align_unchecked(new_size, self.layout.align());
502            }
503        }
504    }
505
506    unsafe impl Send for AllocationHandle<'_> {}
507
508    impl Drop for AllocationHandle<'_> {
509        fn drop(&mut self) {
510            unsafe { self.allocator.dealloc(self.ptr, self.layout) };
511        }
512    }
513}