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}