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