Skip to main content

icydb_core/metrics/
state.rs

1//! Module: metrics::state
2//! Responsibility: mutable runtime metrics state and outward report DTOs.
3//! Does not own: instrumentation call sites or sink routing.
4//! Boundary: in-memory metrics state behind the crate-level sink/report surface.
5//!
6//! Runtime metrics are update-only by contract.
7//! Query-side instrumentation is intentionally not surfaced by `report`, so
8//! query metrics are non-existent by design under IC query semantics.
9
10use candid::CandidType;
11use canic_cdk::utils::time::now_millis;
12use serde::Deserialize;
13use std::{cell::RefCell, collections::BTreeMap};
14
15#[derive(Clone, Debug)]
16pub(crate) struct EventState {
17    pub(crate) ops: EventOps,
18    pub(crate) perf: EventPerf,
19    pub(crate) entities: BTreeMap<String, EntityCounters>,
20    pub(crate) window_start_ms: u64,
21}
22
23impl Default for EventState {
24    fn default() -> Self {
25        Self {
26            ops: EventOps::default(),
27            perf: EventPerf::default(),
28            entities: BTreeMap::new(),
29            window_start_ms: now_millis(),
30        }
31    }
32}
33
34#[cfg_attr(doc, doc = "EventOps\n\nOperation counters.")]
35#[derive(CandidType, Clone, Debug, Default, Deserialize)]
36pub struct EventOps {
37    // Executor entrypoints
38    pub(crate) load_calls: u64,
39    pub(crate) save_calls: u64,
40    pub(crate) delete_calls: u64,
41
42    // Planner kinds
43    pub(crate) plan_index: u64,
44    pub(crate) plan_keys: u64,
45    pub(crate) plan_range: u64,
46    pub(crate) plan_full_scan: u64,
47    pub(crate) plan_grouped_hash_materialized: u64,
48    pub(crate) plan_grouped_ordered_materialized: u64,
49
50    // Rows touched
51    pub(crate) rows_loaded: u64,
52    pub(crate) rows_scanned: u64,
53    pub(crate) rows_filtered: u64,
54    pub(crate) rows_aggregated: u64,
55    pub(crate) rows_emitted: u64,
56    pub(crate) rows_deleted: u64,
57
58    // Index maintenance
59    pub(crate) index_inserts: u64,
60    pub(crate) index_removes: u64,
61    pub(crate) reverse_index_inserts: u64,
62    pub(crate) reverse_index_removes: u64,
63    pub(crate) relation_reverse_lookups: u64,
64    pub(crate) relation_delete_blocks: u64,
65    pub(crate) unique_violations: u64,
66    pub(crate) non_atomic_partial_commits: u64,
67    pub(crate) non_atomic_partial_rows_committed: u64,
68}
69
70impl EventOps {
71    #[must_use]
72    pub const fn load_calls(&self) -> u64 {
73        self.load_calls
74    }
75
76    #[must_use]
77    pub const fn save_calls(&self) -> u64 {
78        self.save_calls
79    }
80
81    #[must_use]
82    pub const fn delete_calls(&self) -> u64 {
83        self.delete_calls
84    }
85
86    #[must_use]
87    pub const fn plan_index(&self) -> u64 {
88        self.plan_index
89    }
90
91    #[must_use]
92    pub const fn plan_keys(&self) -> u64 {
93        self.plan_keys
94    }
95
96    #[must_use]
97    pub const fn plan_range(&self) -> u64 {
98        self.plan_range
99    }
100
101    #[must_use]
102    pub const fn plan_full_scan(&self) -> u64 {
103        self.plan_full_scan
104    }
105
106    #[must_use]
107    pub const fn plan_grouped_hash_materialized(&self) -> u64 {
108        self.plan_grouped_hash_materialized
109    }
110
111    #[must_use]
112    pub const fn plan_grouped_ordered_materialized(&self) -> u64 {
113        self.plan_grouped_ordered_materialized
114    }
115
116    #[must_use]
117    pub const fn rows_loaded(&self) -> u64 {
118        self.rows_loaded
119    }
120
121    #[must_use]
122    pub const fn rows_scanned(&self) -> u64 {
123        self.rows_scanned
124    }
125
126    #[must_use]
127    pub const fn rows_filtered(&self) -> u64 {
128        self.rows_filtered
129    }
130
131    #[must_use]
132    pub const fn rows_aggregated(&self) -> u64 {
133        self.rows_aggregated
134    }
135
136    #[must_use]
137    pub const fn rows_emitted(&self) -> u64 {
138        self.rows_emitted
139    }
140
141    #[must_use]
142    pub const fn rows_deleted(&self) -> u64 {
143        self.rows_deleted
144    }
145
146    #[must_use]
147    pub const fn index_inserts(&self) -> u64 {
148        self.index_inserts
149    }
150
151    #[must_use]
152    pub const fn index_removes(&self) -> u64 {
153        self.index_removes
154    }
155
156    #[must_use]
157    pub const fn reverse_index_inserts(&self) -> u64 {
158        self.reverse_index_inserts
159    }
160
161    #[must_use]
162    pub const fn reverse_index_removes(&self) -> u64 {
163        self.reverse_index_removes
164    }
165
166    #[must_use]
167    pub const fn relation_reverse_lookups(&self) -> u64 {
168        self.relation_reverse_lookups
169    }
170
171    #[must_use]
172    pub const fn relation_delete_blocks(&self) -> u64 {
173        self.relation_delete_blocks
174    }
175
176    #[must_use]
177    pub const fn unique_violations(&self) -> u64 {
178        self.unique_violations
179    }
180
181    #[must_use]
182    pub const fn non_atomic_partial_commits(&self) -> u64 {
183        self.non_atomic_partial_commits
184    }
185
186    #[must_use]
187    pub const fn non_atomic_partial_rows_committed(&self) -> u64 {
188        self.non_atomic_partial_rows_committed
189    }
190}
191
192#[derive(Clone, Debug, Default)]
193pub(crate) struct EntityCounters {
194    pub(crate) load_calls: u64,
195    pub(crate) save_calls: u64,
196    pub(crate) delete_calls: u64,
197    pub(crate) rows_loaded: u64,
198    pub(crate) rows_scanned: u64,
199    pub(crate) rows_filtered: u64,
200    pub(crate) rows_aggregated: u64,
201    pub(crate) rows_emitted: u64,
202    pub(crate) rows_deleted: u64,
203    pub(crate) index_inserts: u64,
204    pub(crate) index_removes: u64,
205    pub(crate) reverse_index_inserts: u64,
206    pub(crate) reverse_index_removes: u64,
207    pub(crate) relation_reverse_lookups: u64,
208    pub(crate) relation_delete_blocks: u64,
209    pub(crate) unique_violations: u64,
210    pub(crate) non_atomic_partial_commits: u64,
211    pub(crate) non_atomic_partial_rows_committed: u64,
212}
213
214#[cfg_attr(doc, doc = "EventPerf\n\nInstruction totals and maxima.")]
215#[derive(CandidType, Clone, Debug, Default, Deserialize)]
216pub struct EventPerf {
217    // Instruction totals per executor (ic_cdk::api::performance_counter(1))
218    pub(crate) load_inst_total: u128,
219    pub(crate) save_inst_total: u128,
220    pub(crate) delete_inst_total: u128,
221
222    // Maximum observed instruction deltas
223    pub(crate) load_inst_max: u64,
224    pub(crate) save_inst_max: u64,
225    pub(crate) delete_inst_max: u64,
226}
227
228impl EventPerf {
229    #[must_use]
230    pub const fn new(
231        load_inst_total: u128,
232        save_inst_total: u128,
233        delete_inst_total: u128,
234        load_inst_max: u64,
235        save_inst_max: u64,
236        delete_inst_max: u64,
237    ) -> Self {
238        Self {
239            load_inst_total,
240            save_inst_total,
241            delete_inst_total,
242            load_inst_max,
243            save_inst_max,
244            delete_inst_max,
245        }
246    }
247
248    #[must_use]
249    pub const fn load_inst_total(&self) -> u128 {
250        self.load_inst_total
251    }
252
253    #[must_use]
254    pub const fn save_inst_total(&self) -> u128 {
255        self.save_inst_total
256    }
257
258    #[must_use]
259    pub const fn delete_inst_total(&self) -> u128 {
260        self.delete_inst_total
261    }
262
263    #[must_use]
264    pub const fn load_inst_max(&self) -> u64 {
265        self.load_inst_max
266    }
267
268    #[must_use]
269    pub const fn save_inst_max(&self) -> u64 {
270        self.save_inst_max
271    }
272
273    #[must_use]
274    pub const fn delete_inst_max(&self) -> u64 {
275        self.delete_inst_max
276    }
277}
278
279thread_local! {
280    static EVENT_STATE: RefCell<EventState> = RefCell::new(EventState::default());
281}
282
283// Borrow metrics immutably.
284pub(crate) fn with_state<R>(f: impl FnOnce(&EventState) -> R) -> R {
285    EVENT_STATE.with(|m| f(&m.borrow()))
286}
287
288// Borrow metrics mutably.
289pub(crate) fn with_state_mut<R>(f: impl FnOnce(&mut EventState) -> R) -> R {
290    EVENT_STATE.with(|m| f(&mut m.borrow_mut()))
291}
292
293// Reset all counters (useful in tests).
294pub(super) fn reset() {
295    with_state_mut(|m| *m = EventState::default());
296}
297
298// Reset all event state: counters, perf, and serialize counters.
299pub(crate) fn reset_all() {
300    reset();
301}
302
303// Accumulate instruction counts and track a max.
304pub(super) fn add_instructions(total: &mut u128, max: &mut u64, delta_inst: u64) {
305    *total = total.saturating_add(u128::from(delta_inst));
306    if delta_inst > *max {
307        *max = delta_inst;
308    }
309}
310
311#[cfg_attr(doc, doc = "EventReport\n\nMetrics query payload.")]
312#[derive(CandidType, Clone, Debug, Default, Deserialize)]
313pub struct EventReport {
314    counters: Option<EventCounters>,
315    entity_counters: Vec<EntitySummary>,
316}
317
318impl EventReport {
319    #[must_use]
320    pub(crate) const fn new(
321        counters: Option<EventCounters>,
322        entity_counters: Vec<EntitySummary>,
323    ) -> Self {
324        Self {
325            counters,
326            entity_counters,
327        }
328    }
329
330    #[must_use]
331    pub const fn counters(&self) -> Option<&EventCounters> {
332        self.counters.as_ref()
333    }
334
335    #[must_use]
336    pub fn entity_counters(&self) -> &[EntitySummary] {
337        &self.entity_counters
338    }
339
340    #[must_use]
341    pub fn into_counters(self) -> Option<EventCounters> {
342        self.counters
343    }
344
345    #[must_use]
346    pub fn into_entity_counters(self) -> Vec<EntitySummary> {
347        self.entity_counters
348    }
349}
350
351//
352// EventCounters
353//
354// Top-level metrics counters returned by `icydb_metrics()`.
355// This keeps aggregate ops/perf totals while leaving per-entity detail to the
356// separate `entity_counters` payload.
357//
358
359#[derive(CandidType, Clone, Debug, Default, Deserialize)]
360pub struct EventCounters {
361    pub(crate) ops: EventOps,
362    pub(crate) perf: EventPerf,
363    pub(crate) window_start_ms: u64,
364}
365
366impl EventCounters {
367    #[must_use]
368    pub(crate) const fn new(ops: EventOps, perf: EventPerf, window_start_ms: u64) -> Self {
369        Self {
370            ops,
371            perf,
372            window_start_ms,
373        }
374    }
375
376    #[must_use]
377    pub const fn ops(&self) -> &EventOps {
378        &self.ops
379    }
380
381    #[must_use]
382    pub const fn perf(&self) -> &EventPerf {
383        &self.perf
384    }
385
386    #[must_use]
387    pub const fn window_start_ms(&self) -> u64 {
388        self.window_start_ms
389    }
390}
391
392#[cfg_attr(doc, doc = "EntitySummary\n\nPer-entity metrics summary.")]
393#[derive(CandidType, Clone, Debug, Default, Deserialize)]
394pub struct EntitySummary {
395    path: String,
396    load_calls: u64,
397    delete_calls: u64,
398    rows_loaded: u64,
399    rows_scanned: u64,
400    rows_deleted: u64,
401}
402
403impl EntitySummary {
404    #[must_use]
405    pub const fn path(&self) -> &str {
406        self.path.as_str()
407    }
408
409    #[must_use]
410    pub const fn load_calls(&self) -> u64 {
411        self.load_calls
412    }
413
414    #[must_use]
415    pub const fn delete_calls(&self) -> u64 {
416        self.delete_calls
417    }
418
419    #[must_use]
420    pub const fn rows_loaded(&self) -> u64 {
421        self.rows_loaded
422    }
423
424    #[must_use]
425    pub const fn rows_scanned(&self) -> u64 {
426        self.rows_scanned
427    }
428
429    #[must_use]
430    pub const fn rows_deleted(&self) -> u64 {
431        self.rows_deleted
432    }
433}
434
435// Build a metrics report gated by `window_start_ms`.
436//
437// This is a window-start filter:
438// - If `window_start_ms` is `None`, return the current window.
439// - If `window_start_ms <= state.window_start_ms`, return the current window.
440// - If `window_start_ms > state.window_start_ms`, return an empty report.
441//
442// IcyDB stores aggregate counters only, so it cannot produce a precise
443// sub-window report after `state.window_start_ms`.
444#[must_use]
445pub(super) fn report_window_start(window_start_ms: Option<u64>) -> EventReport {
446    let snap = with_state(Clone::clone);
447    if let Some(requested_window_start_ms) = window_start_ms
448        && requested_window_start_ms > snap.window_start_ms
449    {
450        return EventReport::default();
451    }
452
453    let mut entity_counters: Vec<EntitySummary> = Vec::new();
454    for (path, ops) in &snap.entities {
455        entity_counters.push(EntitySummary {
456            path: path.clone(),
457            load_calls: ops.load_calls,
458            delete_calls: ops.delete_calls,
459            rows_loaded: ops.rows_loaded,
460            rows_scanned: ops.rows_scanned,
461            rows_deleted: ops.rows_deleted,
462        });
463    }
464
465    entity_counters.sort_by(|a, b| {
466        b.rows_loaded
467            .cmp(&a.rows_loaded)
468            .then_with(|| b.rows_scanned.cmp(&a.rows_scanned))
469            .then_with(|| b.rows_deleted.cmp(&a.rows_deleted))
470            .then_with(|| a.path.cmp(&b.path))
471    });
472
473    EventReport::new(
474        Some(EventCounters::new(
475            snap.ops.clone(),
476            snap.perf.clone(),
477            snap.window_start_ms,
478        )),
479        entity_counters,
480    )
481}