use std::cell::Cell;
use std::marker::PhantomData;
use std::time::{Duration, Instant};
use crate::{Counter, Histogram, LabelEnum};
thread_local! {
static SAMPLE_SEQ: Cell<u64> = const { Cell::new(0) };
}
pub struct SampledTimer {
inner: SampledTimerInner,
}
pub struct LabeledSampledTimer<L: LabelEnum> {
timers: Vec<SampledTimerInner>,
_phantom: PhantomData<L>,
}
pub struct SampledTimerGuard<'a> {
timer: &'a SampledTimerInner,
start: Option<Instant>,
finished: bool,
}
struct SampledTimerInner {
calls: Counter,
samples: Histogram,
stride_mask: u64,
}
impl SampledTimer {
pub fn new(bounds_nanos: &[u64], shard_count: usize, sample_stride: u64) -> Self {
Self {
inner: SampledTimerInner::new(bounds_nanos, shard_count, sample_stride),
}
}
pub fn with_latency_buckets(shard_count: usize, sample_stride: u64) -> Self {
Self {
inner: SampledTimerInner::with_latency_buckets(shard_count, sample_stride),
}
}
#[inline]
pub fn start(&self) -> SampledTimerGuard<'_> {
self.inner.start()
}
#[inline]
pub fn record_elapsed(&self, elapsed: Duration) {
self.inner.record_elapsed(elapsed);
}
#[inline]
pub fn calls(&self) -> u64 {
self.inner.calls()
}
#[inline]
pub fn sample_count(&self) -> u64 {
self.inner.sample_count()
}
#[inline]
pub fn sample_sum_nanos(&self) -> u64 {
self.inner.sample_sum_nanos()
}
pub fn avg_sample_nanos(&self) -> Option<f64> {
self.inner.avg_sample_nanos()
}
#[inline]
pub fn calls_metric(&self) -> &Counter {
&self.inner.calls
}
#[inline]
pub fn histogram(&self) -> &Histogram {
&self.inner.samples
}
}
impl<L: LabelEnum> LabeledSampledTimer<L> {
pub fn new(bounds_nanos: &[u64], shard_count: usize, sample_stride: u64) -> Self {
let timers = (0..L::CARDINALITY)
.map(|_| SampledTimerInner::new(bounds_nanos, shard_count, sample_stride))
.collect();
Self {
timers,
_phantom: PhantomData,
}
}
pub fn with_latency_buckets(shard_count: usize, sample_stride: u64) -> Self {
let timers = (0..L::CARDINALITY)
.map(|_| SampledTimerInner::with_latency_buckets(shard_count, sample_stride))
.collect();
Self {
timers,
_phantom: PhantomData,
}
}
#[inline]
pub fn start(&self, label: L) -> SampledTimerGuard<'_> {
self.timer(label).start()
}
#[inline]
pub fn record_elapsed(&self, label: L, elapsed: Duration) {
self.timer(label).record_elapsed(elapsed);
}
#[inline]
pub fn calls(&self, label: L) -> u64 {
self.timer(label).calls()
}
#[inline]
pub fn sample_count(&self, label: L) -> u64 {
self.timer(label).sample_count()
}
#[inline]
pub fn sample_sum_nanos(&self, label: L) -> u64 {
self.timer(label).sample_sum_nanos()
}
pub fn avg_sample_nanos(&self, label: L) -> Option<f64> {
self.timer(label).avg_sample_nanos()
}
#[inline]
pub fn calls_metric(&self, label: L) -> &Counter {
&self.timer(label).calls
}
#[inline]
pub fn histogram(&self, label: L) -> &Histogram {
&self.timer(label).samples
}
pub fn iter(&self) -> impl Iterator<Item = (L, &Counter, &Histogram)> + '_ {
self.timers
.iter()
.enumerate()
.map(|(idx, timer)| (L::from_index(idx), &timer.calls, &timer.samples))
}
#[inline]
fn timer(&self, label: L) -> &SampledTimerInner {
let idx = label.as_index();
debug_assert!(idx < self.timers.len(), "label index out of bounds");
if cfg!(debug_assertions) {
&self.timers[idx]
} else {
unsafe { self.timers.get_unchecked(idx) }
}
}
}
impl SampledTimerGuard<'_> {
#[inline]
pub fn finish(mut self) {
self.record();
self.finished = true;
}
#[inline]
fn record(&mut self) {
let Some(start) = self.start.take() else {
return;
};
self.timer.samples.record(duration_nanos(start.elapsed()));
}
}
impl Drop for SampledTimerGuard<'_> {
#[inline]
fn drop(&mut self) {
if !self.finished {
self.record();
}
}
}
impl SampledTimerInner {
fn new(bounds_nanos: &[u64], shard_count: usize, sample_stride: u64) -> Self {
Self {
calls: Counter::new(shard_count),
samples: Histogram::new(bounds_nanos, shard_count),
stride_mask: stride_mask(sample_stride),
}
}
fn with_latency_buckets(shard_count: usize, sample_stride: u64) -> Self {
Self::new(
&[
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, ],
shard_count,
sample_stride,
)
}
#[inline]
fn start(&self) -> SampledTimerGuard<'_> {
self.calls.inc();
let sampled = should_sample(self.stride_mask);
SampledTimerGuard {
timer: self,
start: sampled.then(Instant::now),
finished: false,
}
}
#[inline]
fn record_elapsed(&self, elapsed: Duration) {
self.calls.inc();
if should_sample(self.stride_mask) {
self.samples.record(duration_nanos(elapsed));
}
}
#[inline]
fn calls(&self) -> u64 {
self.calls.sum() as u64
}
#[inline]
fn sample_count(&self) -> u64 {
self.samples.count()
}
#[inline]
fn sample_sum_nanos(&self) -> u64 {
self.samples.sum()
}
fn avg_sample_nanos(&self) -> Option<f64> {
let count = self.sample_count();
if count == 0 {
return None;
}
Some(self.sample_sum_nanos() as f64 / count as f64)
}
}
fn should_sample(stride_mask: u64) -> bool {
SAMPLE_SEQ.with(|seq| {
let next = seq.get().wrapping_add(1);
seq.set(next);
next & stride_mask == 0
})
}
fn stride_mask(sample_stride: u64) -> u64 {
sample_stride.max(1).next_power_of_two() - 1
}
fn duration_nanos(elapsed: Duration) -> u64 {
elapsed.as_nanos().min(u64::MAX as u128) as u64
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Copy, Clone, Debug, PartialEq)]
enum TestLabel {
A,
B,
}
impl LabelEnum for TestLabel {
const CARDINALITY: usize = 2;
const LABEL_NAME: &'static str = "label";
fn as_index(self) -> usize {
self as usize
}
fn from_index(index: usize) -> Self {
match index {
0 => Self::A,
_ => Self::B,
}
}
fn variant_name(self) -> &'static str {
match self {
Self::A => "a",
Self::B => "b",
}
}
}
#[test]
fn stride_one_records_every_call() {
let timer = SampledTimer::with_latency_buckets(4, 1);
timer.record_elapsed(Duration::from_nanos(10));
timer.record_elapsed(Duration::from_nanos(20));
assert_eq!(timer.calls(), 2);
assert_eq!(timer.sample_count(), 2);
assert_eq!(timer.sample_sum_nanos(), 30);
assert_eq!(timer.avg_sample_nanos(), Some(15.0));
}
#[test]
fn stride_samples_subset() {
let timer = SampledTimer::with_latency_buckets(4, 4);
for _ in 0..8 {
timer.record_elapsed(Duration::from_nanos(10));
}
assert_eq!(timer.calls(), 8);
assert_eq!(timer.sample_count(), 2);
}
#[test]
fn guard_records_on_drop() {
let timer = SampledTimer::with_latency_buckets(4, 1);
{
let _guard = timer.start();
}
assert_eq!(timer.calls(), 1);
assert_eq!(timer.sample_count(), 1);
}
#[test]
fn explicit_finish_records_once() {
let timer = SampledTimer::with_latency_buckets(4, 1);
timer.start().finish();
assert_eq!(timer.calls(), 1);
assert_eq!(timer.sample_count(), 1);
}
#[test]
fn labeled_timer_tracks_labels_independently() {
let timer: LabeledSampledTimer<TestLabel> = LabeledSampledTimer::with_latency_buckets(4, 1);
timer.record_elapsed(TestLabel::A, Duration::from_nanos(15));
timer.record_elapsed(TestLabel::B, Duration::from_nanos(25));
timer.record_elapsed(TestLabel::A, Duration::from_nanos(35));
assert_eq!(timer.calls(TestLabel::A), 2);
assert_eq!(timer.calls(TestLabel::B), 1);
assert_eq!(timer.sample_count(TestLabel::A), 2);
assert_eq!(timer.sample_sum_nanos(TestLabel::A), 50);
assert_eq!(timer.sample_sum_nanos(TestLabel::B), 25);
}
}