1use std::cell::Cell;
8use std::marker::PhantomData;
9use std::time::{Duration, Instant};
10
11use crate::{Counter, Histogram, LabelEnum};
12
13thread_local! {
14 static SAMPLE_SEQ: Cell<u64> = const { Cell::new(0) };
15}
16
17pub struct SampledTimer {
21 inner: SampledTimerInner,
22}
23
24pub struct LabeledSampledTimer<L: LabelEnum> {
26 timers: Vec<SampledTimerInner>,
27 _phantom: PhantomData<L>,
28}
29
30pub struct SampledTimerGuard<'a> {
35 timer: &'a SampledTimerInner,
36 start: Option<Instant>,
37 finished: bool,
38}
39
40struct SampledTimerInner {
41 calls: Counter,
42 samples: Histogram,
43 stride_mask: u64,
44}
45
46impl SampledTimer {
47 pub fn new(bounds_nanos: &[u64], shard_count: usize, sample_stride: u64) -> Self {
52 Self {
53 inner: SampledTimerInner::new(bounds_nanos, shard_count, sample_stride),
54 }
55 }
56
57 pub fn with_latency_buckets(shard_count: usize, sample_stride: u64) -> Self {
59 Self {
60 inner: SampledTimerInner::with_latency_buckets(shard_count, sample_stride),
61 }
62 }
63
64 #[inline]
66 pub fn start(&self) -> SampledTimerGuard<'_> {
67 self.inner.start()
68 }
69
70 #[inline]
72 pub fn record_elapsed(&self, elapsed: Duration) {
73 self.inner.record_elapsed(elapsed);
74 }
75
76 #[inline]
78 pub fn calls(&self) -> u64 {
79 self.inner.calls()
80 }
81
82 #[inline]
84 pub fn sample_count(&self) -> u64 {
85 self.inner.sample_count()
86 }
87
88 #[inline]
90 pub fn sample_sum_nanos(&self) -> u64 {
91 self.inner.sample_sum_nanos()
92 }
93
94 pub fn avg_sample_nanos(&self) -> Option<f64> {
96 self.inner.avg_sample_nanos()
97 }
98
99 #[inline]
101 pub fn calls_metric(&self) -> &Counter {
102 &self.inner.calls
103 }
104
105 #[inline]
107 pub fn histogram(&self) -> &Histogram {
108 &self.inner.samples
109 }
110}
111
112impl<L: LabelEnum> LabeledSampledTimer<L> {
113 pub fn new(bounds_nanos: &[u64], shard_count: usize, sample_stride: u64) -> Self {
115 let timers = (0..L::CARDINALITY)
116 .map(|_| SampledTimerInner::new(bounds_nanos, shard_count, sample_stride))
117 .collect();
118 Self {
119 timers,
120 _phantom: PhantomData,
121 }
122 }
123
124 pub fn with_latency_buckets(shard_count: usize, sample_stride: u64) -> Self {
126 let timers = (0..L::CARDINALITY)
127 .map(|_| SampledTimerInner::with_latency_buckets(shard_count, sample_stride))
128 .collect();
129 Self {
130 timers,
131 _phantom: PhantomData,
132 }
133 }
134
135 #[inline]
137 pub fn start(&self, label: L) -> SampledTimerGuard<'_> {
138 self.timer(label).start()
139 }
140
141 #[inline]
143 pub fn record_elapsed(&self, label: L, elapsed: Duration) {
144 self.timer(label).record_elapsed(elapsed);
145 }
146
147 #[inline]
149 pub fn calls(&self, label: L) -> u64 {
150 self.timer(label).calls()
151 }
152
153 #[inline]
155 pub fn sample_count(&self, label: L) -> u64 {
156 self.timer(label).sample_count()
157 }
158
159 #[inline]
161 pub fn sample_sum_nanos(&self, label: L) -> u64 {
162 self.timer(label).sample_sum_nanos()
163 }
164
165 pub fn avg_sample_nanos(&self, label: L) -> Option<f64> {
167 self.timer(label).avg_sample_nanos()
168 }
169
170 #[inline]
172 pub fn calls_metric(&self, label: L) -> &Counter {
173 &self.timer(label).calls
174 }
175
176 #[inline]
178 pub fn histogram(&self, label: L) -> &Histogram {
179 &self.timer(label).samples
180 }
181
182 pub fn iter(&self) -> impl Iterator<Item = (L, &Counter, &Histogram)> + '_ {
184 self.timers
185 .iter()
186 .enumerate()
187 .map(|(idx, timer)| (L::from_index(idx), &timer.calls, &timer.samples))
188 }
189
190 #[inline]
191 fn timer(&self, label: L) -> &SampledTimerInner {
192 let idx = label.as_index();
193 debug_assert!(idx < self.timers.len(), "label index out of bounds");
194 if cfg!(debug_assertions) {
195 &self.timers[idx]
196 } else {
197 unsafe { self.timers.get_unchecked(idx) }
198 }
199 }
200}
201
202impl SampledTimerGuard<'_> {
203 #[inline]
205 pub fn finish(mut self) {
206 self.record();
207 self.finished = true;
208 }
209
210 #[inline]
211 fn record(&mut self) {
212 let Some(start) = self.start.take() else {
213 return;
214 };
215 self.timer.samples.record(duration_nanos(start.elapsed()));
216 }
217}
218
219impl Drop for SampledTimerGuard<'_> {
220 #[inline]
221 fn drop(&mut self) {
222 if !self.finished {
223 self.record();
224 }
225 }
226}
227
228impl SampledTimerInner {
229 fn new(bounds_nanos: &[u64], shard_count: usize, sample_stride: u64) -> Self {
230 Self {
231 calls: Counter::new(shard_count),
232 samples: Histogram::new(bounds_nanos, shard_count),
233 stride_mask: stride_mask(sample_stride),
234 }
235 }
236
237 fn with_latency_buckets(shard_count: usize, sample_stride: u64) -> Self {
238 Self::new(
239 &[
240 10_000, 50_000, 100_000, 500_000, 1_000_000, 5_000_000, 10_000_000, 50_000_000, 100_000_000, 500_000_000, 1_000_000_000, 5_000_000_000, 10_000_000_000, ],
254 shard_count,
255 sample_stride,
256 )
257 }
258
259 #[inline]
260 fn start(&self) -> SampledTimerGuard<'_> {
261 self.calls.inc();
262 let sampled = should_sample(self.stride_mask);
263 SampledTimerGuard {
264 timer: self,
265 start: sampled.then(Instant::now),
266 finished: false,
267 }
268 }
269
270 #[inline]
271 fn record_elapsed(&self, elapsed: Duration) {
272 self.calls.inc();
273 if should_sample(self.stride_mask) {
274 self.samples.record(duration_nanos(elapsed));
275 }
276 }
277
278 #[inline]
279 fn calls(&self) -> u64 {
280 self.calls.sum() as u64
281 }
282
283 #[inline]
284 fn sample_count(&self) -> u64 {
285 self.samples.count()
286 }
287
288 #[inline]
289 fn sample_sum_nanos(&self) -> u64 {
290 self.samples.sum()
291 }
292
293 fn avg_sample_nanos(&self) -> Option<f64> {
294 let count = self.sample_count();
295 if count == 0 {
296 return None;
297 }
298 Some(self.sample_sum_nanos() as f64 / count as f64)
299 }
300}
301
302fn should_sample(stride_mask: u64) -> bool {
303 SAMPLE_SEQ.with(|seq| {
304 let next = seq.get().wrapping_add(1);
305 seq.set(next);
306 next & stride_mask == 0
307 })
308}
309
310fn stride_mask(sample_stride: u64) -> u64 {
311 sample_stride.max(1).next_power_of_two() - 1
312}
313
314fn duration_nanos(elapsed: Duration) -> u64 {
315 elapsed.as_nanos().min(u64::MAX as u128) as u64
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[derive(Copy, Clone, Debug, PartialEq)]
323 enum TestLabel {
324 A,
325 B,
326 }
327
328 impl LabelEnum for TestLabel {
329 const CARDINALITY: usize = 2;
330 const LABEL_NAME: &'static str = "label";
331
332 fn as_index(self) -> usize {
333 self as usize
334 }
335
336 fn from_index(index: usize) -> Self {
337 match index {
338 0 => Self::A,
339 _ => Self::B,
340 }
341 }
342
343 fn variant_name(self) -> &'static str {
344 match self {
345 Self::A => "a",
346 Self::B => "b",
347 }
348 }
349 }
350
351 #[test]
352 fn stride_one_records_every_call() {
353 let timer = SampledTimer::with_latency_buckets(4, 1);
354
355 timer.record_elapsed(Duration::from_nanos(10));
356 timer.record_elapsed(Duration::from_nanos(20));
357
358 assert_eq!(timer.calls(), 2);
359 assert_eq!(timer.sample_count(), 2);
360 assert_eq!(timer.sample_sum_nanos(), 30);
361 assert_eq!(timer.avg_sample_nanos(), Some(15.0));
362 }
363
364 #[test]
365 fn stride_samples_subset() {
366 let timer = SampledTimer::with_latency_buckets(4, 4);
367
368 for _ in 0..8 {
369 timer.record_elapsed(Duration::from_nanos(10));
370 }
371
372 assert_eq!(timer.calls(), 8);
373 assert_eq!(timer.sample_count(), 2);
374 }
375
376 #[test]
377 fn guard_records_on_drop() {
378 let timer = SampledTimer::with_latency_buckets(4, 1);
379
380 {
381 let _guard = timer.start();
382 }
383
384 assert_eq!(timer.calls(), 1);
385 assert_eq!(timer.sample_count(), 1);
386 }
387
388 #[test]
389 fn explicit_finish_records_once() {
390 let timer = SampledTimer::with_latency_buckets(4, 1);
391
392 timer.start().finish();
393
394 assert_eq!(timer.calls(), 1);
395 assert_eq!(timer.sample_count(), 1);
396 }
397
398 #[test]
399 fn labeled_timer_tracks_labels_independently() {
400 let timer: LabeledSampledTimer<TestLabel> = LabeledSampledTimer::with_latency_buckets(4, 1);
401
402 timer.record_elapsed(TestLabel::A, Duration::from_nanos(15));
403 timer.record_elapsed(TestLabel::B, Duration::from_nanos(25));
404 timer.record_elapsed(TestLabel::A, Duration::from_nanos(35));
405
406 assert_eq!(timer.calls(TestLabel::A), 2);
407 assert_eq!(timer.calls(TestLabel::B), 1);
408 assert_eq!(timer.sample_count(TestLabel::A), 2);
409 assert_eq!(timer.sample_sum_nanos(TestLabel::A), 50);
410 assert_eq!(timer.sample_sum_nanos(TestLabel::B), 25);
411 }
412}