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}