Skip to main content

dial9_core/
thread.rs

1//! Thread identity helpers and per-thread source enrollment.
2
3use std::{
4    fmt::{Formatter, Result},
5    marker::PhantomData,
6};
7
8use crate::handle::Dial9Handle;
9
10/// Keeps the calling thread enrolled with the recorder's per-thread sources.
11///
12/// Dropping it stops tracking, so hold it for as long as the thread should be profiled.
13#[must_use = "dropping the guard stops tracking this thread"]
14pub struct ThreadTrackingGuard {
15    handle: Dial9Handle,
16    // Perf events are keyed by `gettid()` and the ctimer timer lives in a
17    // thread-local, so the guard has to drop on the thread that created it.
18    _not_send: PhantomData<*const ()>,
19}
20
21impl ThreadTrackingGuard {
22    pub(crate) fn new(handle: Dial9Handle) -> Self {
23        Self {
24            handle,
25            _not_send: PhantomData,
26        }
27    }
28}
29
30impl std::fmt::Debug for ThreadTrackingGuard {
31    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
32        f.debug_struct("ThreadTrackingGuard")
33            .finish_non_exhaustive()
34    }
35}
36
37impl Drop for ThreadTrackingGuard {
38    fn drop(&mut self) {
39        let Some(shared) = self.handle.shared() else {
40            return;
41        };
42        let stopped = shared.with_sources_mut(|sources| {
43            for source in sources.iter_mut() {
44                source.on_thread_stop();
45            }
46        });
47        if stopped.is_none() {
48            crate::rate_limited!(std::time::Duration::from_secs(60), {
49                tracing::warn!("sources lock poisoned, thread left tracked");
50            });
51        }
52    }
53}
54
55/// OS thread ID (tid) of the calling thread.
56///
57/// `gettid()` on Linux/Android (a vDSO/syscall); a stable per-thread counter
58/// elsewhere. Allocation-free, so it is safe to call from the allocator hook.
59/// On hot paths prefer [`cached_tid`], which pays the syscall once per
60/// thread.
61#[cfg(any(target_os = "linux", target_os = "android"))]
62pub fn current_tid() -> u32 {
63    // SAFETY: gettid takes no args and only returns the caller's tid.
64    unsafe { libc::syscall(libc::SYS_gettid) as u32 }
65}
66
67/// [`current_tid`], read once per thread and cached: a thread's tid never
68/// changes, and `gettid` is a real syscall on Linux.
69///
70/// Allocation-free (const-initialized thread local), so it is as
71/// hook-safe as [`current_tid`] itself.
72pub fn cached_tid() -> u32 {
73    use std::cell::Cell;
74    thread_local! {
75        static TID: Cell<u32> = const { Cell::new(0) };
76    }
77    TID.with(|tid| {
78        let cached = tid.get();
79        if cached != 0 {
80            return cached;
81        }
82        let fresh = current_tid();
83        tid.set(fresh);
84        fresh
85    })
86}
87
88#[cfg(not(any(target_os = "linux", target_os = "android")))]
89pub fn current_tid() -> u32 {
90    use std::sync::atomic::{AtomicU32, Ordering};
91    static NEXT: AtomicU32 = AtomicU32::new(1);
92    thread_local! { static TID: u32 = NEXT.fetch_add(1, Ordering::Relaxed); }
93    TID.with(|t| *t)
94}
95
96#[cfg(test)]
97mod tid_tests {
98    use super::*;
99
100    #[test]
101    fn cached_tid_matches_current_tid_and_is_stable() {
102        assert_eq!(cached_tid(), current_tid());
103        assert_eq!(cached_tid(), cached_tid());
104        let other = std::thread::spawn(|| (cached_tid(), current_tid()))
105            .join()
106            .unwrap();
107        assert_eq!(other.0, other.1);
108        assert_ne!(other.0, cached_tid(), "tids must differ across threads");
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use crate::buffer::MemoryBuffer;
116    use crate::recorder::recorder;
117    use crate::source::{FlushContext, Source};
118    use std::sync::Arc;
119    use std::sync::atomic::{AtomicUsize, Ordering};
120
121    #[derive(Default)]
122    struct Counts {
123        started: AtomicUsize,
124        stopped: AtomicUsize,
125    }
126
127    struct CountingSource {
128        counts: Arc<Counts>,
129        fails: bool,
130    }
131
132    impl Source for CountingSource {
133        fn flush(&mut self, _ctx: &FlushContext<'_>) {}
134
135        fn name(&self) -> &'static str {
136            "counting"
137        }
138
139        fn on_thread_start(&mut self) -> std::io::Result<()> {
140            if self.fails {
141                return Err(std::io::Error::other("no room for this thread"));
142            }
143            self.counts.started.fetch_add(1, Ordering::SeqCst);
144            Ok(())
145        }
146
147        fn on_thread_stop(&mut self) {
148            self.counts.stopped.fetch_add(1, Ordering::SeqCst);
149        }
150    }
151
152    fn counting_recorder(
153        sources: impl IntoIterator<Item = CountingSource>,
154    ) -> crate::recording::Recorder {
155        let mut builder = recorder(MemoryBuffer::new(64 * 1024).expect("writer"));
156        for source in sources {
157            builder = builder.source(source);
158        }
159        builder.build()
160    }
161
162    #[test]
163    fn guard_tracks_the_thread_until_it_drops() {
164        let counts = Arc::new(Counts::default());
165        let rec = counting_recorder([CountingSource {
166            counts: Arc::clone(&counts),
167            fails: false,
168        }]);
169
170        let guard = rec.handle().track_current_thread().expect("track");
171        assert_eq!(counts.started.load(Ordering::SeqCst), 1);
172        assert_eq!(counts.stopped.load(Ordering::SeqCst), 0);
173
174        drop(guard);
175        assert_eq!(counts.stopped.load(Ordering::SeqCst), 1);
176    }
177
178    #[test]
179    fn a_failing_source_rolls_back_the_started_ones() {
180        let counts = Arc::new(Counts::default());
181        let rec = counting_recorder([
182            CountingSource {
183                counts: Arc::clone(&counts),
184                fails: false,
185            },
186            CountingSource {
187                counts: Arc::clone(&counts),
188                fails: true,
189            },
190        ]);
191
192        let err = rec
193            .handle()
194            .track_current_thread()
195            .expect_err("second source fails");
196        assert_eq!(err.to_string(), "no room for this thread");
197        // The one that did start was stopped again: no half-tracked thread.
198        assert_eq!(counts.started.load(Ordering::SeqCst), 1);
199        assert_eq!(counts.stopped.load(Ordering::SeqCst), 1);
200    }
201
202    #[test]
203    fn disabled_handle_hands_back_an_inert_guard() {
204        let guard = Dial9Handle::disabled()
205            .track_current_thread()
206            .expect("inert guard");
207        drop(guard);
208    }
209}