tracking_allocator/
lib.rs

1//! # tracking-allocator
2//!
3//! This crate provides a global allocator implementation (compatible with [`GlobalAlloc`][global_alloc]) that allows
4//! users to trace allocations and deallocations directly.  Allocation tokens can also be registered, which allows users
5//! to get an identifier that has associated metadata, which when used, can enhance the overall tracking of allocations.
6//!
7//! ## high-level usage
8//!
9//! `tracking-allocator` has three main components:
10//! - [`Allocator`], a [`GlobalAlloc`][global_alloc]-compatible allocator that intercepts allocations and deallocations
11//! - the [`AllocationTracker`] trait, which defines an interface for receiving allocation and deallocation events
12//! - [`AllocationGroupToken`] which is used to associate allocation events with a logical group
13//!
14//! These components all work in tandem together.  Once the allocator is installed, an appropriate tracker
15//! implementation can also be installed to handle the allocation and deallocation events as desired, whether you're
16//! simply tracking the frequency of allocations, or trying to track the real-time usage of different allocation groups.
17//! Allocation groups can be created on-demand, as well, which makes them suitable to tracking additional logical groups
18//! over the lifetime of the process.
19//!
20//! Additionally, tracking can be enabled and disabled at runtime, allowing you to make the choice of when to incur the
21//! performance overhead of tracking.
22//!
23//! ## examples
24//!
25//! Two main examples are provided: `stdout` and `tracing`.  Both examples demonstrate how to effectively to use the
26//! crate, but the `tracing` example is specific to using the `tracing-compat` feature.
27//!
28//! The examples are considered the primary documentation for the "how" of using this crate effectively.  They are
29//! extensively documented, and touch on the finer points of writing a tracker implementation, including how to avoid
30//! specific pitfalls related to deadlocking and reentrant code that could lead to stack overflows.
31//!
32//! [global_alloc]: std::alloc::GlobalAlloc
33#![cfg_attr(docsrs, feature(doc_cfg))]
34#![deny(missing_docs)]
35#![deny(clippy::pedantic)]
36#![allow(clippy::inline_always)]
37#![allow(clippy::module_name_repetitions)]
38use std::{
39    error, fmt,
40    sync::{
41        atomic::{AtomicBool, AtomicUsize, Ordering},
42        Arc,
43    },
44};
45
46mod allocator;
47mod stack;
48mod token;
49#[cfg(feature = "tracing-compat")]
50mod tracing;
51mod util;
52
53use token::with_suspended_allocation_group;
54
55pub use crate::allocator::Allocator;
56pub use crate::token::{AllocationGroupId, AllocationGroupToken, AllocationGuard};
57#[cfg(feature = "tracing-compat")]
58pub use crate::tracing::AllocationLayer;
59
60/// Whether or not allocations should be tracked.
61static TRACKING_ENABLED: AtomicBool = AtomicBool::new(false);
62
63// The global tracker.  This is called for all allocations, passing through the information to
64// whichever implementation is currently set.
65static mut GLOBAL_TRACKER: Option<Tracker> = None;
66static GLOBAL_INIT: AtomicUsize = AtomicUsize::new(UNINITIALIZED);
67
68const UNINITIALIZED: usize = 0;
69const INITIALIZING: usize = 1;
70const INITIALIZED: usize = 2;
71
72/// Tracks allocations and deallocations.
73pub trait AllocationTracker {
74    /// Tracks when an allocation has occurred.
75    ///
76    /// All allocations/deallocations that occur within the call to `AllocationTracker::allocated` are ignored, so
77    /// implementors can allocate/deallocate without risk of reentrancy bugs. It does mean, however, that the
78    /// allocations/deallocations that occur will be effectively lost, so implementors should ensure that the only data
79    /// they deallocate in the tracker is data that was similarly allocated, and vise versa.
80    ///
81    /// As the allocator will customize the layout to include the group ID which owns an allocation, we provide two
82    /// sizes: the object size and the wrapped size. The object size is the original layout of the allocation, and is
83    /// valid against the given object address. The wrapped size is the true size of the underlying allocation that is
84    /// made, and represents the actual memory usage for the given allocation.
85    fn allocated(
86        &self,
87        addr: usize,
88        object_size: usize,
89        wrapped_size: usize,
90        group_id: AllocationGroupId,
91    );
92
93    /// Tracks when a deallocation has occurred.
94    ///
95    /// `source_group_id` contains the group ID where the given allocation originated from, while `current_group_id` is
96    /// the current group ID, and as such, these values may differ depending on how values have had their ownership
97    /// transferred.
98    ///
99    /// All allocations/deallocations that occur within the call to `AllocationTracker::deallocated` are ignored, so
100    /// implementors can allocate/deallocate without risk of reentrancy bugs. It does mean, however, that the
101    /// allocations/deallocations that occur will be effectively lost, so implementors should ensure that the only data
102    /// they deallocate in the tracker is data that was similarly allocated, and vise versa.
103    ///
104    /// As the allocator will customize the layout to include the group ID which owns an allocation, we provide two
105    /// sizes: the object size and the wrapped size. The object size is the original layout of the allocation, and is
106    /// valid against the given object address. The wrapped size is the true size of the underlying allocation that is
107    /// made, and represents the actual memory usage for the given allocation.
108    fn deallocated(
109        &self,
110        addr: usize,
111        object_size: usize,
112        wrapped_size: usize,
113        source_group_id: AllocationGroupId,
114        current_group_id: AllocationGroupId,
115    );
116}
117
118struct Tracker {
119    tracker: Arc<dyn AllocationTracker + Send + Sync + 'static>,
120}
121
122impl Tracker {
123    fn from_allocation_tracker<T>(allocation_tracker: T) -> Self
124    where
125        T: AllocationTracker + Send + Sync + 'static,
126    {
127        Self {
128            tracker: Arc::new(allocation_tracker),
129        }
130    }
131
132    /// Tracks when an allocation has occurred.
133    fn allocated(
134        &self,
135        addr: usize,
136        object_size: usize,
137        wrapped_size: usize,
138        group_id: AllocationGroupId,
139    ) {
140        self.tracker
141            .allocated(addr, object_size, wrapped_size, group_id);
142    }
143
144    /// Tracks when a deallocation has occurred.
145    fn deallocated(
146        &self,
147        addr: usize,
148        object_size: usize,
149        wrapped_size: usize,
150        source_group_id: AllocationGroupId,
151        current_group_id: AllocationGroupId,
152    ) {
153        self.tracker.deallocated(
154            addr,
155            object_size,
156            wrapped_size,
157            source_group_id,
158            current_group_id,
159        );
160    }
161}
162
163/// Returned if trying to set the global tracker fails.
164#[derive(Debug)]
165pub struct SetTrackerError {
166    _sealed: (),
167}
168
169impl fmt::Display for SetTrackerError {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        f.pad("a global tracker has already been set")
172    }
173}
174
175impl error::Error for SetTrackerError {}
176
177/// Handles registering tokens for tracking different allocation groups.
178pub struct AllocationRegistry;
179
180impl AllocationRegistry {
181    /// Enables the tracking of allocations.
182    pub fn enable_tracking() {
183        TRACKING_ENABLED.store(true, Ordering::SeqCst);
184    }
185
186    /// Disables the tracking of allocations.
187    pub fn disable_tracking() {
188        TRACKING_ENABLED.store(false, Ordering::SeqCst);
189    }
190
191    /// Sets the global tracker.
192    ///
193    /// Setting a global tracker does not enable or disable the tracking of allocations, so callers
194    /// still need to call `enable_tracking` after this in order to fully enable tracking.
195    ///
196    /// # Errors
197    /// `Err(SetTrackerError)` is returned if a global tracker has already been set, otherwise `Ok(())`.
198    pub fn set_global_tracker<T>(tracker: T) -> Result<(), SetTrackerError>
199    where
200        T: AllocationTracker + Send + Sync + 'static,
201    {
202        if GLOBAL_INIT
203            .compare_exchange(
204                UNINITIALIZED,
205                INITIALIZING,
206                Ordering::AcqRel,
207                Ordering::Relaxed,
208            )
209            .is_ok()
210        {
211            unsafe {
212                GLOBAL_TRACKER = Some(Tracker::from_allocation_tracker(tracker));
213            }
214            GLOBAL_INIT.store(INITIALIZED, Ordering::Release);
215            Ok(())
216        } else {
217            Err(SetTrackerError { _sealed: () })
218        }
219    }
220
221    /// Runs the given closure without tracking allocations or deallocations.
222    ///
223    /// Inevitably, users of this crate will need to allocate storage for the actual data being tracked. While
224    /// `AllocationTracker::allocated` and `AllocationTracker::deallocated` already avoid reentrantly tracking
225    /// allocations, this method provides a way to do so outside of the tracker implementation.
226    pub fn untracked<F, R>(f: F) -> R
227    where
228        F: FnOnce() -> R,
229    {
230        with_suspended_allocation_group(f)
231    }
232
233    /// Clears the global tracker.
234    ///
235    /// # Safety
236    ///
237    /// Well, there is none.  It's not safe.  This method clears the static reference to the
238    /// tracker, which means we're violating the central assumption that a reference with a
239    /// `'static` lifetime is valid for the lifetime of the process.
240    ///
241    /// All of this said, you're looking at the code comments for a function that is intended to be
242    /// hidden from the docs, so here's where this function may be useful: in tests.
243    ///
244    /// If you can ensure that only one thread is running, thus ensuring there will be no competing
245    /// concurrent accesses, then this is safe.  Also, of course, this leaks whatever allocation
246    /// tracker was set before. Likely not a problem in tests, but for posterity's sake..
247    ///
248    /// YOU'VE BEEN WARNED. :)
249    #[doc(hidden)]
250    pub unsafe fn clear_global_tracker() {
251        GLOBAL_INIT.store(INITIALIZING, Ordering::Release);
252        GLOBAL_TRACKER = None;
253        GLOBAL_INIT.store(UNINITIALIZED, Ordering::Release);
254    }
255}
256
257#[inline(always)]
258fn get_global_tracker() -> Option<&'static Tracker> {
259    // If tracking isn't enabled, then there's no point returning the tracker.
260    if !TRACKING_ENABLED.load(Ordering::Relaxed) {
261        return None;
262    }
263
264    // Tracker has to actually be installed.
265    if GLOBAL_INIT.load(Ordering::Acquire) != INITIALIZED {
266        return None;
267    }
268
269    unsafe {
270        let tracker = GLOBAL_TRACKER
271            .as_ref()
272            .expect("global tracked marked as initialized, but failed to unwrap");
273        Some(tracker)
274    }
275}