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}