Skip to main content

concinnity_memory/
tracking.rs

1// concinnity-memory/src/alloc.rs
2//
3// The `GlobalAlloc` wrapper that counts what the Rust heap is holding.
4//
5// Installing it is the binary's call, since `#[global_allocator]` is a
6// per-program item and an embedder may want an allocator of its own:
7//
8//     #[global_allocator]
9//     static ALLOC: TrackingAlloc<System> = TrackingAlloc::new(System);
10//
11// Until some binary does, `stats()` reports `None` rather than a zero that would
12// read as "the heap is empty".
13
14use core::alloc::{GlobalAlloc, Layout};
15
16use crate::counters::Counters;
17
18pub(crate) static COUNTERS: Counters = Counters::new();
19
20/// A `GlobalAlloc` that counts allocations and forwards them to `A`.
21pub struct TrackingAlloc<A> {
22    inner: A,
23}
24
25impl<A> TrackingAlloc<A> {
26    /// Wrap `inner` so every allocation through it is counted.
27    pub const fn new(inner: A) -> Self {
28        Self { inner }
29    }
30}
31
32// SAFETY: every method forwards to `inner`, which upholds the `GlobalAlloc`
33// contract, and returns its pointer unchanged. The counter updates are relaxed
34// atomics over statically allocated storage, so the allocator cannot re-enter
35// itself.
36unsafe impl<A: GlobalAlloc> GlobalAlloc for TrackingAlloc<A> {
37    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
38        // SAFETY: `layout` is forwarded unchanged from our caller, who upholds
39        // the `GlobalAlloc::alloc` contract.
40        let ptr = unsafe { self.inner.alloc(layout) };
41        if !ptr.is_null() {
42            COUNTERS.record_alloc(layout.size());
43            crate::detail::record_alloc(layout.size());
44        }
45        ptr
46    }
47
48    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
49        // SAFETY: as `alloc`, forwarding our caller's obligations to `inner`.
50        let ptr = unsafe { self.inner.alloc_zeroed(layout) };
51        if !ptr.is_null() {
52            COUNTERS.record_alloc(layout.size());
53            crate::detail::record_alloc(layout.size());
54        }
55        ptr
56    }
57
58    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
59        // SAFETY: `ptr` and `layout` come from our caller, who guarantees the
60        // block was allocated by us with this layout; we allocated it through
61        // `inner`, so `inner` is the right allocator to free it.
62        unsafe { self.inner.dealloc(ptr, layout) };
63        COUNTERS.record_free(layout.size());
64        crate::detail::record_free(layout.size());
65    }
66
67    // Forwarded rather than left to the default alloc-copy-dealloc so `inner`
68    // can grow a block in place. A failed resize leaves the original block
69    // allocated and unchanged, so the counters only move on success.
70    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
71        // SAFETY: `ptr`, `layout` and `new_size` are forwarded unchanged from
72        // our caller, who upholds the `GlobalAlloc::realloc` contract.
73        let new_ptr = unsafe { self.inner.realloc(ptr, layout, new_size) };
74        if !new_ptr.is_null() {
75            COUNTERS.record_realloc(layout.size(), new_size);
76            crate::detail::record_realloc(layout.size(), new_size);
77        }
78        new_ptr
79    }
80}
81
82/// Install [`TrackingAlloc`] over the system allocator as this program's
83/// `#[global_allocator]`.
84///
85/// `#[global_allocator]` is a per-program item, so every binary, test binary,
86/// and benchmark that should count its own heap invokes this once at its crate
87/// root. A program without it runs on Rust's default allocator and reports no
88/// memory at all, which is what [`crate::stats`] returning `None` means.
89///
90/// ```
91/// concinnity_memory::install_global_allocator!();
92/// ```
93#[macro_export]
94macro_rules! install_global_allocator {
95    () => {
96        #[global_allocator]
97        static CN_GLOBAL_ALLOC: $crate::TrackingAlloc<std::alloc::System> =
98            $crate::TrackingAlloc::new(std::alloc::System);
99    };
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use std::alloc::System;
106
107    // Drives the real wrapper against the real system allocator. This is the
108    // one test that touches the process-global counters, so it asserts on
109    // deltas rather than absolute values.
110    #[test]
111    fn tracking_alloc_forwards_and_counts() {
112        let alloc = TrackingAlloc::new(System);
113        let layout = Layout::from_size_align(4096, 8).expect("valid layout");
114
115        let before = crate::stats().map(|s| s.live_bytes).unwrap_or(0);
116        // SAFETY: `layout` has a non-zero size, and the block is freed below
117        // through the same allocator and layout it was allocated with.
118        let ptr = unsafe { alloc.alloc(layout) };
119        assert!(!ptr.is_null(), "system allocator returned null for 4 KiB");
120
121        let during = crate::stats()
122            .expect("the wrapper just allocated")
123            .live_bytes;
124        assert!(
125            during >= before + 4096,
126            "live bytes {during} did not rise by the allocation size from {before}"
127        );
128
129        // SAFETY: `ptr` came from `alloc.alloc(layout)` above and has not been
130        // freed or reallocated since.
131        unsafe { alloc.dealloc(ptr, layout) };
132        assert!(crate::stats().expect("counters are live").live_bytes >= before);
133    }
134}