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, `pthread_getthreadid_np()` on FreeBSD, and a
58/// stable per-thread counter elsewhere. Allocation-free, so it is safe to call
59/// from the allocator hook. On hot paths prefer [`cached_tid`], which pays the
60/// syscall once per 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(target_os = "freebsd")]
89pub fn current_tid() -> u32 {
90    // SAFETY: pthread_getthreadid_np takes no arguments and returns the
91    // calling kernel thread's id.
92    unsafe { libc::pthread_getthreadid_np() as u32 }
93}
94
95#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "freebsd")))]
96pub fn current_tid() -> u32 {
97    use std::sync::atomic::{AtomicU32, Ordering};
98    static NEXT: AtomicU32 = AtomicU32::new(1);
99    thread_local! { static TID: u32 = NEXT.fetch_add(1, Ordering::Relaxed); }
100    TID.with(|t| *t)
101}
102
103#[cfg(test)]
104mod tid_tests {
105    use super::*;
106
107    #[test]
108    fn cached_tid_matches_current_tid_and_is_stable() {
109        assert_eq!(cached_tid(), current_tid());
110        assert_eq!(cached_tid(), cached_tid());
111        let other = std::thread::spawn(|| (cached_tid(), current_tid()))
112            .join()
113            .unwrap();
114        assert_eq!(other.0, other.1);
115        assert_ne!(other.0, cached_tid(), "tids must differ across threads");
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::buffer::MemoryBuffer;
123    use crate::recorder::recorder;
124    use crate::source::{FlushContext, Source};
125    use std::sync::Arc;
126    use std::sync::atomic::{AtomicUsize, Ordering};
127
128    #[cfg(target_os = "freebsd")]
129    #[test]
130    fn freebsd_tid_matches_kernel_thread_id() {
131        // SAFETY: pthread_getthreadid_np has no preconditions.
132        let expected = unsafe { libc::pthread_getthreadid_np() as u32 };
133        assert_eq!(current_tid(), expected);
134    }
135
136    #[derive(Default)]
137    struct Counts {
138        started: AtomicUsize,
139        stopped: AtomicUsize,
140    }
141
142    struct CountingSource {
143        counts: Arc<Counts>,
144        fails: bool,
145    }
146
147    impl Source for CountingSource {
148        fn flush(&mut self, _ctx: &FlushContext<'_>) {}
149
150        fn name(&self) -> &'static str {
151            "counting"
152        }
153
154        fn on_thread_start(&mut self) -> std::io::Result<()> {
155            if self.fails {
156                return Err(std::io::Error::other("no room for this thread"));
157            }
158            self.counts.started.fetch_add(1, Ordering::SeqCst);
159            Ok(())
160        }
161
162        fn on_thread_stop(&mut self) {
163            self.counts.stopped.fetch_add(1, Ordering::SeqCst);
164        }
165    }
166
167    fn counting_recorder(
168        sources: impl IntoIterator<Item = CountingSource>,
169    ) -> crate::recording::Recorder {
170        let mut builder = recorder(MemoryBuffer::new(64 * 1024).expect("writer"));
171        for source in sources {
172            builder = builder.source(source);
173        }
174        builder.build()
175    }
176
177    #[test]
178    fn guard_tracks_the_thread_until_it_drops() {
179        let counts = Arc::new(Counts::default());
180        let rec = counting_recorder([CountingSource {
181            counts: Arc::clone(&counts),
182            fails: false,
183        }]);
184
185        let guard = rec.handle().track_current_thread().expect("track");
186        assert_eq!(counts.started.load(Ordering::SeqCst), 1);
187        assert_eq!(counts.stopped.load(Ordering::SeqCst), 0);
188
189        drop(guard);
190        assert_eq!(counts.stopped.load(Ordering::SeqCst), 1);
191    }
192
193    #[test]
194    fn a_failing_source_rolls_back_the_started_ones() {
195        let counts = Arc::new(Counts::default());
196        let rec = counting_recorder([
197            CountingSource {
198                counts: Arc::clone(&counts),
199                fails: false,
200            },
201            CountingSource {
202                counts: Arc::clone(&counts),
203                fails: true,
204            },
205        ]);
206
207        let err = rec
208            .handle()
209            .track_current_thread()
210            .expect_err("second source fails");
211        assert_eq!(err.to_string(), "no room for this thread");
212        // The one that did start was stopped again: no half-tracked thread.
213        assert_eq!(counts.started.load(Ordering::SeqCst), 1);
214        assert_eq!(counts.stopped.load(Ordering::SeqCst), 1);
215    }
216
217    #[test]
218    fn disabled_handle_hands_back_an_inert_guard() {
219        let guard = Dial9Handle::disabled()
220            .track_current_thread()
221            .expect("inert guard");
222        drop(guard);
223    }
224}