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}