Skip to main content

darwin_kperf/sampler/
thread.rs

1use core::{array, fmt, marker::PhantomData, ptr::NonNull};
2
3use darwin_kperf_sys::{kperf::KPC_MAX_COUNTERS, kperfdata::kpep_config};
4
5use super::{
6    Sampler,
7    error::{SamplerError, try_kpc},
8};
9use crate::utils::DropGuard;
10
11/// Per-thread performance counter reader.
12///
13/// Created via [`Sampler::thread`]. You call [`start`](Self::start) to enable
14/// counting, [`sample`](Self::sample) to read the current raw counter values,
15/// and [`stop`](Self::stop) to disable counting. A `ThreadSampler` is
16/// reusable across multiple start/stop cycles.
17///
18/// Hardware performance counters are thread-local: each CPU core maintains
19/// separate counter registers, and the kernel tracks per-thread accumulations
20/// as threads migrate between cores. This means a `ThreadSampler` must be
21/// used on the thread that created it, which is why it is `!Send + !Sync`.
22pub struct ThreadSampler<'sampler, const N: usize> {
23    running: bool,
24
25    sampler: &'sampler Sampler,
26    config: NonNull<kpep_config>,
27
28    classes: u32,
29    counter_map: [usize; N],
30
31    _marker: PhantomData<*mut ()>,
32}
33
34impl<'sampler, const N: usize> ThreadSampler<'sampler, N> {
35    pub(crate) const fn new(
36        sampler: &'sampler Sampler,
37        config: NonNull<kpep_config>,
38        classes: u32,
39        counter_map: [usize; N],
40    ) -> Self {
41        Self {
42            running: false,
43            sampler,
44            config,
45            classes,
46            counter_map,
47            _marker: PhantomData,
48        }
49    }
50
51    /// Returns `true` if counting is currently enabled.
52    #[must_use]
53    pub const fn is_running(&self) -> bool {
54        self.running
55    }
56
57    /// Enables counting for the configured events.
58    ///
59    /// If counting is already enabled, this is a no-op.
60    ///
61    /// # Errors
62    ///
63    /// Returns [`SamplerError`] if the kernel rejects the counting request.
64    pub fn start(&mut self) -> Result<(), SamplerError> {
65        if self.running {
66            return Ok(());
67        }
68
69        let kpc_vt = self.sampler.kperf.vtable();
70
71        try_kpc(
72            // SAFETY: kpc_set_counting is a sysctl write; classes was obtained from
73            // a valid kpep_config. Passing 0 on failure is always safe.
74            unsafe { (kpc_vt.kpc_set_counting)(self.classes) },
75            SamplerError::UnableToStartCounting,
76        )?;
77
78        let counting_guard = DropGuard::new((), |()| {
79            // SAFETY: Disable counting by writing 0 to the sysctl. The function
80            // pointer is valid, and 0 is a valid argument.
81            let _res = unsafe { (kpc_vt.kpc_set_counting)(0) };
82        });
83
84        try_kpc(
85            // SAFETY: same as kpc_set_counting, sysctl write with valid classes.
86            unsafe { (kpc_vt.kpc_set_thread_counting)(self.classes) },
87            SamplerError::UnableToStartThreadCounting,
88        )?;
89
90        self.running = true;
91
92        // NOTE: On some macOS versions, configurable counters can return stale
93        // thread samples immediately after start when reconfiguring rapidly.
94        // Callers can force an all-CPU read via kpc_get_cpu_counters(true, ...)
95        // between start and the first sample to flush counters.
96        // TODO: consider providing a built-in flush or stabilized sample API.
97
98        DropGuard::dismiss(counting_guard);
99
100        Ok(())
101    }
102
103    /// Reads the current raw counter values for the configured events.
104    ///
105    /// Each element in the returned array corresponds to the event at the same
106    /// index in the `events` array passed to [`Sampler::thread`]. Values are
107    /// absolute hardware counter readings; compute deltas between two calls to
108    /// get per-region counts.
109    ///
110    /// # Errors
111    ///
112    /// Returns [`SamplerError`] if reading thread counters fails.
113    #[expect(clippy::cast_possible_truncation, clippy::indexing_slicing)]
114    pub fn sample(&self) -> Result<[u64; N], SamplerError> {
115        if !self.running {
116            return Err(SamplerError::SamplerNotRunning);
117        }
118
119        let kpc_vt = self.sampler.kperf.vtable();
120        let mut counters = [0; KPC_MAX_COUNTERS];
121
122        try_kpc(
123            // SAFETY: buffer is KPC_MAX_COUNTERS elements, matching the count
124            // parameter. tid=0 reads the calling thread's counters.
125            unsafe {
126                (kpc_vt.kpc_get_thread_counters)(0, KPC_MAX_COUNTERS as u32, counters.as_mut_ptr())
127            },
128            SamplerError::UnableToReadCounters,
129        )?;
130
131        let output = array::from_fn(|index| {
132            let counter_index = self.counter_map[index];
133            counters[counter_index]
134        });
135
136        Ok(output)
137    }
138
139    /// Disables counting.
140    ///
141    /// If counting is already disabled, this is a no-op.
142    ///
143    /// # Errors
144    ///
145    /// Returns [`SamplerError`] if the kernel rejects the request. Both calls
146    /// are attempted even if the first fails.
147    pub fn stop(&mut self) -> Result<(), SamplerError> {
148        if !self.running {
149            return Ok(());
150        }
151
152        let kpc_vt = self.sampler.kperf.vtable();
153
154        // Reverse order of start: thread counting first, then counting.
155        // SAFETY: passing 0 to disable counting is always safe.
156        let ret_thread_counting = unsafe { (kpc_vt.kpc_set_thread_counting)(0) };
157        // SAFETY: same as above.
158        let ret_counting = unsafe { (kpc_vt.kpc_set_counting)(0) };
159
160        self.running = false;
161
162        try_kpc(
163            ret_thread_counting,
164            SamplerError::UnableToStopThreadCounting,
165        )?;
166        try_kpc(ret_counting, SamplerError::UnableToStopCounting)?;
167
168        Ok(())
169    }
170}
171
172impl<const N: usize> Drop for ThreadSampler<'_, N> {
173    fn drop(&mut self) {
174        let _result = self.stop();
175
176        let kpep_vt = self.sampler.kperfdata.vtable();
177        // SAFETY: config was allocated by kpep_config_create in ll_start and
178        // has not been freed. The kperfdata framework is still loaded because
179        // we hold a reference to the Sampler which owns it.
180        unsafe {
181            (kpep_vt.kpep_config_free)(self.config.as_ptr());
182        }
183    }
184}
185
186impl<const N: usize> fmt::Debug for ThreadSampler<'_, N> {
187    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
188        fmt.debug_struct("ThreadSampler")
189            .field("running", &self.running)
190            .field("classes", &self.classes)
191            .field("counter_map", &self.counter_map)
192            .finish_non_exhaustive()
193    }
194}