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