blitz_traits/profiling.rs
1//! Process-wide switch for the intrusive performance collectors.
2//!
3//! Inspection and input control do not need these collectors. They stay dormant
4//! until the embedder explicitly starts a profiling session, so shipping the
5//! capability does not mean continuously paying clocks, locks, maps, or sample
6//! retention.
7
8use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
9
10/// Whether this process is *allowed* to sample, not whether it is sampling.
11///
12/// The distinction is the point. This used to be the activation switch, so a
13/// profile that had been enabled once began collecting at boot and kept
14/// collecting for the life of the process, for a reader that in the ordinary
15/// case never attached: the readers are the inspector and `blitz-bench`, and
16/// both are separate processes. Samples that go nowhere have no value and are
17/// not free.
18///
19/// Permission is now separate from activation. Collection runs only while a
20/// consumer holds a [`DeepProfilingGuard`], and this flag decides whether one
21/// can be taken at all.
22static DEEP_PROFILING_PERMITTED: AtomicBool = AtomicBool::new(false);
23
24/// How many consumers currently want samples.
25///
26/// A counter rather than a flag because two consumers must be able to overlap:
27/// the inspector attaching while a benchmark is mid-profile is normal, and the
28/// first one to finish must not stop the second one's collection.
29static DEEP_PROFILING_CONSUMERS: AtomicUsize = AtomicUsize::new(0);
30
31/// The two owner-controlled debug capabilities shared by Blitz embedders.
32///
33/// Socket lifecycle belongs to the embedder (for example izumo
34/// or Chuzz), while the engine profiling decision is common to every stack.
35#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
36pub struct DebugOptions {
37 pub inspection_and_agent_control: bool,
38 pub deep_intrusive_profiling: bool,
39}
40
41impl DebugOptions {
42 /// Whether the intrusive collectors should run.
43 ///
44 /// This is the deep-profiling switch alone. It was once ANDed with
45 /// `inspection_and_agent_control`, on the reasoning that samples are only
46 /// useful while a plane exists to read them back — but the collectors also
47 /// feed the frame log and phase timings, which need no socket, and the AND
48 /// made the embedder's toggle silently inert with inspection off. Turning
49 /// inspection off and on then lost the setting entirely.
50 ///
51 /// Kept as a named method rather than a field read because embedders call
52 /// it across a version boundary.
53 #[must_use]
54 pub const fn effective_deep_profiling(self) -> bool {
55 self.deep_intrusive_profiling
56 }
57}
58
59/// Permit or forbid intrusive collection in this process.
60///
61/// This is the owner's toggle, and it starts nothing on its own. Turning it off
62/// while consumers are still attached also stops collection, because permission
63/// is withdrawn: [`deep_profiling_enabled`] requires both.
64///
65/// Relaxed ordering is sufficient: this is a performance mode, not a memory
66/// publication boundary. Each whole frame, resolve, or script section observes
67/// either state and the next section observes a newly selected state.
68pub fn set_deep_profiling_permitted(permitted: bool) {
69 DEEP_PROFILING_PERMITTED.store(permitted, Ordering::Relaxed);
70}
71
72/// Whether the owner has allowed sampling, regardless of any consumer.
73#[inline]
74#[must_use]
75pub fn deep_profiling_permitted() -> bool {
76 DEEP_PROFILING_PERMITTED.load(Ordering::Relaxed)
77}
78
79/// Ask for samples, for as long as the returned guard is held.
80///
81/// `None` means the profile does not permit sampling. That check lives here
82/// rather than at each call site, so a consumer cannot start collection by
83/// forgetting it.
84///
85/// The guard is the whole reason this is not a bare `start()`/`stop()` pair: an
86/// early return, a `?`, or a panic on the way out of a profiling request would
87/// otherwise leave the collectors running for the life of the process, which is
88/// the failure this function exists to prevent.
89#[must_use = "sampling stops as soon as the guard is dropped"]
90pub fn begin_deep_profiling() -> Option<DeepProfilingGuard> {
91 if !deep_profiling_permitted() {
92 return None;
93 }
94 DEEP_PROFILING_CONSUMERS.fetch_add(1, Ordering::Relaxed);
95 Some(DeepProfilingGuard { _private: () })
96}
97
98/// How many consumers are currently holding sampling open.
99#[inline]
100#[must_use]
101pub fn deep_profiling_consumers() -> usize {
102 DEEP_PROFILING_CONSUMERS.load(Ordering::Relaxed)
103}
104
105/// Holds intrusive collection open. Sampling stops when the last one drops.
106///
107/// Not constructible except through [`begin_deep_profiling`], so the count can
108/// only be raised by a consumer that passed the permission check, and not
109/// `Clone`, so one guard is one consumer.
110#[derive(Debug)]
111pub struct DeepProfilingGuard {
112 _private: (),
113}
114
115impl Drop for DeepProfilingGuard {
116 fn drop(&mut self) {
117 // Saturating rather than wrapping: a count that underflowed would read
118 // as "a consumer is attached" forever, which is precisely the state
119 // this type exists to make impossible.
120 let _ =
121 DEEP_PROFILING_CONSUMERS.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |count| {
122 Some(count.saturating_sub(1))
123 });
124 }
125}
126
127/// Whether intrusive collection is active for the next whole measured section.
128///
129/// Both halves are required: the owner permits it, and some consumer is
130/// actually reading. Either alone collects nothing.
131#[inline(always)]
132pub fn deep_profiling_enabled() -> bool {
133 DEEP_PROFILING_PERMITTED.load(Ordering::Relaxed)
134 && DEEP_PROFILING_CONSUMERS.load(Ordering::Relaxed) > 0
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 /// The statics are process-wide, so the tests that move them share one
142 /// lock rather than racing each other under the default parallel harness.
143 static SERIALISE: std::sync::Mutex<()> = std::sync::Mutex::new(());
144
145 fn exclusive() -> std::sync::MutexGuard<'static, ()> {
146 let guard = SERIALISE.lock().unwrap_or_else(|e| e.into_inner());
147 set_deep_profiling_permitted(false);
148 assert_eq!(
149 deep_profiling_consumers(),
150 0,
151 "a previous test leaked a guard",
152 );
153 guard
154 }
155
156 /// Permission alone must collect nothing.
157 ///
158 /// This is the behaviour the whole change is for: the toggle had been the
159 /// activation switch, so a profile enabled once began sampling at boot and
160 /// never stopped, for a reader that was not there.
161 #[test]
162 fn permission_alone_does_not_start_sampling() {
163 let _lock = exclusive();
164
165 set_deep_profiling_permitted(true);
166 assert!(deep_profiling_permitted());
167 assert!(
168 !deep_profiling_enabled(),
169 "sampling must wait for a consumer, not begin with the setting",
170 );
171
172 set_deep_profiling_permitted(false);
173 }
174
175 /// A consumer with no permission gets nothing to hold.
176 #[test]
177 fn a_forbidden_profile_hands_back_no_guard() {
178 let _lock = exclusive();
179
180 assert!(begin_deep_profiling().is_none());
181 assert!(!deep_profiling_enabled());
182 }
183
184 /// Two consumers overlap, and the first to finish does not stop the second.
185 #[test]
186 fn sampling_runs_until_the_last_consumer_drops() {
187 let _lock = exclusive();
188 set_deep_profiling_permitted(true);
189
190 let first = begin_deep_profiling().expect("permitted, so a guard is available");
191 let second = begin_deep_profiling().expect("a second consumer may overlap the first");
192 assert!(deep_profiling_enabled());
193
194 drop(first);
195 assert!(
196 deep_profiling_enabled(),
197 "one consumer finishing must not stop the other's collection",
198 );
199
200 drop(second);
201 assert!(!deep_profiling_enabled(), "the last drop stops sampling");
202 assert_eq!(deep_profiling_consumers(), 0);
203
204 set_deep_profiling_permitted(false);
205 }
206
207 /// A panic on the way out of a profiling request still stops sampling.
208 ///
209 /// The reason this is a guard rather than `start()`/`stop()`: an early
210 /// return or an unwind past a bare `stop()` would leave the collectors
211 /// running for the life of the process.
212 #[test]
213 fn an_unwound_guard_still_stops_sampling() {
214 let _lock = exclusive();
215 set_deep_profiling_permitted(true);
216
217 let panicked = std::panic::catch_unwind(|| {
218 let _guard = begin_deep_profiling().expect("permitted");
219 panic!("a profiling request failed mid-flight");
220 });
221
222 assert!(panicked.is_err(), "the closure above must have panicked");
223 assert_eq!(
224 deep_profiling_consumers(),
225 0,
226 "the guard's drop must have run while unwinding",
227 );
228 assert!(!deep_profiling_enabled());
229
230 set_deep_profiling_permitted(false);
231 }
232
233 /// Withdrawing permission stops collection even while a consumer is held.
234 ///
235 /// The owner's switch is the outer authority: a tool that is still attached
236 /// does not get to keep sampling after the profile forbids it.
237 #[test]
238 fn withdrawing_permission_stops_a_live_consumer() {
239 let _lock = exclusive();
240 set_deep_profiling_permitted(true);
241
242 let guard = begin_deep_profiling().expect("permitted");
243 assert!(deep_profiling_enabled());
244
245 set_deep_profiling_permitted(false);
246 assert!(!deep_profiling_enabled());
247
248 drop(guard);
249 assert_eq!(deep_profiling_consumers(), 0);
250 }
251
252 /// Deep profiling answers for itself. It used to be ANDed with inspection,
253 /// which made the switch inert whenever the inspection plane was off — the
254 /// embedder's toggle appeared to do nothing, and the stored preference was
255 /// then cleared to match, losing it.
256 #[test]
257 fn deep_profiling_is_independent_of_the_inspection_plane() {
258 assert!(!DebugOptions::default().effective_deep_profiling());
259 for inspection in [false, true] {
260 assert!(
261 DebugOptions {
262 inspection_and_agent_control: inspection,
263 deep_intrusive_profiling: true,
264 }
265 .effective_deep_profiling(),
266 "deep profiling should follow its own switch (inspection {inspection})"
267 );
268 assert!(
269 !DebugOptions {
270 inspection_and_agent_control: inspection,
271 deep_intrusive_profiling: false,
272 }
273 .effective_deep_profiling(),
274 "deep profiling off means off (inspection {inspection})"
275 );
276 }
277 }
278}