Skip to main content

mod_alloc/
lib.rs

1//! # mod-alloc
2//!
3//! Allocation profiling for Rust. Tracks allocation counts, total
4//! bytes, peak resident memory, and current resident memory by
5//! wrapping the system allocator via [`GlobalAlloc`].
6//!
7//! Designed as a lean replacement for `dhat` with MSRV 1.75 and
8//! zero external dependencies on the hot path.
9//!
10//! ## Installing as the global allocator
11//!
12//! ```no_run
13//! use mod_alloc::{ModAlloc, Profiler};
14//!
15//! #[global_allocator]
16//! static GLOBAL: ModAlloc = ModAlloc::new();
17//!
18//! fn main() {
19//!     let p = Profiler::start();
20//!
21//!     let v: Vec<u64> = (0..1000).collect();
22//!     drop(v);
23//!
24//!     let stats = p.stop();
25//!     println!("Allocations: {}", stats.alloc_count);
26//!     println!("Total bytes: {}", stats.total_bytes);
27//!     println!("Peak bytes (absolute): {}", stats.peak_bytes);
28//! }
29//! ```
30//!
31//! ## Counter semantics
32//!
33//! The four Tier 1 counters track allocator activity since the
34//! installed [`ModAlloc`] began counting (or since the last
35//! [`ModAlloc::reset`] call):
36//!
37//! | Counter         | Updated on `alloc`            | Updated on `dealloc` |
38//! |-----------------|-------------------------------|----------------------|
39//! | `alloc_count`   | `+= 1`                        | (unchanged)          |
40//! | `total_bytes`   | `+= size`                     | (unchanged)          |
41//! | `current_bytes` | `+= size`                     | `-= size`            |
42//! | `peak_bytes`    | high-water mark of `current`  | (unchanged)          |
43//!
44//! `realloc` is counted as one allocation event. `total_bytes`
45//! increases by the growth delta on a growing realloc and is
46//! unchanged on a shrinking realloc.
47//!
48//! ## Status
49//!
50//! v0.9.1 adds Tier 2 (inline backtrace capture) behind the
51//! `backtraces` feature. v0.9.2 adds symbolication behind the
52//! `symbolicate` feature. v0.9.3 wires up `dhat-compat` to emit
53//! the per-call-site report as DHAT-format JSON that the upstream
54//! `dh_view.html` viewer loads directly. Default builds still
55//! ship Tier 1 counters only.
56//!
57//! ## Backtraces (`backtraces` feature)
58//!
59//! With `mod-alloc = { version = "0.9", features = ["backtraces"] }`
60//! and `RUSTFLAGS="-C force-frame-pointers=yes"`, each tracked
61//! allocation captures up to 8 frames of its call site via inline
62//! frame-pointer walking on `x86_64` and `aarch64`. Per-call-site
63//! aggregation is exposed via `ModAlloc::call_sites` (available
64//! only with the `backtraces` feature); the result is raw return
65//! addresses. Symbolication ships in v0.9.2.
66//!
67//! Aggregation-table size is controlled by the `MOD_ALLOC_BUCKETS`
68//! environment variable at process start (default 4,096 buckets,
69//! ~384 KB).
70
71#![cfg_attr(docsrs, feature(doc_cfg))]
72#![warn(missing_docs)]
73#![warn(rust_2018_idioms)]
74
75#[cfg(feature = "backtraces")]
76mod backtrace;
77
78#[cfg(feature = "backtraces")]
79pub use backtrace::CallSiteStats;
80
81#[cfg(feature = "symbolicate")]
82mod symbolicate;
83
84#[cfg(feature = "symbolicate")]
85pub use symbolicate::{SymbolicatedCallSite, SymbolicatedFrame};
86
87#[cfg(feature = "dhat-compat")]
88mod dhat_json;
89
90#[cfg(feature = "dhat-compat")]
91pub mod dhat_compat;
92
93use std::alloc::{GlobalAlloc, Layout, System};
94use std::cell::Cell;
95use std::ptr;
96use std::sync::atomic::{AtomicPtr, AtomicU64, Ordering};
97
98// Process-wide handle to the installed `ModAlloc`. Populated lazily
99// on the first non-reentrant alloc call. `Profiler` reads from this
100// to locate the canonical counters without requiring an explicit
101// registration call from the user.
102static GLOBAL_HANDLE: AtomicPtr<ModAlloc> = AtomicPtr::new(ptr::null_mut());
103
104thread_local! {
105    // Reentrancy flag. Set while inside the tracking path; if any
106    // allocation occurs while set, the recursive call bypasses
107    // tracking and forwards directly to the System allocator.
108    //
109    // `const` initialization (stable since 1.59) avoids any lazy
110    // construction allocation inside the TLS access path.
111    static IN_ALLOC: Cell<bool> = const { Cell::new(false) };
112}
113
114// RAII guard for the reentrancy flag. `enter` returns `None` if the
115// current thread is already inside a tracked allocation (caller
116// must skip counter updates) or if TLS is unavailable (e.g. during
117// thread teardown). The guard clears the flag on drop.
118struct ReentryGuard;
119
120impl ReentryGuard {
121    fn enter() -> Option<Self> {
122        IN_ALLOC
123            .try_with(|flag| {
124                if flag.get() {
125                    None
126                } else {
127                    flag.set(true);
128                    Some(ReentryGuard)
129                }
130            })
131            .ok()
132            .flatten()
133    }
134}
135
136impl Drop for ReentryGuard {
137    fn drop(&mut self) {
138        let _ = IN_ALLOC.try_with(|flag| flag.set(false));
139    }
140}
141
142/// Global allocator wrapper that tracks allocations.
143///
144/// Install as `#[global_allocator]` to enable tracking. The wrapper
145/// forwards every allocation, deallocation, reallocation, and
146/// zero-initialised allocation to [`std::alloc::System`] and records
147/// the event in four lock-free [`AtomicU64`] counters.
148///
149/// # Example
150///
151/// ```no_run
152/// use mod_alloc::ModAlloc;
153///
154/// #[global_allocator]
155/// static GLOBAL: ModAlloc = ModAlloc::new();
156///
157/// fn main() {
158///     let v: Vec<u8> = vec![0; 1024];
159///     let stats = GLOBAL.snapshot();
160///     assert!(stats.alloc_count >= 1);
161///     drop(v);
162/// }
163/// ```
164pub struct ModAlloc {
165    alloc_count: AtomicU64,
166    total_bytes: AtomicU64,
167    peak_bytes: AtomicU64,
168    current_bytes: AtomicU64,
169    live_count: AtomicU64,
170    peak_live_count: AtomicU64,
171}
172
173impl ModAlloc {
174    /// Construct a new `ModAlloc` allocator wrapper.
175    ///
176    /// All counters start at zero. This function is `const`, which
177    /// allows construction in a `static` for use as
178    /// `#[global_allocator]`.
179    ///
180    /// # Example
181    ///
182    /// ```
183    /// use mod_alloc::ModAlloc;
184    ///
185    /// static GLOBAL: ModAlloc = ModAlloc::new();
186    /// let stats = GLOBAL.snapshot();
187    /// assert_eq!(stats.alloc_count, 0);
188    /// ```
189    pub const fn new() -> Self {
190        Self {
191            alloc_count: AtomicU64::new(0),
192            total_bytes: AtomicU64::new(0),
193            peak_bytes: AtomicU64::new(0),
194            current_bytes: AtomicU64::new(0),
195            live_count: AtomicU64::new(0),
196            peak_live_count: AtomicU64::new(0),
197        }
198    }
199
200    /// Snapshot the current counter values.
201    ///
202    /// Each counter is read independently with `Relaxed` ordering;
203    /// the resulting [`AllocStats`] is a coherent best-effort view
204    /// but does not represent a single atomic moment in time. For
205    /// scoped measurement, prefer [`Profiler`].
206    ///
207    /// # Example
208    ///
209    /// ```
210    /// use mod_alloc::ModAlloc;
211    ///
212    /// let alloc = ModAlloc::new();
213    /// let stats = alloc.snapshot();
214    /// assert_eq!(stats.alloc_count, 0);
215    /// ```
216    pub fn snapshot(&self) -> AllocStats {
217        AllocStats {
218            alloc_count: self.alloc_count.load(Ordering::Relaxed),
219            total_bytes: self.total_bytes.load(Ordering::Relaxed),
220            peak_bytes: self.peak_bytes.load(Ordering::Relaxed),
221            current_bytes: self.current_bytes.load(Ordering::Relaxed),
222            live_count: self.live_count.load(Ordering::Relaxed),
223            peak_live_count: self.peak_live_count.load(Ordering::Relaxed),
224        }
225    }
226
227    /// Reset all counters to zero.
228    ///
229    /// Intended for use at the start of a profile run, before any
230    /// outstanding allocations exist. Calling `reset` while
231    /// allocations are live can cause `current_bytes` to wrap on
232    /// subsequent deallocations; the other counters are unaffected.
233    ///
234    /// # Example
235    ///
236    /// ```
237    /// use mod_alloc::ModAlloc;
238    ///
239    /// let alloc = ModAlloc::new();
240    /// alloc.reset();
241    /// let stats = alloc.snapshot();
242    /// assert_eq!(stats.alloc_count, 0);
243    /// ```
244    pub fn reset(&self) {
245        self.alloc_count.store(0, Ordering::Relaxed);
246        self.total_bytes.store(0, Ordering::Relaxed);
247        self.peak_bytes.store(0, Ordering::Relaxed);
248        self.current_bytes.store(0, Ordering::Relaxed);
249        self.live_count.store(0, Ordering::Relaxed);
250        self.peak_live_count.store(0, Ordering::Relaxed);
251    }
252
253    #[inline]
254    fn record_alloc(&self, size: u64) {
255        self.alloc_count.fetch_add(1, Ordering::Relaxed);
256        self.total_bytes.fetch_add(size, Ordering::Relaxed);
257        let new_current = self.current_bytes.fetch_add(size, Ordering::Relaxed) + size;
258        self.peak_bytes.fetch_max(new_current, Ordering::Relaxed);
259        let new_live = self.live_count.fetch_add(1, Ordering::Relaxed) + 1;
260        self.peak_live_count.fetch_max(new_live, Ordering::Relaxed);
261    }
262
263    #[inline]
264    fn record_dealloc(&self, size: u64) {
265        self.current_bytes.fetch_sub(size, Ordering::Relaxed);
266        self.live_count.fetch_sub(1, Ordering::Relaxed);
267    }
268
269    #[inline]
270    fn record_realloc(&self, old_size: u64, new_size: u64) {
271        self.alloc_count.fetch_add(1, Ordering::Relaxed);
272        if new_size > old_size {
273            let delta = new_size - old_size;
274            self.total_bytes.fetch_add(delta, Ordering::Relaxed);
275            let new_current = self.current_bytes.fetch_add(delta, Ordering::Relaxed) + delta;
276            self.peak_bytes.fetch_max(new_current, Ordering::Relaxed);
277        } else if new_size < old_size {
278            self.current_bytes
279                .fetch_sub(old_size - new_size, Ordering::Relaxed);
280        }
281    }
282
283    #[inline]
284    fn register_self(&self) {
285        if GLOBAL_HANDLE.load(Ordering::Relaxed).is_null() {
286            let _ = GLOBAL_HANDLE.compare_exchange(
287                ptr::null_mut(),
288                self as *const ModAlloc as *mut ModAlloc,
289                Ordering::Release,
290                Ordering::Relaxed,
291            );
292        }
293    }
294
295    /// Drain the per-call-site aggregation table into a `Vec`.
296    ///
297    /// Available only with the `backtraces` cargo feature. The
298    /// returned vector contains one [`CallSiteStats`] per unique
299    /// call site observed since the table was first written. Each
300    /// row carries up to 8 raw return addresses (top of stack
301    /// first), the number of allocations attributed to that site,
302    /// and the total bytes.
303    ///
304    /// Symbolication (resolving addresses to function names)
305    /// lands in `v0.9.2`. This method exposes raw addresses only.
306    ///
307    /// # Example
308    ///
309    /// ```no_run
310    /// # #[cfg(feature = "backtraces")]
311    /// # fn demo() {
312    /// use mod_alloc::ModAlloc;
313    ///
314    /// #[global_allocator]
315    /// static GLOBAL: ModAlloc = ModAlloc::new();
316    ///
317    /// let _v: Vec<u8> = vec![0; 1024];
318    /// for site in GLOBAL.call_sites() {
319    ///     println!("{} allocs, {} bytes at {:#x}",
320    ///         site.count, site.total_bytes, site.frames[0]);
321    /// }
322    /// # }
323    /// ```
324    #[cfg(feature = "backtraces")]
325    pub fn call_sites(&self) -> Vec<CallSiteStats> {
326        backtrace::call_sites_report()
327    }
328
329    /// Drain the per-call-site table and symbolicate each frame
330    /// against the running binary's own debug info.
331    ///
332    /// Available only with the `symbolicate` cargo feature, which
333    /// also implies `backtraces`. Returns one
334    /// [`SymbolicatedCallSite`] per unique call site, each
335    /// carrying resolved function names plus (where available)
336    /// source file and line.
337    ///
338    /// Allocates. Safe to call from non-allocator contexts only
339    /// (ordinary user code outside the global-allocator hook).
340    ///
341    /// Results are cached per-address across calls.
342    ///
343    /// # Example
344    ///
345    /// ```no_run
346    /// # #[cfg(feature = "symbolicate")]
347    /// # fn demo() {
348    /// use mod_alloc::ModAlloc;
349    ///
350    /// #[global_allocator]
351    /// static GLOBAL: ModAlloc = ModAlloc::new();
352    ///
353    /// let _v: Vec<u8> = vec![0; 1024];
354    /// for site in GLOBAL.symbolicated_report() {
355    ///     let top = &site.frames[0];
356    ///     println!("{} allocs / {} bytes at {}",
357    ///         site.count,
358    ///         site.total_bytes,
359    ///         top.function.as_deref().unwrap_or("<unresolved>"));
360    /// }
361    /// # }
362    /// ```
363    #[cfg(feature = "symbolicate")]
364    pub fn symbolicated_report(&self) -> Vec<SymbolicatedCallSite> {
365        symbolicate::symbolicated_report()
366    }
367
368    /// Render the per-call-site report as a DHAT-compatible JSON
369    /// string.
370    ///
371    /// Available only with the `dhat-compat` cargo feature (which
372    /// implies `backtraces`). When the `symbolicate` feature is
373    /// also active, frame strings carry function names and
374    /// (where available) source file and line; otherwise they
375    /// carry raw hex addresses.
376    ///
377    /// Allocates. Safe to call from non-allocator contexts only
378    /// (ordinary user code outside the global-allocator hook).
379    ///
380    /// The output schema (`dhatFileVersion: 2`, `mode: "rust-heap"`)
381    /// matches the format consumed by the upstream
382    /// `dh_view.html` viewer shipped with Valgrind.
383    ///
384    /// # Example
385    ///
386    /// ```no_run
387    /// # #[cfg(feature = "dhat-compat")]
388    /// # fn demo() {
389    /// use mod_alloc::ModAlloc;
390    ///
391    /// #[global_allocator]
392    /// static GLOBAL: ModAlloc = ModAlloc::new();
393    ///
394    /// let _v: Vec<u8> = vec![0; 1024];
395    /// let json = GLOBAL.dhat_json_string();
396    /// assert!(json.contains("\"dhatFileVersion\":2"));
397    /// # }
398    /// ```
399    #[cfg(feature = "dhat-compat")]
400    pub fn dhat_json_string(&self) -> String {
401        dhat_json::dhat_json_string()
402    }
403
404    /// Render the per-call-site report and write it to `path` as
405    /// DHAT-compatible JSON.
406    ///
407    /// Available only with the `dhat-compat` cargo feature.
408    /// Mirrors `dhat-rs`'s convention of writing
409    /// `dhat-heap.json` to the current directory; pass that path
410    /// to drop a file the upstream viewer will load directly.
411    ///
412    /// Allocates. Safe to call from non-allocator contexts only.
413    ///
414    /// # Example
415    ///
416    /// ```no_run
417    /// # #[cfg(feature = "dhat-compat")]
418    /// # fn demo() -> std::io::Result<()> {
419    /// use mod_alloc::ModAlloc;
420    ///
421    /// #[global_allocator]
422    /// static GLOBAL: ModAlloc = ModAlloc::new();
423    ///
424    /// let _v: Vec<u8> = vec![0; 1024];
425    /// GLOBAL.write_dhat_json("dhat-heap.json")?;
426    /// # Ok(())
427    /// # }
428    /// ```
429    #[cfg(feature = "dhat-compat")]
430    pub fn write_dhat_json<P: AsRef<std::path::Path>>(&self, path: P) -> std::io::Result<()> {
431        dhat_json::write_dhat_json(path.as_ref())
432    }
433}
434
435impl Default for ModAlloc {
436    fn default() -> Self {
437        Self::new()
438    }
439}
440
441// SAFETY: `ModAlloc` adds counter bookkeeping but performs all
442// underlying allocation through [`std::alloc::System`]. Each method
443// forwards its arguments unchanged to `System` and only inspects
444// the result; size/alignment invariants required by the
445// `GlobalAlloc` contract are passed through unmodified, so the
446// caller's contract to us becomes our contract to System.
447//
448// The counter-update path uses thread-local reentrancy detection
449// (see `ReentryGuard`) so that any allocation triggered transitively
450// inside the tracking path bypasses tracking and forwards directly
451// to System, preserving the "hook MUST NOT itself allocate"
452// invariant from REPS section 4.
453unsafe impl GlobalAlloc for ModAlloc {
454    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
455        // SAFETY: per `GlobalAlloc::alloc`, `layout` has non-zero
456        // size; we forward unchanged to `System.alloc`, which has
457        // the same contract.
458        let ptr = unsafe { System.alloc(layout) };
459        if !ptr.is_null() {
460            if let Some(_g) = ReentryGuard::enter() {
461                let size = layout.size() as u64;
462                self.record_alloc(size);
463                self.register_self();
464                #[cfg(feature = "backtraces")]
465                backtrace::record_event(size);
466            }
467        }
468        ptr
469    }
470
471    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
472        // SAFETY: same invariants as `alloc`; `layout` forwarded
473        // unchanged. `System.alloc_zeroed` zero-fills the returned
474        // memory, satisfying the `GlobalAlloc::alloc_zeroed`
475        // contract.
476        let ptr = unsafe { System.alloc_zeroed(layout) };
477        if !ptr.is_null() {
478            if let Some(_g) = ReentryGuard::enter() {
479                let size = layout.size() as u64;
480                self.record_alloc(size);
481                self.register_self();
482                #[cfg(feature = "backtraces")]
483                backtrace::record_event(size);
484            }
485        }
486        ptr
487    }
488
489    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
490        // SAFETY: per `GlobalAlloc::dealloc`, `ptr` was returned by a
491        // prior call to `alloc`/`alloc_zeroed`/`realloc` on this
492        // allocator with the given `layout`; we forwarded all of
493        // those to `System` with the same `layout`, so the inverse
494        // pairing for `System.dealloc(ptr, layout)` is valid.
495        unsafe { System.dealloc(ptr, layout) };
496        if let Some(_g) = ReentryGuard::enter() {
497            self.record_dealloc(layout.size() as u64);
498        }
499    }
500
501    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
502        // SAFETY: per `GlobalAlloc::realloc`, `ptr` was returned by
503        // a prior allocation with `layout`, `new_size` is non-zero,
504        // and the alignment in `layout` remains valid for the new
505        // size. We forward all three to `System.realloc` which has
506        // the same contract.
507        let new_ptr = unsafe { System.realloc(ptr, layout, new_size) };
508        if !new_ptr.is_null() {
509            if let Some(_g) = ReentryGuard::enter() {
510                self.record_realloc(layout.size() as u64, new_size as u64);
511                self.register_self();
512                // Per dhat semantics: realloc records as one event
513                // attributed to `new_size` (including shrinks).
514                #[cfg(feature = "backtraces")]
515                backtrace::record_event(new_size as u64);
516            }
517        }
518        new_ptr
519    }
520}
521
522/// Snapshot of allocation statistics at a point in time.
523///
524/// Produced by [`ModAlloc::snapshot`] and [`Profiler::stop`].
525///
526/// # Example
527///
528/// ```
529/// use mod_alloc::AllocStats;
530///
531/// let stats = AllocStats {
532///     alloc_count: 10,
533///     total_bytes: 1024,
534///     peak_bytes: 512,
535///     current_bytes: 256,
536///     live_count: 4,
537///     peak_live_count: 7,
538/// };
539/// assert_eq!(stats.alloc_count, 10);
540/// ```
541///
542/// # Version note
543///
544/// `live_count` and `peak_live_count` were added in v0.9.4 to
545/// support dhat-rs's `HeapStats` shape via the `dhat-compat`
546/// feature. Callers constructing `AllocStats` via struct literal
547/// must initialise the new fields; callers that only consume
548/// `AllocStats` via [`ModAlloc::snapshot`] or
549/// [`Profiler::stop`] are unaffected.
550#[derive(Debug, Clone, Copy, PartialEq, Eq)]
551pub struct AllocStats {
552    /// Number of allocations performed.
553    pub alloc_count: u64,
554    /// Total bytes allocated across all allocations. Reallocations
555    /// contribute the growth delta (or zero on shrink).
556    pub total_bytes: u64,
557    /// Peak resident bytes (highest `current_bytes` ever observed).
558    pub peak_bytes: u64,
559    /// Currently-allocated bytes (allocations minus deallocations).
560    pub current_bytes: u64,
561    /// Currently-alive allocation count (allocations minus
562    /// deallocations). Mirrors `dhat::HeapStats::curr_blocks`.
563    pub live_count: u64,
564    /// Peak live allocation count (highest `live_count` ever
565    /// observed). Mirrors `dhat::HeapStats::max_blocks`.
566    pub peak_live_count: u64,
567}
568
569/// Scoped profiler that captures a delta between start and stop.
570///
571/// Read the snapshot of the installed [`ModAlloc`] on construction
572/// and again on [`Profiler::stop`], returning the difference. If no
573/// `ModAlloc` is installed as `#[global_allocator]` and no
574/// allocation has occurred through it yet, both snapshots are
575/// zero and the delta is zero.
576///
577/// # Example
578///
579/// ```no_run
580/// use mod_alloc::{ModAlloc, Profiler};
581///
582/// #[global_allocator]
583/// static GLOBAL: ModAlloc = ModAlloc::new();
584///
585/// fn main() {
586///     let p = Profiler::start();
587///     let v: Vec<u8> = vec![0; 1024];
588///     drop(v);
589///     let stats = p.stop();
590///     println!("Captured {} alloc events", stats.alloc_count);
591/// }
592/// ```
593pub struct Profiler {
594    baseline: AllocStats,
595}
596
597impl Profiler {
598    /// Begin profiling, capturing the current allocation state.
599    ///
600    /// If no `ModAlloc` is installed as `#[global_allocator]` or no
601    /// allocation has occurred yet, the captured baseline is all
602    /// zeros.
603    ///
604    /// # Example
605    ///
606    /// ```
607    /// use mod_alloc::Profiler;
608    ///
609    /// let p = Profiler::start();
610    /// let _delta = p.stop();
611    /// ```
612    pub fn start() -> Self {
613        Self {
614            baseline: current_snapshot_or_zeros(),
615        }
616    }
617
618    /// Stop profiling and return the delta from start.
619    ///
620    /// `alloc_count`, `total_bytes`, and `current_bytes` are deltas
621    /// from `start()` to `stop()`. `peak_bytes` is the absolute
622    /// high-water mark observed during the profiling window (peak
623    /// has no meaningful delta semantic).
624    ///
625    /// # Example
626    ///
627    /// ```
628    /// use mod_alloc::Profiler;
629    ///
630    /// let p = Profiler::start();
631    /// let stats = p.stop();
632    /// assert_eq!(stats.alloc_count, 0);
633    /// ```
634    pub fn stop(self) -> AllocStats {
635        let now = current_snapshot_or_zeros();
636        AllocStats {
637            alloc_count: now.alloc_count.saturating_sub(self.baseline.alloc_count),
638            total_bytes: now.total_bytes.saturating_sub(self.baseline.total_bytes),
639            current_bytes: now
640                .current_bytes
641                .saturating_sub(self.baseline.current_bytes),
642            peak_bytes: now.peak_bytes,
643            live_count: now.live_count.saturating_sub(self.baseline.live_count),
644            peak_live_count: now.peak_live_count,
645        }
646    }
647}
648
649pub(crate) fn current_snapshot_or_zeros() -> AllocStats {
650    let p = GLOBAL_HANDLE.load(Ordering::Acquire);
651    if p.is_null() {
652        AllocStats {
653            alloc_count: 0,
654            total_bytes: 0,
655            peak_bytes: 0,
656            current_bytes: 0,
657            live_count: 0,
658            peak_live_count: 0,
659        }
660    } else {
661        // SAFETY: `GLOBAL_HANDLE` is only ever set by
662        // `ModAlloc::register_self` to point at the address of a
663        // `#[global_allocator] static` (or any other `'static`
664        // `ModAlloc`). That target has `'static` lifetime, so the
665        // pointer remains valid for the remainder of the program.
666        // We produce only a shared borrow used to call `&self`
667        // methods that read atomic counters; no mutation through
668        // the pointer occurs here.
669        unsafe { (*p).snapshot() }
670    }
671}
672
673#[cfg(test)]
674mod tests {
675    use super::*;
676
677    #[test]
678    fn allocator_constructs() {
679        let _ = ModAlloc::new();
680    }
681
682    #[test]
683    fn snapshot_returns_zeros_initially() {
684        let a = ModAlloc::new();
685        let s = a.snapshot();
686        assert_eq!(s.alloc_count, 0);
687        assert_eq!(s.total_bytes, 0);
688        assert_eq!(s.peak_bytes, 0);
689        assert_eq!(s.current_bytes, 0);
690    }
691
692    #[test]
693    fn reset_works() {
694        let a = ModAlloc::new();
695        a.reset();
696        let s = a.snapshot();
697        assert_eq!(s.alloc_count, 0);
698    }
699
700    #[test]
701    fn record_alloc_updates_counters() {
702        let a = ModAlloc::new();
703        a.record_alloc(128);
704        a.record_alloc(256);
705        let s = a.snapshot();
706        assert_eq!(s.alloc_count, 2);
707        assert_eq!(s.total_bytes, 384);
708        assert_eq!(s.current_bytes, 384);
709        assert_eq!(s.peak_bytes, 384);
710    }
711
712    #[test]
713    fn record_dealloc_decreases_current_only() {
714        let a = ModAlloc::new();
715        a.record_alloc(1000);
716        a.record_dealloc(400);
717        let s = a.snapshot();
718        assert_eq!(s.alloc_count, 1);
719        assert_eq!(s.total_bytes, 1000);
720        assert_eq!(s.current_bytes, 600);
721        assert_eq!(s.peak_bytes, 1000);
722        // live_count tracks the *count* of alive allocations, not
723        // bytes — a single alloc + a single (partial) dealloc
724        // event leaves zero alive allocations from the counter's
725        // perspective even though current_bytes is non-zero.
726        // This mirrors dhat's curr_blocks semantics where every
727        // alloc/dealloc pair is a 1:1 block lifecycle.
728        assert_eq!(s.live_count, 0);
729        assert_eq!(s.peak_live_count, 1);
730    }
731
732    #[test]
733    fn live_counters_track_alive_blocks() {
734        let a = ModAlloc::new();
735        a.record_alloc(100);
736        a.record_alloc(200);
737        a.record_alloc(300);
738        let s = a.snapshot();
739        assert_eq!(s.live_count, 3);
740        assert_eq!(s.peak_live_count, 3);
741
742        a.record_dealloc(100);
743        a.record_dealloc(200);
744        let s = a.snapshot();
745        assert_eq!(s.live_count, 1);
746        assert_eq!(s.peak_live_count, 3, "peak holds high-water mark");
747    }
748
749    #[test]
750    fn record_realloc_does_not_touch_live_count() {
751        let a = ModAlloc::new();
752        a.record_alloc(100);
753        let pre = a.snapshot();
754        a.record_realloc(100, 250);
755        let post = a.snapshot();
756        assert_eq!(
757            pre.live_count, post.live_count,
758            "realloc keeps the same block, live_count unchanged"
759        );
760    }
761
762    #[test]
763    fn record_realloc_growth_updates_total_and_peak() {
764        let a = ModAlloc::new();
765        a.record_alloc(100);
766        a.record_realloc(100, 250);
767        let s = a.snapshot();
768        assert_eq!(s.alloc_count, 2);
769        assert_eq!(s.total_bytes, 250);
770        assert_eq!(s.current_bytes, 250);
771        assert_eq!(s.peak_bytes, 250);
772    }
773
774    #[test]
775    fn record_realloc_shrink_only_adjusts_current() {
776        let a = ModAlloc::new();
777        a.record_alloc(500);
778        a.record_realloc(500, 200);
779        let s = a.snapshot();
780        assert_eq!(s.alloc_count, 2);
781        assert_eq!(s.total_bytes, 500);
782        assert_eq!(s.current_bytes, 200);
783        assert_eq!(s.peak_bytes, 500);
784    }
785
786    #[test]
787    fn peak_holds_high_water_mark() {
788        let a = ModAlloc::new();
789        a.record_alloc(1000);
790        a.record_dealloc(1000);
791        a.record_alloc(500);
792        let s = a.snapshot();
793        assert_eq!(s.peak_bytes, 1000);
794        assert_eq!(s.current_bytes, 500);
795    }
796
797    #[test]
798    fn reentry_guard_blocks_nested_entry() {
799        let outer = ReentryGuard::enter();
800        assert!(outer.is_some());
801        let inner = ReentryGuard::enter();
802        assert!(inner.is_none(), "nested entry must be denied");
803        drop(outer);
804        let after = ReentryGuard::enter();
805        assert!(after.is_some(), "entry must be allowed after outer drops");
806    }
807
808    #[test]
809    fn profiler_start_stop_with_no_handle() {
810        let p = Profiler::start();
811        let s = p.stop();
812        assert_eq!(s.alloc_count, 0);
813    }
814}