mod_alloc/dhat_compat/mod.rs
1//! dhat-rs-shaped compatibility surface.
2//!
3//! Behind the `dhat-compat` cargo feature. Provides drop-in
4//! replacements for `dhat::Alloc`, `dhat::Profiler`,
5//! `dhat::ProfilerBuilder`, `dhat::HeapStats`, `dhat::AdHocStats`,
6//! and `dhat::ad_hoc_event` so consumers migrating from dhat-rs
7//! can swap allocator profilers with a one-line import change:
8//!
9//! ```no_run
10//! # #[cfg(feature = "dhat-compat")]
11//! # mod swap_example {
12//! use mod_alloc::dhat_compat as dhat;
13//!
14//! #[global_allocator]
15//! static ALLOC: dhat::Alloc = dhat::Alloc;
16//!
17//! fn main() {
18//! let _profiler = dhat::Profiler::new_heap();
19//! // ... work ...
20//! // _profiler drops here → writes dhat-heap.json
21//! }
22//! # }
23//! ```
24//!
25//! ## Differences from dhat-rs
26//!
27//! Documented in `MIGRATING_FROM_DHAT.md`. Summary:
28//!
29//! - Backtrace depth is capped at 8 frames (Tier 2 walker limit);
30//! `ProfilerBuilder::trim_backtraces` is accepted for parity
31//! but silently clamps.
32//! - Drop-time file-write errors are swallowed silently — same
33//! as dhat-rs's behaviour.
34//! - Double-Profiler construction is a no-op rather than a panic
35//! (dhat-rs panics). Last writer wins on the JSON file.
36//! - `dhat::assert!` / `assert_eq!` / `assert_ne!` macros are not
37//! yet shipped. Use `HeapStats::get()` directly in test
38//! assertions.
39
40use std::alloc::{GlobalAlloc, Layout};
41
42mod ad_hoc_writer;
43mod profiler;
44mod stats;
45
46pub use profiler::{Mode, Profiler, ProfilerBuilder};
47pub use stats::{ad_hoc_event, AdHocStats, HeapStats};
48
49/// Drop-in replacement for `dhat::Alloc`.
50///
51/// Unit struct usable in the literal `static A: Alloc = Alloc;`
52/// pattern that dhat-rs documents. Internally forwards every
53/// allocation to a process-wide static [`crate::ModAlloc`] so
54/// `HeapStats::get()` and `Profiler` find the live counters.
55///
56/// # Example
57///
58/// ```no_run
59/// # #[cfg(feature = "dhat-compat")]
60/// # mod ex {
61/// use mod_alloc::dhat_compat::Alloc;
62///
63/// #[global_allocator]
64/// static ALLOC: Alloc = Alloc;
65/// # }
66/// ```
67pub struct Alloc;
68
69impl Alloc {
70 /// Construct an `Alloc` value (also usable as just `Alloc`).
71 ///
72 /// Provided for parity with `ModAlloc::new()` and for
73 /// downstream code that prefers the constructor form. The
74 /// preferred dhat-style pattern is `static A: Alloc = Alloc;`.
75 pub const fn new() -> Self {
76 Self
77 }
78}
79
80impl Default for Alloc {
81 fn default() -> Self {
82 Self
83 }
84}
85
86// Process-wide tracking allocator that all `Alloc` instances
87// delegate to. Hosting it as a `static` (rather than per-`Alloc`)
88// is necessary because `Alloc` itself is a zero-sized type — it
89// has no place to put atomic counters — and because dhat-rs's
90// pattern uses literal `dhat::Alloc` values in `static` position
91// without any constructor call.
92static INNER: crate::ModAlloc = crate::ModAlloc::new();
93
94// SAFETY: `Alloc` is a thin forwarding wrapper around `INNER`,
95// which is a `'static` `ModAlloc`. Every `GlobalAlloc` method
96// forwards its arguments unchanged to `INNER`'s implementation;
97// size and alignment invariants pass through unmodified. `INNER`
98// itself satisfies the `GlobalAlloc` contract (see
99// `crate::ModAlloc`'s `unsafe impl`), so the same contract holds
100// through the forwarder.
101unsafe impl GlobalAlloc for Alloc {
102 // `#[inline(always)]` on each forwarder is important for the
103 // backtrace path: without it, the call chain becomes
104 // `user_code -> Alloc::alloc -> ModAlloc::alloc ->
105 // record_event`, which adds an extra stack frame in front of
106 // the user's call site. Inlining folds `Alloc::alloc` away,
107 // so the captured frame-pointer chain matches the direct
108 // `ModAlloc` usage path and the walker recovers the same
109 // depth of user frames.
110 #[inline(always)]
111 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
112 // SAFETY: `layout` forwarded unchanged to `INNER.alloc`,
113 // which has the same `GlobalAlloc::alloc` contract.
114 unsafe { INNER.alloc(layout) }
115 }
116
117 #[inline(always)]
118 unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
119 // SAFETY: same invariants as `alloc`; forwarded unchanged.
120 unsafe { INNER.alloc_zeroed(layout) }
121 }
122
123 #[inline(always)]
124 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
125 // SAFETY: `ptr` came from a prior call to one of this
126 // type's `alloc` family methods, all of which forwarded
127 // to `INNER`. The `(ptr, layout)` pairing is therefore
128 // valid for `INNER.dealloc`.
129 unsafe { INNER.dealloc(ptr, layout) }
130 }
131
132 #[inline(always)]
133 unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
134 // SAFETY: same reasoning as `dealloc`; the `(ptr,
135 // layout)` pairing is valid for `INNER.realloc`, and
136 // `new_size` and alignment invariants are passed through
137 // unmodified.
138 unsafe { INNER.realloc(ptr, layout, new_size) }
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn alloc_is_zero_sized() {
148 assert_eq!(core::mem::size_of::<Alloc>(), 0);
149 }
150
151 #[test]
152 fn alloc_new_constructs_unit() {
153 let _a = Alloc::new();
154 let _b: Alloc = Alloc;
155 let _c: Alloc = <Alloc as Default>::default();
156 }
157
158 #[test]
159 fn heap_stats_reflects_zero_initial_state_on_uninstalled_path() {
160 // Without anything installed as `#[global_allocator]`,
161 // `HeapStats::get` returns zeros (the GLOBAL_HANDLE
162 // path's null-pointer branch). The integration test
163 // covers the installed path.
164 // We cannot reliably assert zeros here in unit-test
165 // position because the test harness itself uses the
166 // system allocator and may have populated the
167 // GLOBAL_HANDLE from prior tests that exercised
168 // `ModAlloc`. Shape-only smoke check:
169 let s = HeapStats::get();
170 // Field access shapes; no value assertions.
171 let _ = (
172 s.total_blocks,
173 s.total_bytes,
174 s.curr_blocks,
175 s.curr_bytes,
176 s.max_blocks,
177 s.max_bytes,
178 );
179 }
180}