canic_core/model/metrics/
timer.rs

1use candid::CandidType;
2use serde::{Deserialize, Serialize};
3use std::{cell::RefCell, collections::HashMap, time::Duration};
4
5thread_local! {
6    static TIMER_METRICS: RefCell<HashMap<TimerMetricKey, u64>> = RefCell::new(HashMap::new());
7}
8
9///
10/// TimerMode
11///
12
13#[derive(
14    CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize,
15)]
16pub enum TimerMode {
17    Interval,
18    Once,
19}
20
21///
22/// TimerMetricKey
23/// Uniquely identifies a timer by mode + delay (ms) + label.
24///
25
26#[derive(CandidType, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
27pub struct TimerMetricKey {
28    pub mode: TimerMode,
29    pub delay_ms: u64,
30    pub label: String,
31}
32
33///
34/// TimerMetricEntry
35/// Snapshot entry pairing a timer mode/delay with its count.
36///
37
38#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
39pub struct TimerMetricEntry {
40    pub mode: TimerMode,
41    pub delay_ms: u64,
42    pub label: String,
43    pub count: u64,
44}
45
46///
47/// TimerMetricsSnapshot
48///
49
50pub type TimerMetricsSnapshot = Vec<TimerMetricEntry>;
51
52///
53/// TimerMetrics
54///
55/// Volatile counters for timers keyed by `(mode, delay_ms, label)`.
56///
57/// ## What this measures
58///
59/// `TimerMetrics` is intended to answer two related questions:
60///
61/// 1) **Which timers have been scheduled?**
62///    - Use [`ensure`] at scheduling time to guarantee the timer appears in snapshots,
63///      even if it has not fired yet (e.g., newly-created interval timers).
64///
65/// 2) **How many scheduling events have occurred for a given timer key?**
66///    - Use [`increment`] when you explicitly want to count scheduling operations.
67///
68/// Note that this type does **not** count executions/ticks of interval timers.
69/// Execution counts should be tracked separately (e.g., via perf records or a
70/// dedicated “timer runs” metric), because scheduling and execution are different signals.
71///
72/// ## Cardinality and labels
73///
74/// Labels are used as metric keys. Keep labels stable and low-cardinality (avoid
75/// embedding principals, IDs, or other high-variance values).
76///
77/// ## Thread safety / runtime model
78///
79/// This uses `thread_local!` storage. On the IC, this is the standard way to maintain
80/// mutable global state without `unsafe`.
81///
82
83pub struct TimerMetrics;
84
85impl TimerMetrics {
86    #[allow(clippy::cast_possible_truncation)]
87    fn delay_ms(delay: Duration) -> u64 {
88        delay.as_millis().min(u128::from(u64::MAX)) as u64
89    }
90
91    /// Ensure a timer key exists in the metrics table with an initial count of `0`.
92    ///
93    /// This is used at **schedule time** to make timers visible in snapshots before they
94    /// have fired (particularly important for interval timers).
95    ///
96    /// Idempotent: calling `ensure` repeatedly for the same key does not change the count.
97    pub fn ensure(mode: TimerMode, delay: Duration, label: &str) {
98        let delay_ms = Self::delay_ms(delay);
99
100        TIMER_METRICS.with_borrow_mut(|counts| {
101            let key = TimerMetricKey {
102                mode,
103                delay_ms,
104                label: label.to_string(),
105            };
106
107            counts.entry(key).or_insert(0);
108        });
109    }
110
111    /// Increment the scheduling counter for a timer key.
112    ///
113    /// Use this when you want to count how many times a given timer (identified by
114    /// `(mode, delay_ms, label)`) has been scheduled.
115    ///
116    /// This uses saturating arithmetic to avoid overflow.
117    pub fn increment(mode: TimerMode, delay: Duration, label: &str) {
118        let delay_ms = Self::delay_ms(delay);
119
120        TIMER_METRICS.with_borrow_mut(|counts| {
121            let key = TimerMetricKey {
122                mode,
123                delay_ms,
124                label: label.to_string(),
125            };
126
127            let entry = counts.entry(key).or_insert(0);
128            *entry = entry.saturating_add(1);
129        });
130    }
131
132    /// Snapshot all timer scheduling metrics.
133    ///
134    /// Returns the current contents of the metrics table as a vector of entries.
135    /// Callers may sort or page the results as needed at the API layer.
136    #[must_use]
137    pub fn snapshot() -> TimerMetricsSnapshot {
138        TIMER_METRICS.with_borrow(|counts| {
139            counts
140                .iter()
141                .map(|(key, count)| TimerMetricEntry {
142                    mode: key.mode,
143                    delay_ms: key.delay_ms,
144                    label: key.label.clone(),
145                    count: *count,
146                })
147                .collect()
148        })
149    }
150
151    /// Test-only helper: clear all timer metrics.
152    #[cfg(test)]
153    pub fn reset() {
154        TIMER_METRICS.with_borrow_mut(HashMap::clear);
155    }
156}
157
158///
159/// TESTS
160///
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn timer_metrics_track_mode_delay_and_label() {
168        TimerMetrics::reset();
169
170        TimerMetrics::increment(TimerMode::Once, Duration::from_secs(1), "once:a");
171        TimerMetrics::increment(TimerMode::Once, Duration::from_secs(1), "once:a");
172        TimerMetrics::increment(
173            TimerMode::Interval,
174            Duration::from_millis(500),
175            "interval:b",
176        );
177
178        let snapshot = TimerMetrics::snapshot();
179        let mut map: HashMap<(TimerMode, u64, String), u64> = snapshot
180            .into_iter()
181            .map(|entry| ((entry.mode, entry.delay_ms, entry.label), entry.count))
182            .collect();
183
184        assert_eq!(
185            map.remove(&(TimerMode::Once, 1_000, "once:a".to_string())),
186            Some(2)
187        );
188        assert_eq!(
189            map.remove(&(TimerMode::Interval, 500, "interval:b".to_string())),
190            Some(1)
191        );
192        assert!(map.is_empty());
193    }
194}