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 execution 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 timer executions 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 times has a given timer fired?**
66///    - Use [`increment`] when a timer fires (one-shot completion or interval tick).
67///
68/// Note that this type **does** count executions/ticks of interval timers.
69/// Scheduling counts are tracked separately (e.g., via `SystemMetricKind::TimerScheduled`),
70/// and instruction costs are tracked via perf counters, because scheduling and
71/// execution are different signals.
72///
73/// ## Cardinality and labels
74///
75/// Labels are used as metric keys. Keep labels stable and low-cardinality (avoid
76/// embedding principals, IDs, or other high-variance values).
77///
78/// ## Thread safety / runtime model
79///
80/// This uses `thread_local!` storage. On the IC, this is the standard way to maintain
81/// mutable global state without `unsafe`.
82///
83
84pub struct TimerMetrics;
85
86impl TimerMetrics {
87    #[allow(clippy::cast_possible_truncation)]
88    fn delay_ms(delay: Duration) -> u64 {
89        delay.as_millis().min(u128::from(u64::MAX)) as u64
90    }
91
92    /// Ensure a timer key exists in the metrics table with an initial count of `0`.
93    ///
94    /// This is used at **schedule time** to make timers visible in snapshots before they
95    /// have fired (particularly important for interval timers).
96    ///
97    /// Idempotent: calling `ensure` repeatedly for the same key does not change the count.
98    pub fn ensure(mode: TimerMode, delay: Duration, label: &str) {
99        let delay_ms = Self::delay_ms(delay);
100
101        TIMER_METRICS.with_borrow_mut(|counts| {
102            let key = TimerMetricKey {
103                mode,
104                delay_ms,
105                label: label.to_string(),
106            };
107
108            counts.entry(key).or_insert(0);
109        });
110    }
111
112    /// Increment the execution counter for a timer key.
113    ///
114    /// Use this when you want to count how many times a given timer (identified by
115    /// `(mode, delay_ms, label)`) has fired.
116    ///
117    /// This uses saturating arithmetic to avoid overflow.
118    pub fn increment(mode: TimerMode, delay: Duration, label: &str) {
119        let delay_ms = Self::delay_ms(delay);
120
121        TIMER_METRICS.with_borrow_mut(|counts| {
122            let key = TimerMetricKey {
123                mode,
124                delay_ms,
125                label: label.to_string(),
126            };
127
128            let entry = counts.entry(key).or_insert(0);
129            *entry = entry.saturating_add(1);
130        });
131    }
132
133    /// Snapshot all timer execution metrics.
134    ///
135    /// Returns the current contents of the metrics table as a vector of entries.
136    /// Callers may sort or page the results as needed at the API layer.
137    #[must_use]
138    pub fn snapshot() -> TimerMetricsSnapshot {
139        TIMER_METRICS.with_borrow(|counts| {
140            counts
141                .iter()
142                .map(|(key, count)| TimerMetricEntry {
143                    mode: key.mode,
144                    delay_ms: key.delay_ms,
145                    label: key.label.clone(),
146                    count: *count,
147                })
148                .collect()
149        })
150    }
151
152    /// Test-only helper: clear all timer metrics.
153    #[cfg(test)]
154    pub fn reset() {
155        TIMER_METRICS.with_borrow_mut(HashMap::clear);
156    }
157}
158
159///
160/// TESTS
161///
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn timer_metrics_track_mode_delay_and_label() {
169        TimerMetrics::reset();
170
171        TimerMetrics::increment(TimerMode::Once, Duration::from_secs(1), "once:a");
172        TimerMetrics::increment(TimerMode::Once, Duration::from_secs(1), "once:a");
173        TimerMetrics::increment(
174            TimerMode::Interval,
175            Duration::from_millis(500),
176            "interval:b",
177        );
178
179        let snapshot = TimerMetrics::snapshot();
180        let mut map: HashMap<(TimerMode, u64, String), u64> = snapshot
181            .into_iter()
182            .map(|entry| ((entry.mode, entry.delay_ms, entry.label), entry.count))
183            .collect();
184
185        assert_eq!(
186            map.remove(&(TimerMode::Once, 1_000, "once:a".to_string())),
187            Some(2)
188        );
189        assert_eq!(
190            map.remove(&(TimerMode::Interval, 500, "interval:b".to_string())),
191            Some(1)
192        );
193        assert!(map.is_empty());
194    }
195}