Skip to main content

icydb_core/metrics/
state.rs

1//! Module: metrics::state
2//! Responsibility: module-local ownership and contracts for metrics::state.
3//! Does not own: cross-module orchestration outside this module.
4//! Boundary: exposes this module API while keeping implementation details internal.
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, cmp::Ordering, collections::BTreeMap};
14
15///
16/// EventState
17///
18/// Metrics window state.
19///
20
21#[derive(CandidType, Clone, Debug, Deserialize)]
22pub struct EventState {
23    pub(crate) ops: EventOps,
24    pub(crate) perf: EventPerf,
25    pub(crate) entities: BTreeMap<String, EntityCounters>,
26    pub(crate) window_start_ms: u64,
27}
28
29impl EventState {
30    #[must_use]
31    pub const fn new(
32        ops: EventOps,
33        perf: EventPerf,
34        entities: BTreeMap<String, EntityCounters>,
35        window_start_ms: u64,
36    ) -> Self {
37        Self {
38            ops,
39            perf,
40            entities,
41            window_start_ms,
42        }
43    }
44
45    #[must_use]
46    pub const fn ops(&self) -> &EventOps {
47        &self.ops
48    }
49
50    #[must_use]
51    pub const fn perf(&self) -> &EventPerf {
52        &self.perf
53    }
54
55    #[must_use]
56    pub const fn entities(&self) -> &BTreeMap<String, EntityCounters> {
57        &self.entities
58    }
59
60    #[must_use]
61    pub const fn window_start_ms(&self) -> u64 {
62        self.window_start_ms
63    }
64}
65
66impl Default for EventState {
67    fn default() -> Self {
68        Self {
69            ops: EventOps::default(),
70            perf: EventPerf::default(),
71            entities: BTreeMap::new(),
72            window_start_ms: now_millis(),
73        }
74    }
75}
76
77///
78/// EventOps
79///
80/// Operation counters.
81///
82#[derive(CandidType, Clone, Debug, Default, Deserialize)]
83pub struct EventOps {
84    // Executor entrypoints
85    pub(crate) load_calls: u64,
86    pub(crate) save_calls: u64,
87    pub(crate) delete_calls: u64,
88
89    // Planner kinds
90    pub(crate) plan_index: u64,
91    pub(crate) plan_keys: u64,
92    pub(crate) plan_range: u64,
93    pub(crate) plan_full_scan: u64,
94    pub(crate) plan_grouped_hash_materialized: u64,
95    pub(crate) plan_grouped_ordered_materialized: u64,
96
97    // Rows touched
98    pub(crate) rows_loaded: u64,
99    pub(crate) rows_scanned: u64,
100    pub(crate) rows_filtered: u64,
101    pub(crate) rows_aggregated: u64,
102    pub(crate) rows_emitted: u64,
103    pub(crate) rows_deleted: u64,
104
105    // Index maintenance
106    pub(crate) index_inserts: u64,
107    pub(crate) index_removes: u64,
108    pub(crate) reverse_index_inserts: u64,
109    pub(crate) reverse_index_removes: u64,
110    pub(crate) relation_reverse_lookups: u64,
111    pub(crate) relation_delete_blocks: u64,
112    pub(crate) unique_violations: u64,
113    pub(crate) non_atomic_partial_commits: u64,
114    pub(crate) non_atomic_partial_rows_committed: u64,
115}
116
117impl EventOps {
118    #[must_use]
119    pub const fn load_calls(&self) -> u64 {
120        self.load_calls
121    }
122
123    #[must_use]
124    pub const fn save_calls(&self) -> u64 {
125        self.save_calls
126    }
127
128    #[must_use]
129    pub const fn delete_calls(&self) -> u64 {
130        self.delete_calls
131    }
132
133    #[must_use]
134    pub const fn plan_index(&self) -> u64 {
135        self.plan_index
136    }
137
138    #[must_use]
139    pub const fn plan_keys(&self) -> u64 {
140        self.plan_keys
141    }
142
143    #[must_use]
144    pub const fn plan_range(&self) -> u64 {
145        self.plan_range
146    }
147
148    #[must_use]
149    pub const fn plan_full_scan(&self) -> u64 {
150        self.plan_full_scan
151    }
152
153    #[must_use]
154    pub const fn plan_grouped_hash_materialized(&self) -> u64 {
155        self.plan_grouped_hash_materialized
156    }
157
158    #[must_use]
159    pub const fn plan_grouped_ordered_materialized(&self) -> u64 {
160        self.plan_grouped_ordered_materialized
161    }
162
163    #[must_use]
164    pub const fn rows_loaded(&self) -> u64 {
165        self.rows_loaded
166    }
167
168    #[must_use]
169    pub const fn rows_scanned(&self) -> u64 {
170        self.rows_scanned
171    }
172
173    #[must_use]
174    pub const fn rows_filtered(&self) -> u64 {
175        self.rows_filtered
176    }
177
178    #[must_use]
179    pub const fn rows_aggregated(&self) -> u64 {
180        self.rows_aggregated
181    }
182
183    #[must_use]
184    pub const fn rows_emitted(&self) -> u64 {
185        self.rows_emitted
186    }
187
188    #[must_use]
189    pub const fn rows_deleted(&self) -> u64 {
190        self.rows_deleted
191    }
192
193    #[must_use]
194    pub const fn index_inserts(&self) -> u64 {
195        self.index_inserts
196    }
197
198    #[must_use]
199    pub const fn index_removes(&self) -> u64 {
200        self.index_removes
201    }
202
203    #[must_use]
204    pub const fn reverse_index_inserts(&self) -> u64 {
205        self.reverse_index_inserts
206    }
207
208    #[must_use]
209    pub const fn reverse_index_removes(&self) -> u64 {
210        self.reverse_index_removes
211    }
212
213    #[must_use]
214    pub const fn relation_reverse_lookups(&self) -> u64 {
215        self.relation_reverse_lookups
216    }
217
218    #[must_use]
219    pub const fn relation_delete_blocks(&self) -> u64 {
220        self.relation_delete_blocks
221    }
222
223    #[must_use]
224    pub const fn unique_violations(&self) -> u64 {
225        self.unique_violations
226    }
227
228    #[must_use]
229    pub const fn non_atomic_partial_commits(&self) -> u64 {
230        self.non_atomic_partial_commits
231    }
232
233    #[must_use]
234    pub const fn non_atomic_partial_rows_committed(&self) -> u64 {
235        self.non_atomic_partial_rows_committed
236    }
237}
238
239///
240/// EntityCounters
241///
242/// Per-entity counters.
243///
244
245#[derive(CandidType, Clone, Debug, Default, Deserialize)]
246pub struct EntityCounters {
247    pub(crate) load_calls: u64,
248    pub(crate) save_calls: u64,
249    pub(crate) delete_calls: u64,
250    pub(crate) rows_loaded: u64,
251    pub(crate) rows_scanned: u64,
252    pub(crate) rows_filtered: u64,
253    pub(crate) rows_aggregated: u64,
254    pub(crate) rows_emitted: u64,
255    pub(crate) rows_deleted: u64,
256    pub(crate) index_inserts: u64,
257    pub(crate) index_removes: u64,
258    pub(crate) reverse_index_inserts: u64,
259    pub(crate) reverse_index_removes: u64,
260    pub(crate) relation_reverse_lookups: u64,
261    pub(crate) relation_delete_blocks: u64,
262    pub(crate) unique_violations: u64,
263    pub(crate) non_atomic_partial_commits: u64,
264    pub(crate) non_atomic_partial_rows_committed: u64,
265}
266
267impl EntityCounters {
268    #[must_use]
269    pub const fn load_calls(&self) -> u64 {
270        self.load_calls
271    }
272
273    #[must_use]
274    pub const fn save_calls(&self) -> u64 {
275        self.save_calls
276    }
277
278    #[must_use]
279    pub const fn delete_calls(&self) -> u64 {
280        self.delete_calls
281    }
282
283    #[must_use]
284    pub const fn rows_loaded(&self) -> u64 {
285        self.rows_loaded
286    }
287
288    #[must_use]
289    pub const fn rows_scanned(&self) -> u64 {
290        self.rows_scanned
291    }
292
293    #[must_use]
294    pub const fn rows_filtered(&self) -> u64 {
295        self.rows_filtered
296    }
297
298    #[must_use]
299    pub const fn rows_aggregated(&self) -> u64 {
300        self.rows_aggregated
301    }
302
303    #[must_use]
304    pub const fn rows_emitted(&self) -> u64 {
305        self.rows_emitted
306    }
307
308    #[must_use]
309    pub const fn rows_deleted(&self) -> u64 {
310        self.rows_deleted
311    }
312
313    #[must_use]
314    pub const fn index_inserts(&self) -> u64 {
315        self.index_inserts
316    }
317
318    #[must_use]
319    pub const fn index_removes(&self) -> u64 {
320        self.index_removes
321    }
322
323    #[must_use]
324    pub const fn reverse_index_inserts(&self) -> u64 {
325        self.reverse_index_inserts
326    }
327
328    #[must_use]
329    pub const fn reverse_index_removes(&self) -> u64 {
330        self.reverse_index_removes
331    }
332
333    #[must_use]
334    pub const fn relation_reverse_lookups(&self) -> u64 {
335        self.relation_reverse_lookups
336    }
337
338    #[must_use]
339    pub const fn relation_delete_blocks(&self) -> u64 {
340        self.relation_delete_blocks
341    }
342
343    #[must_use]
344    pub const fn unique_violations(&self) -> u64 {
345        self.unique_violations
346    }
347
348    #[must_use]
349    pub const fn non_atomic_partial_commits(&self) -> u64 {
350        self.non_atomic_partial_commits
351    }
352
353    #[must_use]
354    pub const fn non_atomic_partial_rows_committed(&self) -> u64 {
355        self.non_atomic_partial_rows_committed
356    }
357}
358
359///
360/// EventPerf
361///
362/// Instruction totals and maxima.
363///
364#[derive(CandidType, Clone, Debug, Default, Deserialize)]
365pub struct EventPerf {
366    // Instruction totals per executor (ic_cdk::api::performance_counter(1))
367    pub(crate) load_inst_total: u128,
368    pub(crate) save_inst_total: u128,
369    pub(crate) delete_inst_total: u128,
370
371    // Maximum observed instruction deltas
372    pub(crate) load_inst_max: u64,
373    pub(crate) save_inst_max: u64,
374    pub(crate) delete_inst_max: u64,
375}
376
377impl EventPerf {
378    #[must_use]
379    pub const fn new(
380        load_inst_total: u128,
381        save_inst_total: u128,
382        delete_inst_total: u128,
383        load_inst_max: u64,
384        save_inst_max: u64,
385        delete_inst_max: u64,
386    ) -> Self {
387        Self {
388            load_inst_total,
389            save_inst_total,
390            delete_inst_total,
391            load_inst_max,
392            save_inst_max,
393            delete_inst_max,
394        }
395    }
396
397    #[must_use]
398    pub const fn load_inst_total(&self) -> u128 {
399        self.load_inst_total
400    }
401
402    #[must_use]
403    pub const fn save_inst_total(&self) -> u128 {
404        self.save_inst_total
405    }
406
407    #[must_use]
408    pub const fn delete_inst_total(&self) -> u128 {
409        self.delete_inst_total
410    }
411
412    #[must_use]
413    pub const fn load_inst_max(&self) -> u64 {
414        self.load_inst_max
415    }
416
417    #[must_use]
418    pub const fn save_inst_max(&self) -> u64 {
419        self.save_inst_max
420    }
421
422    #[must_use]
423    pub const fn delete_inst_max(&self) -> u64 {
424        self.delete_inst_max
425    }
426}
427
428thread_local! {
429    static EVENT_STATE: RefCell<EventState> = RefCell::new(EventState::default());
430}
431
432/// Borrow metrics immutably.
433pub(crate) fn with_state<R>(f: impl FnOnce(&EventState) -> R) -> R {
434    EVENT_STATE.with(|m| f(&m.borrow()))
435}
436
437/// Borrow metrics mutably.
438pub(crate) fn with_state_mut<R>(f: impl FnOnce(&mut EventState) -> R) -> R {
439    EVENT_STATE.with(|m| f(&mut m.borrow_mut()))
440}
441
442/// Reset all counters (useful in tests).
443pub(super) fn reset() {
444    with_state_mut(|m| *m = EventState::default());
445}
446
447/// Reset all event state: counters, perf, and serialize counters.
448pub(crate) fn reset_all() {
449    reset();
450}
451
452/// Accumulate instruction counts and track a max.
453pub(super) fn add_instructions(total: &mut u128, max: &mut u64, delta_inst: u64) {
454    *total = total.saturating_add(u128::from(delta_inst));
455    if delta_inst > *max {
456        *max = delta_inst;
457    }
458}
459
460///
461/// EventReport
462///
463/// Metrics query payload.
464///
465
466#[derive(CandidType, Clone, Debug, Default, Deserialize)]
467pub struct EventReport {
468    counters: Option<EventState>,
469    entity_counters: Vec<EntitySummary>,
470}
471
472impl EventReport {
473    #[must_use]
474    pub(crate) const fn new(
475        counters: Option<EventState>,
476        entity_counters: Vec<EntitySummary>,
477    ) -> Self {
478        Self {
479            counters,
480            entity_counters,
481        }
482    }
483
484    #[must_use]
485    pub const fn counters(&self) -> Option<&EventState> {
486        self.counters.as_ref()
487    }
488
489    #[must_use]
490    pub fn entity_counters(&self) -> &[EntitySummary] {
491        &self.entity_counters
492    }
493
494    #[must_use]
495    pub fn into_counters(self) -> Option<EventState> {
496        self.counters
497    }
498
499    #[must_use]
500    pub fn into_entity_counters(self) -> Vec<EntitySummary> {
501        self.entity_counters
502    }
503}
504
505///
506/// EntitySummary
507///
508/// Per-entity metrics summary.
509///
510
511#[derive(CandidType, Clone, Debug, Default, Deserialize)]
512pub struct EntitySummary {
513    path: String,
514    load_calls: u64,
515    delete_calls: u64,
516    rows_loaded: u64,
517    rows_scanned: u64,
518    rows_filtered: u64,
519    rows_aggregated: u64,
520    rows_emitted: u64,
521    rows_deleted: u64,
522    avg_rows_per_load: f64,
523    avg_rows_scanned_per_load: f64,
524    avg_rows_per_delete: f64,
525    index_inserts: u64,
526    index_removes: u64,
527    reverse_index_inserts: u64,
528    reverse_index_removes: u64,
529    relation_reverse_lookups: u64,
530    relation_delete_blocks: u64,
531    unique_violations: u64,
532    non_atomic_partial_commits: u64,
533    non_atomic_partial_rows_committed: u64,
534}
535
536impl EntitySummary {
537    #[must_use]
538    pub const fn path(&self) -> &str {
539        self.path.as_str()
540    }
541
542    #[must_use]
543    pub const fn load_calls(&self) -> u64 {
544        self.load_calls
545    }
546
547    #[must_use]
548    pub const fn delete_calls(&self) -> u64 {
549        self.delete_calls
550    }
551
552    #[must_use]
553    pub const fn rows_loaded(&self) -> u64 {
554        self.rows_loaded
555    }
556
557    #[must_use]
558    pub const fn rows_scanned(&self) -> u64 {
559        self.rows_scanned
560    }
561
562    #[must_use]
563    pub const fn rows_filtered(&self) -> u64 {
564        self.rows_filtered
565    }
566
567    #[must_use]
568    pub const fn rows_aggregated(&self) -> u64 {
569        self.rows_aggregated
570    }
571
572    #[must_use]
573    pub const fn rows_emitted(&self) -> u64 {
574        self.rows_emitted
575    }
576
577    #[must_use]
578    pub const fn rows_deleted(&self) -> u64 {
579        self.rows_deleted
580    }
581
582    #[must_use]
583    pub const fn avg_rows_per_load(&self) -> f64 {
584        self.avg_rows_per_load
585    }
586
587    #[must_use]
588    pub const fn avg_rows_scanned_per_load(&self) -> f64 {
589        self.avg_rows_scanned_per_load
590    }
591
592    #[must_use]
593    pub const fn avg_rows_per_delete(&self) -> f64 {
594        self.avg_rows_per_delete
595    }
596
597    #[must_use]
598    pub const fn index_inserts(&self) -> u64 {
599        self.index_inserts
600    }
601
602    #[must_use]
603    pub const fn index_removes(&self) -> u64 {
604        self.index_removes
605    }
606
607    #[must_use]
608    pub const fn reverse_index_inserts(&self) -> u64 {
609        self.reverse_index_inserts
610    }
611
612    #[must_use]
613    pub const fn reverse_index_removes(&self) -> u64 {
614        self.reverse_index_removes
615    }
616
617    #[must_use]
618    pub const fn relation_reverse_lookups(&self) -> u64 {
619        self.relation_reverse_lookups
620    }
621
622    #[must_use]
623    pub const fn relation_delete_blocks(&self) -> u64 {
624        self.relation_delete_blocks
625    }
626
627    #[must_use]
628    pub const fn unique_violations(&self) -> u64 {
629        self.unique_violations
630    }
631
632    #[must_use]
633    pub const fn non_atomic_partial_commits(&self) -> u64 {
634        self.non_atomic_partial_commits
635    }
636
637    #[must_use]
638    pub const fn non_atomic_partial_rows_committed(&self) -> u64 {
639        self.non_atomic_partial_rows_committed
640    }
641}
642
643/// Build a metrics report gated by `window_start_ms`.
644///
645/// This is a window-start filter:
646/// - If `window_start_ms` is `None`, return the current window.
647/// - If `window_start_ms <= state.window_start_ms`, return the current window.
648/// - If `window_start_ms > state.window_start_ms`, return an empty report.
649///
650/// IcyDB stores aggregate counters only, so it cannot produce a precise
651/// sub-window report after `state.window_start_ms`.
652#[must_use]
653#[expect(clippy::cast_precision_loss)]
654pub(super) fn report_window_start(window_start_ms: Option<u64>) -> EventReport {
655    let snap = with_state(Clone::clone);
656    if let Some(requested_window_start_ms) = window_start_ms
657        && requested_window_start_ms > snap.window_start_ms
658    {
659        return EventReport::default();
660    }
661
662    let mut entity_counters: Vec<EntitySummary> = Vec::new();
663    for (path, ops) in &snap.entities {
664        let avg_load = if ops.load_calls > 0 {
665            ops.rows_loaded as f64 / ops.load_calls as f64
666        } else {
667            0.0
668        };
669        let avg_scanned = if ops.load_calls > 0 {
670            ops.rows_scanned as f64 / ops.load_calls as f64
671        } else {
672            0.0
673        };
674        let avg_delete = if ops.delete_calls > 0 {
675            ops.rows_deleted as f64 / ops.delete_calls as f64
676        } else {
677            0.0
678        };
679
680        entity_counters.push(EntitySummary {
681            path: path.clone(),
682            load_calls: ops.load_calls,
683            delete_calls: ops.delete_calls,
684            rows_loaded: ops.rows_loaded,
685            rows_scanned: ops.rows_scanned,
686            rows_filtered: ops.rows_filtered,
687            rows_aggregated: ops.rows_aggregated,
688            rows_emitted: ops.rows_emitted,
689            rows_deleted: ops.rows_deleted,
690            avg_rows_per_load: avg_load,
691            avg_rows_scanned_per_load: avg_scanned,
692            avg_rows_per_delete: avg_delete,
693            index_inserts: ops.index_inserts,
694            index_removes: ops.index_removes,
695            reverse_index_inserts: ops.reverse_index_inserts,
696            reverse_index_removes: ops.reverse_index_removes,
697            relation_reverse_lookups: ops.relation_reverse_lookups,
698            relation_delete_blocks: ops.relation_delete_blocks,
699            unique_violations: ops.unique_violations,
700            non_atomic_partial_commits: ops.non_atomic_partial_commits,
701            non_atomic_partial_rows_committed: ops.non_atomic_partial_rows_committed,
702        });
703    }
704
705    entity_counters.sort_by(|a, b| {
706        match b
707            .avg_rows_per_load
708            .partial_cmp(&a.avg_rows_per_load)
709            .unwrap_or(Ordering::Equal)
710        {
711            Ordering::Equal => match b.rows_loaded.cmp(&a.rows_loaded) {
712                Ordering::Equal => a.path.cmp(&b.path),
713                other => other,
714            },
715            other => other,
716        }
717    });
718
719    EventReport::new(Some(snap), entity_counters)
720}