Skip to main content

metricus_allocator/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use metricus::{Counter, CounterOps, Id, PreAllocatedMetric};
4use std::alloc::{GlobalAlloc, Layout};
5use std::cell::Cell;
6use std::sync::LazyLock;
7
8const ALLOC_COUNTER_ID: Id = Id::MAX - 1004;
9const ALLOC_BYTES_COUNTER_ID: Id = Id::MAX - 1003;
10const DEALLOC_COUNTER_ID: Id = Id::MAX - 1002;
11const DEALLOC_BYTES_COUNTER_ID: Id = Id::MAX - 1001;
12
13const fn get_aligned_size(layout: Layout) -> usize {
14    let alignment_mask: usize = layout.align() - 1;
15    (layout.size() + alignment_mask) & !alignment_mask
16}
17
18/// This allocator will use instrumentation to count the number of allocations and de-allocations
19/// occurring in the program. All calls to allocate (and free) memory are delegated to the concrete
20/// allocator (`std::alloc::System` by default). Once the allocator has been registered as
21/// `global_allocator` you need to call [enable_allocator_instrumentation] from each thread that
22/// wants to include its allocation and de-allocation metrics.
23///
24/// ```no_run
25/// use metricus_allocator::CountingAllocator;
26///
27/// #[global_allocator]
28/// static GLOBAL: CountingAllocator = CountingAllocator;
29/// ```
30pub struct CountingAllocator;
31
32#[allow(static_mut_refs)]
33unsafe impl GlobalAlloc for CountingAllocator {
34    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
35        // provide metrics only if instrumentation has been enabled for this thread
36        if INSTRUMENTATION_ENABLED.get() {
37            COUNTERS.alloc_count.increment();
38            COUNTERS.alloc_bytes.increment_by(get_aligned_size(layout) as u64);
39        }
40
41        // delegate to the appropriate allocator
42        #[cfg(all(feature = "jemalloc", not(feature = "mimalloc")))]
43        {
44            return unsafe { jemallocator::Jemalloc.alloc(layout) };
45        }
46        #[cfg(all(feature = "mimalloc", not(feature = "jemalloc")))]
47        {
48            return unsafe { mimalloc::MiMalloc.alloc(layout) };
49        }
50        unsafe { std::alloc::System.alloc(layout) }
51    }
52
53    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
54        // provide metrics only if instrumentation has been enabled for this thread
55        if INSTRUMENTATION_ENABLED.get() {
56            COUNTERS.dealloc_count.increment();
57            COUNTERS.dealloc_bytes.increment_by(get_aligned_size(layout) as u64);
58        }
59
60        // delegate to the appropriate allocator
61        #[cfg(all(feature = "jemalloc", not(feature = "mimalloc")))]
62        {
63            unsafe {
64                jemallocator::Jemalloc.dealloc(ptr, layout);
65            }
66            return;
67        }
68        #[cfg(all(feature = "mimalloc", not(feature = "jemalloc")))]
69        {
70            unsafe {
71                mimalloc::MiMalloc.dealloc(ptr, layout);
72            }
73            return;
74        }
75        unsafe { std::alloc::System.dealloc(ptr, layout) }
76    }
77}
78
79impl CountingAllocator {
80    /// Default counters to be used with the `CountingAllocator`.
81    pub fn metrics() -> Vec<PreAllocatedMetric> {
82        vec![
83            PreAllocatedMetric::counter("global_allocator", ALLOC_COUNTER_ID, &[("fn_name", "alloc")]),
84            PreAllocatedMetric::counter("global_allocator", ALLOC_BYTES_COUNTER_ID, &[("fn_name", "alloc_bytes")]),
85            PreAllocatedMetric::counter("global_allocator", DEALLOC_COUNTER_ID, &[("fn_name", "dealloc")]),
86            PreAllocatedMetric::counter("global_allocator", DEALLOC_BYTES_COUNTER_ID, &[("fn_name", "dealloc_bytes")]),
87        ]
88    }
89}
90
91thread_local! {
92    static INSTRUMENTATION_ENABLED: Cell<bool> = const { Cell::new(false) };
93}
94
95/// This should be called by a thread that wants to opt in to send allocation and de-allocation
96/// metrics. By default, per thread instrumentation is disabled. This is usually backend dependent
97/// as some backends can support sending metrics from multiple threads whereas others can be limited
98/// to the main thread only.
99///
100/// ## Examples
101///
102/// Enable instrumentation for the main thread.
103/// ```no_run
104///
105/// use metricus_allocator::enable_allocator_instrumentation;
106/// use metricus_allocator::CountingAllocator;
107///
108/// #[global_allocator]
109/// static GLOBAL: CountingAllocator = CountingAllocator;
110///
111/// fn main() {
112///     enable_allocator_instrumentation();
113/// }
114/// ```
115///
116/// Enable instrumentation for the background thread.
117/// ```no_run
118///
119/// use metricus_allocator::enable_allocator_instrumentation;
120/// use metricus_allocator::CountingAllocator;
121///
122/// #[global_allocator]
123/// static GLOBAL: CountingAllocator = CountingAllocator;
124///
125/// fn main() {
126///     let _ = std::thread::spawn(|| {
127///         enable_allocator_instrumentation();
128///     });
129/// }
130/// ```
131pub fn enable_allocator_instrumentation() {
132    INSTRUMENTATION_ENABLED.set(true);
133}
134
135static COUNTERS: LazyLock<Counters> = LazyLock::new(|| Counters {
136    // `counter_with_id` creates a counter object without registering it.
137    // These allocation counters are created lazily on first use and cache the active metrics handle.
138    // If they are initialized before `set_metrics`, they will remain bound to the no-op backend.
139    // Ensure the backend is set before enabling allocator instrumentation if you want these to emit.
140    alloc_count: Counter::new_with_id(ALLOC_COUNTER_ID),
141    alloc_bytes: Counter::new_with_id(ALLOC_BYTES_COUNTER_ID),
142    dealloc_count: Counter::new_with_id(DEALLOC_COUNTER_ID),
143    dealloc_bytes: Counter::new_with_id(DEALLOC_BYTES_COUNTER_ID),
144});
145
146struct Counters {
147    alloc_count: Counter,
148    alloc_bytes: Counter,
149    dealloc_count: Counter,
150    dealloc_bytes: Counter,
151}