Skip to main content

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(&current, 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}