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}