icydb_core/obs/metrics/
mod.rs

1use crate::traits::EntityKind;
2use candid::CandidType;
3use canic::{cdk::api::performance_counter, utils::time};
4use serde::{Deserialize, Serialize};
5use std::cmp::Ordering;
6use std::{cell::RefCell, collections::BTreeMap, marker::PhantomData};
7
8///
9/// Metrics
10/// Ephemeral, in-memory counters and simple perf totals for operations.
11///
12
13#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
14pub struct EventState {
15    pub ops: EventOps,
16    pub perf: EventPerf,
17    pub entities: BTreeMap<String, EntityCounters>,
18    pub since_ms: u64,
19}
20
21impl Default for EventState {
22    fn default() -> Self {
23        Self {
24            ops: EventOps::default(),
25            perf: EventPerf::default(),
26            entities: BTreeMap::new(),
27            since_ms: time::now_millis(),
28        }
29    }
30}
31
32///
33/// EventOps
34///
35
36#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
37pub struct EventOps {
38    // Executor entrypoints
39    pub load_calls: u64,
40    pub save_calls: u64,
41    pub delete_calls: u64,
42
43    // Planner kinds
44    pub plan_index: u64,
45    pub plan_keys: u64,
46    pub plan_range: u64,
47
48    // Rows touched
49    pub rows_loaded: u64,
50    pub rows_deleted: u64,
51
52    // Index maintenance
53    pub index_inserts: u64,
54    pub index_removes: u64,
55    pub unique_violations: u64,
56}
57
58///
59/// EntityOps
60///
61
62#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
63pub struct EntityCounters {
64    pub load_calls: u64,
65    pub save_calls: u64,
66    pub delete_calls: u64,
67    pub rows_loaded: u64,
68    pub rows_deleted: u64,
69    pub index_inserts: u64,
70    pub index_removes: u64,
71    pub unique_violations: u64,
72}
73
74///
75/// Perf
76///
77
78#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
79pub struct EventPerf {
80    // Instruction totals per executor (ic_cdk::api::performance_counter(1))
81    pub load_inst_total: u128,
82    pub save_inst_total: u128,
83    pub delete_inst_total: u128,
84
85    // Maximum observed instruction deltas
86    pub load_inst_max: u64,
87    pub save_inst_max: u64,
88    pub delete_inst_max: u64,
89}
90
91thread_local! {
92    static EVENT_STATE: RefCell<EventState> = RefCell::new(EventState::default());
93}
94
95/// Borrow metrics immutably.
96pub fn with_state<R>(f: impl FnOnce(&EventState) -> R) -> R {
97    EVENT_STATE.with(|m| f(&m.borrow()))
98}
99
100/// Borrow metrics mutably.
101pub fn with_state_mut<R>(f: impl FnOnce(&mut EventState) -> R) -> R {
102    EVENT_STATE.with(|m| f(&mut m.borrow_mut()))
103}
104
105/// Reset all counters (useful in tests).
106pub fn reset() {
107    with_state_mut(|m| *m = EventState::default());
108}
109
110/// Reset all event state: counters, perf, and serialize counters.
111pub fn reset_all() {
112    reset();
113}
114
115/// Accumulate instruction counts and track a max.
116#[allow(clippy::missing_const_for_fn)]
117pub fn add_instructions(total: &mut u128, max: &mut u64, delta_inst: u64) {
118    *total = total.saturating_add(u128::from(delta_inst));
119    if delta_inst > *max {
120        *max = delta_inst;
121    }
122}
123
124///
125/// ExecKind
126///
127
128#[derive(Clone, Copy, Debug)]
129pub enum ExecKind {
130    Load,
131    Save,
132    Delete,
133}
134
135/// Begin an executor timing span and increment call counters.
136/// Returns the start instruction counter value.
137#[must_use]
138pub fn exec_start(kind: ExecKind) -> u64 {
139    with_state_mut(|m| match kind {
140        ExecKind::Load => m.ops.load_calls = m.ops.load_calls.saturating_add(1),
141        ExecKind::Save => m.ops.save_calls = m.ops.save_calls.saturating_add(1),
142        ExecKind::Delete => m.ops.delete_calls = m.ops.delete_calls.saturating_add(1),
143    });
144
145    // Instruction counter (counter_type = 1) is per-message and monotonic.
146    performance_counter(1)
147}
148
149/// Finish an executor timing span and aggregate instruction deltas and row counters.
150pub fn exec_finish(kind: ExecKind, start_inst: u64, rows_touched: u64) {
151    let now = performance_counter(1);
152    let delta = now.saturating_sub(start_inst);
153
154    with_state_mut(|m| match kind {
155        ExecKind::Load => {
156            m.ops.rows_loaded = m.ops.rows_loaded.saturating_add(rows_touched);
157            add_instructions(
158                &mut m.perf.load_inst_total,
159                &mut m.perf.load_inst_max,
160                delta,
161            );
162        }
163        ExecKind::Save => {
164            add_instructions(
165                &mut m.perf.save_inst_total,
166                &mut m.perf.save_inst_max,
167                delta,
168            );
169        }
170        ExecKind::Delete => {
171            m.ops.rows_deleted = m.ops.rows_deleted.saturating_add(rows_touched);
172            add_instructions(
173                &mut m.perf.delete_inst_total,
174                &mut m.perf.delete_inst_max,
175                delta,
176            );
177        }
178    });
179}
180
181/// Per-entity variants using EntityKind::PATH
182#[must_use]
183pub fn exec_start_for<E>(kind: ExecKind) -> u64
184where
185    E: EntityKind,
186{
187    let start = exec_start(kind);
188    with_state_mut(|m| {
189        let entry = m.entities.entry(E::PATH.to_string()).or_default();
190        match kind {
191            ExecKind::Load => entry.load_calls = entry.load_calls.saturating_add(1),
192            ExecKind::Save => entry.save_calls = entry.save_calls.saturating_add(1),
193            ExecKind::Delete => entry.delete_calls = entry.delete_calls.saturating_add(1),
194        }
195    });
196    start
197}
198
199pub fn exec_finish_for<E>(kind: ExecKind, start_inst: u64, rows_touched: u64)
200where
201    E: EntityKind,
202{
203    exec_finish(kind, start_inst, rows_touched);
204    with_state_mut(|m| {
205        let entry = m.entities.entry(E::PATH.to_string()).or_default();
206        match kind {
207            ExecKind::Load => entry.rows_loaded = entry.rows_loaded.saturating_add(rows_touched),
208            ExecKind::Delete => {
209                entry.rows_deleted = entry.rows_deleted.saturating_add(rows_touched);
210            }
211            ExecKind::Save => {}
212        }
213    });
214}
215
216///
217/// Span
218/// RAII guard to simplify metrics instrumentation
219///
220
221pub struct Span<E: EntityKind> {
222    kind: ExecKind,
223    start: u64,
224    rows: u64,
225    finished: bool,
226    _marker: PhantomData<E>,
227}
228
229impl<E: EntityKind> Span<E> {
230    #[must_use]
231    pub fn new(kind: ExecKind) -> Self {
232        Self {
233            kind,
234            start: exec_start_for::<E>(kind),
235            rows: 0,
236            finished: false,
237            _marker: PhantomData,
238        }
239    }
240
241    pub const fn set_rows(&mut self, rows: u64) {
242        self.rows = rows;
243    }
244
245    pub const fn add_rows(&mut self, rows: u64) {
246        self.rows = self.rows.saturating_add(rows);
247    }
248
249    pub fn finish(mut self) {
250        if !self.finished {
251            exec_finish_for::<E>(self.kind, self.start, self.rows);
252            self.finished = true;
253        }
254    }
255}
256
257impl<E: EntityKind> Drop for Span<E> {
258    fn drop(&mut self) {
259        if !self.finished {
260            exec_finish_for::<E>(self.kind, self.start, self.rows);
261            self.finished = true;
262        }
263    }
264}
265
266///
267/// EventReport
268/// Event/counter report; storage snapshot types live in snapshot/storage modules.
269///
270
271#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
272pub struct EventReport {
273    /// Ephemeral runtime counters since `since_ms`.
274    pub counters: Option<EventState>,
275    /// Per-entity ephemeral counters and averages.
276    pub entity_counters: Vec<EntitySummary>,
277}
278
279///
280/// EntitySummary
281///
282
283#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
284pub struct EntitySummary {
285    pub path: String,
286    pub load_calls: u64,
287    pub delete_calls: u64,
288    pub rows_loaded: u64,
289    pub rows_deleted: u64,
290    pub avg_rows_per_load: f64,
291    pub avg_rows_per_delete: f64,
292    pub index_inserts: u64,
293    pub index_removes: u64,
294    pub unique_violations: u64,
295}
296
297/// Increment unique-violation counters globally and for a specific entity type.
298pub fn record_unique_violation_for<E>(m: &mut EventState)
299where
300    E: crate::traits::EntityKind,
301{
302    m.ops.unique_violations = m.ops.unique_violations.saturating_add(1);
303    let entry = m.entities.entry(E::PATH.to_string()).or_default();
304    entry.unique_violations = entry.unique_violations.saturating_add(1);
305}
306
307///
308/// EventSelect
309/// Select which parts of the metrics report to include.
310///
311
312#[derive(CandidType, Clone, Copy, Debug, Deserialize, Serialize)]
313#[allow(clippy::struct_excessive_bools)]
314pub struct EventSelect {
315    pub data: bool,
316    pub index: bool,
317    pub counters: bool,
318    pub entities: bool,
319}
320
321impl EventSelect {
322    #[must_use]
323    pub const fn all() -> Self {
324        Self {
325            data: true,
326            index: true,
327            counters: true,
328            entities: true,
329        }
330    }
331}
332
333impl Default for EventSelect {
334    fn default() -> Self {
335        Self::all()
336    }
337}
338
339/// Build a metrics report by inspecting in-memory counters only.
340#[must_use]
341#[allow(clippy::cast_precision_loss)]
342pub fn report() -> EventReport {
343    let snap = with_state(Clone::clone);
344
345    let mut entity_counters: Vec<EntitySummary> = Vec::new();
346    for (path, ops) in &snap.entities {
347        let avg_load = if ops.load_calls > 0 {
348            ops.rows_loaded as f64 / ops.load_calls as f64
349        } else {
350            0.0
351        };
352        let avg_delete = if ops.delete_calls > 0 {
353            ops.rows_deleted as f64 / ops.delete_calls as f64
354        } else {
355            0.0
356        };
357
358        entity_counters.push(EntitySummary {
359            path: path.clone(),
360            load_calls: ops.load_calls,
361            delete_calls: ops.delete_calls,
362            rows_loaded: ops.rows_loaded,
363            rows_deleted: ops.rows_deleted,
364            avg_rows_per_load: avg_load,
365            avg_rows_per_delete: avg_delete,
366            index_inserts: ops.index_inserts,
367            index_removes: ops.index_removes,
368            unique_violations: ops.unique_violations,
369        });
370    }
371
372    entity_counters.sort_by(|a, b| {
373        match b
374            .avg_rows_per_load
375            .partial_cmp(&a.avg_rows_per_load)
376            .unwrap_or(Ordering::Equal)
377        {
378            Ordering::Equal => match b.rows_loaded.cmp(&a.rows_loaded) {
379                Ordering::Equal => a.path.cmp(&b.path),
380                other => other,
381            },
382            other => other,
383        }
384    });
385
386    EventReport {
387        counters: Some(snap),
388        entity_counters,
389    }
390}