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}