Skip to main content

mod_alloc/dhat_compat/
profiler.rs

1//! dhat-rs-shaped `Profiler` / `ProfilerBuilder`.
2//!
3//! Drop semantics match dhat-rs: a `Profiler` constructed via
4//! `new_heap()` (or `builder().build()`) writes its JSON report
5//! when dropped, unless built with `.testing()`.
6
7use std::path::{Path, PathBuf};
8use std::sync::atomic::{AtomicBool, Ordering};
9
10/// Profiler mode — heap-allocation tracking or ad-hoc event
11/// counting. Pick at construction via [`Profiler::new_heap`] /
12/// [`Profiler::new_ad_hoc`] or via [`ProfilerBuilder`].
13///
14/// # Stability
15///
16/// Marked `#[non_exhaustive]` as of v1.0.0. Future minor
17/// versions may add new modes (e.g. event-stream output)
18/// without bumping the major version.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum Mode {
22    /// Heap allocation profiling (default).
23    Heap,
24    /// Ad-hoc event profiling.
25    AdHoc,
26}
27
28#[derive(Clone, Debug)]
29struct Config {
30    mode: Mode,
31    file_name: Option<PathBuf>,
32    testing: bool,
33    #[allow(dead_code)]
34    trim_backtraces: Option<usize>,
35}
36
37impl Default for Config {
38    fn default() -> Self {
39        Self {
40            mode: Mode::Heap,
41            file_name: None,
42            testing: false,
43            trim_backtraces: None,
44        }
45    }
46}
47
48/// RAII handle that writes a DHAT-format JSON report on drop.
49///
50/// Drop-in-shaped replacement for `dhat::Profiler`. Hold the
51/// returned value in `main` (or wherever you want the file to
52/// land) and let scope exit trigger the write.
53///
54/// # Example
55///
56/// ```no_run
57/// # #[cfg(feature = "dhat-compat")]
58/// # fn demo() {
59/// use mod_alloc::dhat_compat::{Alloc, Profiler};
60///
61/// #[global_allocator]
62/// static ALLOC: Alloc = Alloc;
63///
64/// fn main() {
65///     let _profiler = Profiler::new_heap();
66///     let _v: Vec<u8> = vec![0; 1024];
67///     // _profiler drops here → writes dhat-heap.json
68/// }
69/// # }
70/// ```
71pub struct Profiler {
72    config: Config,
73}
74
75impl Profiler {
76    /// Construct a heap-mode profiler. Writes `dhat-heap.json` on
77    /// drop unless `builder().file_name(...)` was used.
78    pub fn new_heap() -> Self {
79        Self::install(Config {
80            mode: Mode::Heap,
81            ..Config::default()
82        })
83    }
84
85    /// Construct an ad-hoc-mode profiler. Writes
86    /// `dhat-ad-hoc.json` on drop.
87    pub fn new_ad_hoc() -> Self {
88        Self::install(Config {
89            mode: Mode::AdHoc,
90            ..Config::default()
91        })
92    }
93
94    /// Start a builder for fine-grained configuration.
95    pub fn builder() -> ProfilerBuilder {
96        ProfilerBuilder::new()
97    }
98
99    fn install(config: Config) -> Self {
100        // Best-effort single-Profiler guard. We do not panic on
101        // re-entry (dhat-rs does) because that surprise is hard to
102        // recover from in downstream test harnesses; document
103        // "last writer wins" instead.
104        PROFILER_ACTIVE.store(true, Ordering::Release);
105        Self { config }
106    }
107}
108
109static PROFILER_ACTIVE: AtomicBool = AtomicBool::new(false);
110
111impl Drop for Profiler {
112    fn drop(&mut self) {
113        PROFILER_ACTIVE.store(false, Ordering::Release);
114
115        if self.config.testing {
116            return;
117        }
118
119        let path = self.config.file_name.clone().unwrap_or_else(|| {
120            PathBuf::from(match self.config.mode {
121                Mode::Heap => "dhat-heap.json",
122                Mode::AdHoc => "dhat-ad-hoc.json",
123            })
124        });
125
126        match self.config.mode {
127            Mode::Heap => {
128                // Errors are intentionally swallowed (matches
129                // dhat-rs). Drop cannot propagate `?`, and a
130                // failed write of a profile report shouldn't
131                // abort the process at scope exit.
132                let _ = crate::dhat_json::write_dhat_json(&path);
133            }
134            Mode::AdHoc => {
135                let _ = super::ad_hoc_writer::write_ad_hoc(&path);
136            }
137        }
138    }
139}
140
141/// Builder for [`Profiler`].
142///
143/// Mirrors `dhat::ProfilerBuilder` method-for-method. Obtain via
144/// [`Profiler::builder`].
145#[derive(Debug)]
146pub struct ProfilerBuilder {
147    config: Config,
148}
149
150impl ProfilerBuilder {
151    fn new() -> Self {
152        Self {
153            config: Config::default(),
154        }
155    }
156
157    /// Switch the profiler to ad-hoc mode.
158    pub fn ad_hoc(mut self) -> Self {
159        self.config.mode = Mode::AdHoc;
160        self
161    }
162
163    /// Build in testing mode — suppresses the drop-time file
164    /// write. Use for tests that snapshot stats directly without
165    /// littering the workspace with `dhat-heap.json`.
166    pub fn testing(mut self) -> Self {
167        self.config.testing = true;
168        self
169    }
170
171    /// Override the output file name. Default is
172    /// `dhat-heap.json` (or `dhat-ad-hoc.json` in ad-hoc mode).
173    pub fn file_name<P: AsRef<Path>>(mut self, p: P) -> Self {
174        self.config.file_name = Some(p.as_ref().to_path_buf());
175        self
176    }
177
178    /// Maximum frames to retain per backtrace (`None` = walker
179    /// default).
180    ///
181    /// Accepted for API parity with `dhat::ProfilerBuilder`;
182    /// silently clamped to mod-alloc's walker cap of 8 frames.
183    /// Values above 8 produce up to 8 frames; values below 8
184    /// produce that many frames in the emitted JSON.
185    pub fn trim_backtraces(mut self, max_frames: Option<usize>) -> Self {
186        self.config.trim_backtraces = max_frames;
187        self
188    }
189
190    /// Build the profiler. The returned value writes the report
191    /// on drop.
192    pub fn build(self) -> Profiler {
193        Profiler::install(self.config)
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn builder_defaults_to_heap_mode() {
203        let p = Profiler::builder().build();
204        assert_eq!(p.config.mode, Mode::Heap);
205        // testing-mode skip on drop
206        let _t = Profiler::builder().testing().build();
207        // both drop here; testing-mode skip prevents the second
208        // from writing to CWD.
209    }
210
211    #[test]
212    fn builder_ad_hoc_switches_mode() {
213        let p = Profiler::builder().ad_hoc().testing().build();
214        assert_eq!(p.config.mode, Mode::AdHoc);
215    }
216
217    #[test]
218    fn builder_file_name_overrides_default() {
219        let p = Profiler::builder()
220            .file_name("custom.json")
221            .testing()
222            .build();
223        assert_eq!(
224            p.config.file_name.as_deref(),
225            Some(std::path::Path::new("custom.json"))
226        );
227    }
228}