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}