use crate::{ids::SystemMetricKind, ops::runtime::metrics::system::SystemMetrics};
use std::{cell::RefCell, collections::HashMap, time::Duration};
thread_local! {
static TIMER_METRICS: RefCell<HashMap<TimerMetricKey, u64>> =
RefCell::new(HashMap::new());
}
#[derive(Clone)]
pub struct TimerMetricsSnapshot {
pub entries: Vec<(TimerMetricKey, u64)>,
}
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum TimerMode {
Interval,
Once,
}
#[derive(Clone, Eq, Hash, PartialEq)]
pub struct TimerMetricKey {
pub mode: TimerMode,
pub delay_ms: u64,
pub label: String,
}
pub struct TimerMetrics;
impl TimerMetrics {
#[expect(clippy::cast_possible_truncation)]
fn delay_ms(delay: Duration) -> u64 {
delay.as_millis().min(u128::from(u64::MAX)) as u64
}
pub fn ensure(mode: TimerMode, delay: Duration, label: &str) {
let delay_ms = Self::delay_ms(delay);
TIMER_METRICS.with_borrow_mut(|counts| {
let key = TimerMetricKey {
mode,
delay_ms,
label: label.to_string(),
};
counts.entry(key).or_insert(0);
});
}
pub fn increment(mode: TimerMode, delay: Duration, label: &str) {
let delay_ms = Self::delay_ms(delay);
TIMER_METRICS.with_borrow_mut(|counts| {
let key = TimerMetricKey {
mode,
delay_ms,
label: label.to_string(),
};
let entry = counts.entry(key).or_insert(0);
*entry = entry.saturating_add(1);
});
}
pub fn record_timer_scheduled(mode: TimerMode, delay: Duration, label: &str) {
SystemMetrics::increment(SystemMetricKind::TimerScheduled);
Self::ensure(mode, delay, label);
}
pub fn record_timer_tick(mode: TimerMode, delay: Duration, label: &str) {
Self::increment(mode, delay, label);
}
#[must_use]
pub fn snapshot() -> TimerMetricsSnapshot {
let entries = TIMER_METRICS
.with_borrow(std::clone::Clone::clone)
.into_iter()
.collect();
TimerMetricsSnapshot { entries }
}
#[cfg(test)]
pub fn reset() {
TIMER_METRICS.with_borrow_mut(HashMap::clear);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn snapshot_map() -> HashMap<TimerMetricKey, u64> {
TimerMetrics::snapshot().entries.into_iter().collect()
}
#[test]
fn timer_metrics_track_mode_delay_and_label() {
TimerMetrics::reset();
TimerMetrics::increment(TimerMode::Once, Duration::from_secs(1), "once:a");
TimerMetrics::increment(TimerMode::Once, Duration::from_secs(1), "once:a");
TimerMetrics::increment(
TimerMode::Interval,
Duration::from_millis(500),
"interval:b",
);
let map = snapshot_map();
let key_once = TimerMetricKey {
mode: TimerMode::Once,
delay_ms: 1_000,
label: "once:a".to_string(),
};
let key_interval = TimerMetricKey {
mode: TimerMode::Interval,
delay_ms: 500,
label: "interval:b".to_string(),
};
assert_eq!(map.get(&key_once), Some(&2));
assert_eq!(map.get(&key_interval), Some(&1));
assert_eq!(map.len(), 2);
}
#[test]
fn ensure_creates_zero_count_entry() {
TimerMetrics::reset();
TimerMetrics::ensure(TimerMode::Interval, Duration::from_secs(2), "heartbeat");
let map = snapshot_map();
let key = TimerMetricKey {
mode: TimerMode::Interval,
delay_ms: 2_000,
label: "heartbeat".to_string(),
};
assert_eq!(map.get(&key), Some(&0));
assert_eq!(map.len(), 1);
}
#[test]
fn ensure_is_idempotent() {
TimerMetrics::reset();
TimerMetrics::ensure(TimerMode::Once, Duration::from_secs(1), "once:x");
TimerMetrics::ensure(TimerMode::Once, Duration::from_secs(1), "once:x");
let map = snapshot_map();
let key = TimerMetricKey {
mode: TimerMode::Once,
delay_ms: 1_000,
label: "once:x".to_string(),
};
assert_eq!(map.get(&key), Some(&0));
assert_eq!(map.len(), 1);
}
}