dial9_core/handle.rs
1use crate::encoder::{Encodable, ThreadLocalEncoder};
2use crate::primitives::sync::Arc;
3use crate::shared_state::SharedState;
4use crate::source::Source;
5use crate::thread::ThreadTrackingGuard;
6use arc_swap::ArcSwapOption;
7use std::any::Any;
8use std::cell::RefCell;
9
10/// First registered source of type `T`, if any.
11fn find_source<T: Source>(sources: &mut [Box<dyn Source>]) -> Option<&mut T> {
12 sources
13 .iter_mut()
14 .find_map(|source| (&mut **source as &mut dyn Any).downcast_mut::<T>())
15}
16
17crate::primitives::thread_local! {
18 /// Per-thread [`Dial9Handle`], populated via [`set_tl_handle`] and cleared
19 /// via [`clear_tl_handle`] (from a runtime's thread-start/stop hooks).
20 /// Backs [`Dial9Handle::current`] and [`current_handle`].
21 static CURRENT_HANDLE: RefCell<Option<Dial9Handle>> = const { RefCell::new(None) };
22}
23
24/// Process-wide [`Dial9Handle`], installed by
25/// [`Recorder::install_global_handle`](crate::recording::Recorder::install_global_handle) and
26/// cleared when that recorder stops.
27static GLOBAL_HANDLE: ArcSwapOption<HandleInner> = ArcSwapOption::const_empty();
28
29/// The installed process-global handle, if any.
30fn global_handle() -> Option<Dial9Handle> {
31 GLOBAL_HANDLE.load().as_ref().map(|inner| Dial9Handle {
32 inner: Some((**inner).clone()),
33 })
34}
35
36/// Commands sent to the flush thread by [`Recorder`](crate::recording::Recorder).
37pub(crate) enum ControlCommand {
38 /// Flush, finalize (seal segment), then exit the thread.
39 FinalizeAndStop(crate::primitives::sync::mpsc::SyncSender<()>),
40}
41
42/// Cheap, cloneable handle for recording events and controlling telemetry.
43///
44/// A handle may be in one of two modes:
45///
46/// - **Enabled** — backed by a live recorder; methods record
47/// events and control recording.
48/// - **Disabled** — an inert sentinel returned by
49/// [`Dial9Handle::disabled`], and by [`Dial9Handle::current`] when neither
50/// the calling thread nor the process has a handle installed.
51/// All methods are no-ops.
52///
53/// Use [`is_enabled`](Self::is_enabled) to distinguish the two modes.
54#[derive(Clone)]
55pub struct Dial9Handle {
56 inner: Option<HandleInner>,
57}
58
59#[derive(Clone)]
60struct HandleInner {
61 shared: Arc<SharedState>,
62 control_tx: crate::primitives::sync::mpsc::SyncSender<ControlCommand>,
63}
64
65impl std::fmt::Debug for Dial9Handle {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.debug_struct("Dial9Handle")
68 .field("enabled", &self.is_enabled())
69 .finish_non_exhaustive()
70 }
71}
72
73impl Dial9Handle {
74 /// Build an enabled handle wired to a flush thread's control sender.
75 /// [`Recorder::start`](crate::recording::Recorder::start) mints the channel
76 /// and owns the matching receiver.
77 pub(crate) fn enabled(
78 shared: Arc<SharedState>,
79 control_tx: crate::primitives::sync::mpsc::SyncSender<ControlCommand>,
80 ) -> Self {
81 Self {
82 inner: Some(HandleInner { shared, control_tx }),
83 }
84 }
85
86 /// Return an inert handle that is not connected to any recorder.
87 /// All methods are no-ops.
88 pub fn disabled() -> Self {
89 Self { inner: None }
90 }
91
92 /// Whether recording through this handle currently does anything: the
93 /// handle is connected to a live recorder AND recording is enabled (not
94 /// paused via [`disable`](Self::disable)).
95 ///
96 /// Returns `false` for handles obtained via [`Dial9Handle::disabled`], for
97 /// any handle [`Dial9Handle::current`] could not resolve, and while a
98 /// connected recorder is paused.
99 ///
100 /// Check this before doing per-event work that would be wasted while
101 /// recording is off, such as work leading up to [`with_encoder`](Self::with_encoder).
102 /// The check can race a concurrent enable/disable, which is benign since the event either
103 /// lands or is skipped anyway.
104 ///
105 /// To ask only whether the handle is connected at all, regardless of
106 /// pause state, use [`is_connected`](Self::is_connected).
107 pub fn is_enabled(&self) -> bool {
108 self.inner.as_ref().is_some_and(|i| i.shared.is_enabled())
109 }
110
111 crate::test_util_pub! {
112 /// Access this handle's [`SharedState`].
113 fn shared(&self) -> Option<&Arc<SharedState>> {
114 self.inner.as_ref().map(|i| &i.shared)
115 }
116 }
117
118 pub(crate) fn control_tx(
119 &self,
120 ) -> Option<&crate::primitives::sync::mpsc::SyncSender<ControlCommand>> {
121 self.inner.as_ref().map(|i| &i.control_tx)
122 }
123
124 /// On-demand dump trigger for this runtime's recorder.
125 ///
126 /// Returns `None` on a disabled handle (see [`disabled`](Self::disabled))
127 /// and when the runtime was built without a dump trigger
128 /// (`with_dump_trigger`). The returned [`DumpTrigger`](crate::dump::DumpTrigger)
129 /// is cheap to clone and every clone shares the configured debounce gate.
130 #[cfg(feature = "pipeline")]
131 pub fn dump_trigger(&self) -> Option<crate::dump::DumpTrigger> {
132 self.inner
133 .as_ref()
134 .and_then(|i| i.shared.dump_trigger().cloned())
135 }
136
137 /// Return the [`Dial9Handle`] to record through, resolved in order:
138 ///
139 /// 1. The handle installed on this thread with [`set_tl_handle`], which
140 /// runtime integrations do for the threads they own.
141 /// 2. The process-global handle, if
142 /// [`Recorder::install_global_handle`](crate::recording::Recorder::install_global_handle)
143 /// has been called.
144 /// 3. An inert [`disabled`](Self::disabled) handle, where recording is a
145 /// no-op.
146 ///
147 /// Use [`is_enabled`](Self::is_enabled) to branch on whether telemetry is
148 /// live here.
149 pub fn current() -> Self {
150 CURRENT_HANDLE
151 .with(|cell| cell.borrow().clone())
152 .or_else(global_handle)
153 .unwrap_or_else(Self::disabled)
154 }
155
156 /// Return the [`Dial9Handle`] installed on this thread with [`set_tl_handle`], or
157 /// `None` if there is none.
158 ///
159 /// Unlike [`current`](Self::current), never falls back to the
160 /// process-global handle. To record an event, use [`current`](Self::current).
161 pub fn try_current_thread() -> Option<Self> {
162 CURRENT_HANDLE.with(|cell| cell.borrow().clone())
163 }
164
165 /// Enable telemetry recording. No-op on a disabled handle.
166 pub fn enable(&self) {
167 if let Some(inner) = &self.inner {
168 inner.shared.enable();
169 }
170 }
171
172 /// Disable telemetry recording. No-op on a disabled handle.
173 pub fn disable(&self) {
174 if let Some(inner) = &self.inner {
175 inner.shared.disable();
176 }
177 }
178
179 /// Profile the calling thread.
180 ///
181 /// Per-thread sources, such as the scheduler-event profiler, only sample
182 /// threads that opt in. Tokio workers opt in on their own, call this from
183 /// any other thread you want profiled. Profiling lasts until the returned
184 /// guard drops.
185 ///
186 /// Returns an error if a source could not start on this thread. No-op on a
187 /// disabled handle.
188 ///
189 /// ```no_run
190 /// use dial9_core::buffer::MemoryBuffer;
191 /// use dial9_core::recorder::recorder;
192 ///
193 /// let rec = recorder(MemoryBuffer::new(1 << 20)?).build();
194 /// let handle = rec.handle().clone();
195 ///
196 /// std::thread::spawn(move || -> std::io::Result<()> {
197 /// let _tracking = handle.track_current_thread()?;
198 /// // work here is sampled by the recorder's per-thread sources
199 /// Ok(())
200 /// });
201 /// # Ok::<_, std::io::Error>(())
202 /// ```
203 pub fn track_current_thread(&self) -> std::io::Result<ThreadTrackingGuard> {
204 let Some(inner) = &self.inner else {
205 return Ok(ThreadTrackingGuard::new(self.clone()));
206 };
207
208 let started = inner.shared.with_sources_mut(|sources| {
209 let mut done = 0;
210 let mut failure = None;
211 for source in sources.iter_mut() {
212 match source.on_thread_start() {
213 Ok(()) => done += 1,
214 Err(e) => {
215 failure = Some(e);
216 break;
217 }
218 }
219 }
220 match failure {
221 // Leave the thread untracked rather than half-tracked.
222 Some(e) => {
223 for source in &mut sources[..done] {
224 source.on_thread_stop();
225 }
226 Err(e)
227 }
228 None => Ok(()),
229 }
230 });
231
232 match started {
233 Some(Ok(())) => Ok(ThreadTrackingGuard::new(self.clone())),
234 Some(Err(e)) => Err(e),
235 None => Err(std::io::Error::other("dial9: sources lock poisoned")),
236 }
237 }
238
239 /// Whether this handle is wired to a recorder at all, regardless of whether
240 /// recording is currently paused.
241 ///
242 /// [`is_enabled`](Self::is_enabled) answers the narrower question of whether
243 /// a record right now would land.
244 pub fn is_connected(&self) -> bool {
245 self.inner.is_some()
246 }
247
248 /// Whether the recorder behind this handle has shut down.
249 ///
250 /// Terminal: a stopped recorder never records again. Returns `false` for a
251 /// handle that is merely paused (see [`disable`](Self::disable)) and for a
252 /// disabled handle, neither of which is stopped.
253 pub fn is_stopped(&self) -> bool {
254 self.inner.as_ref().is_some_and(|i| i.shared.is_stopped())
255 }
256
257 /// Run `f` against this recorder's source of type `T`.
258 ///
259 /// `None` when the handle is disabled, no `T` is registered, or the source
260 /// lock is poisoned.
261 pub fn with_source<T: Source, R>(&self, f: impl FnOnce(&mut T) -> R) -> Option<R> {
262 let inner = self.inner.as_ref()?;
263 inner
264 .shared
265 .with_sources_mut(|sources| Some(f(find_source::<T>(sources)?)))
266 .flatten()
267 }
268
269 /// Run `f` against this recorder's source of type `T`, registering the one
270 /// `make` builds if there is not one yet.
271 ///
272 /// `None` when the handle is disabled, the recorder has shut down, or the
273 /// source lock is poisoned.
274 pub fn with_source_or_insert<T: Source, R>(
275 &self,
276 make: impl FnOnce() -> T,
277 f: impl FnOnce(&mut T) -> R,
278 ) -> Option<R> {
279 let inner = self.inner.as_ref()?;
280 inner
281 .shared
282 .with_sources_vec(|sources| {
283 if inner.shared.is_stopped() {
284 return None;
285 }
286 if find_source::<T>(sources).is_none() {
287 sources.push(Box::new(make()));
288 }
289 Some(f(find_source::<T>(sources).expect("just registered")))
290 })
291 .flatten()
292 }
293
294 /// Record a custom event into the trace.
295 ///
296 /// Any type implementing [`dial9_trace_format::TraceEvent`] (typically via
297 /// `#[derive(TraceEvent)]`) works directly. No-op on a disabled handle or
298 /// when recording is paused.
299 pub fn record_event(&self, event: impl Encodable) {
300 if let Some(inner) = &self.inner {
301 inner
302 .shared
303 .if_enabled(|buf| buf.record_encodable_event(&event));
304 }
305 }
306
307 /// Record an event that is only built when recording is on.
308 ///
309 /// Reach for this over [`record_event`](Self::record_event) when building
310 /// the event costs something you would rather not pay while recording is
311 /// paused, such as a clock read or a lookup. `make` runs only if the event
312 /// will be recorded.
313 pub fn record_event_with<E: Encodable>(&self, make: impl FnOnce() -> E) {
314 if let Some(inner) = &self.inner {
315 inner
316 .shared
317 .if_enabled(|buf| buf.record_encodable_event(&make()));
318 }
319 }
320
321 /// Run a closure with direct access to the thread-local encoder.
322 ///
323 /// The closure is only invoked if telemetry is enabled.
324 /// No-op on a disabled handle or when recording is paused.
325 #[doc(hidden)]
326 pub fn with_encoder(&self, f: impl FnOnce(&mut ThreadLocalEncoder<'_>)) {
327 if let Some(inner) = &self.inner {
328 inner.shared.if_enabled(|buf| buf.with_encoder(f));
329 }
330 }
331}
332
333/// Install `handle` as the current thread's [`Dial9Handle`].
334///
335/// Runtime integrations call this from their thread-start hook (e.g. tokio's
336/// `on_thread_start`) so that [`current_handle`] / [`Dial9Handle::current`]
337/// return the live handle on worker threads.
338pub fn set_tl_handle(handle: Dial9Handle) {
339 CURRENT_HANDLE.with(|cell| *cell.borrow_mut() = Some(handle));
340}
341
342/// Clear the current thread's [`Dial9Handle`], installed by [`set_tl_handle`].
343///
344/// Runtime integrations call this from their thread-stop hook.
345pub fn clear_tl_handle() {
346 CURRENT_HANDLE.with(|cell| *cell.borrow_mut() = None);
347}
348
349/// Install `handle` as the process-global [`Dial9Handle`], unless another is
350/// already installed.
351pub(crate) fn set_global_handle(handle: Dial9Handle) -> Result<(), InstallGlobalHandleError> {
352 let previous =
353 GLOBAL_HANDLE.compare_and_swap(&None::<Arc<HandleInner>>, handle.inner.map(Arc::new));
354 match previous.is_some() {
355 true => Err(InstallGlobalHandleError),
356 false => Ok(()),
357 }
358}
359
360/// [`Recorder::install_global_handle`](crate::recording::Recorder::install_global_handle)
361/// did not install: a process-global [`Dial9Handle`] was already installed.
362#[derive(Debug, Clone, Copy, PartialEq, Eq)]
363#[non_exhaustive]
364pub struct InstallGlobalHandleError;
365
366impl std::fmt::Display for InstallGlobalHandleError {
367 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368 f.write_str("a process-global Dial9Handle is already installed")
369 }
370}
371
372impl std::error::Error for InstallGlobalHandleError {}
373
374/// Clear the process-global [`Dial9Handle`] if it belongs to the recorder whose
375/// state is `shared`, otherwise leave it alone.
376///
377pub(crate) fn clear_global_handle_for(shared: &Arc<SharedState>) {
378 let current = GLOBAL_HANDLE.load();
379 if current
380 .as_ref()
381 .is_some_and(|i| Arc::ptr_eq(&i.shared, shared))
382 {
383 // Compare-and-swap so a stale read here can't undo a newer install.
384 GLOBAL_HANDLE.compare_and_swap(¤t, None);
385 }
386}
387
388/// Return the [`Dial9Handle`] for the current thread, falling back to the
389/// process-global one. Equivalent to [`Dial9Handle::current`].
390pub fn current_handle() -> Dial9Handle {
391 Dial9Handle::current()
392}