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    pub(crate) save_insert_calls: u64,
42    pub(crate) save_update_calls: u64,
43    pub(crate) save_replace_calls: u64,
44    pub(crate) exec_success: u64,
45    pub(crate) exec_error_corruption: u64,
46    pub(crate) exec_error_incompatible_persisted_format: u64,
47    pub(crate) exec_error_not_found: u64,
48    pub(crate) exec_error_internal: u64,
49    pub(crate) exec_error_conflict: u64,
50    pub(crate) exec_error_unsupported: u64,
51    pub(crate) exec_error_invariant_violation: u64,
52    pub(crate) exec_aborted: u64,
53
54    // Planner kinds
55    pub(crate) plan_index: u64,
56    pub(crate) plan_keys: u64,
57    pub(crate) plan_range: u64,
58    pub(crate) plan_full_scan: u64,
59    pub(crate) plan_by_key: u64,
60    pub(crate) plan_by_keys: u64,
61    pub(crate) plan_key_range: u64,
62    pub(crate) plan_index_prefix: u64,
63    pub(crate) plan_index_multi_lookup: u64,
64    pub(crate) plan_index_range: u64,
65    pub(crate) plan_explicit_full_scan: u64,
66    pub(crate) plan_union: u64,
67    pub(crate) plan_intersection: u64,
68    pub(crate) plan_grouped_hash_materialized: u64,
69    pub(crate) plan_grouped_ordered_materialized: u64,
70
71    // Rows touched
72    pub(crate) rows_loaded: u64,
73    pub(crate) rows_saved: u64,
74    pub(crate) rows_inserted: u64,
75    pub(crate) rows_updated: u64,
76    pub(crate) rows_replaced: u64,
77    pub(crate) rows_scanned: u64,
78    pub(crate) rows_filtered: u64,
79    pub(crate) rows_aggregated: u64,
80    pub(crate) rows_emitted: u64,
81    pub(crate) rows_deleted: u64,
82
83    // Index maintenance
84    pub(crate) index_inserts: u64,
85    pub(crate) index_removes: u64,
86    pub(crate) reverse_index_inserts: u64,
87    pub(crate) reverse_index_removes: u64,
88    pub(crate) relation_reverse_lookups: u64,
89    pub(crate) relation_delete_blocks: u64,
90    pub(crate) unique_violations: u64,
91    pub(crate) non_atomic_partial_commits: u64,
92    pub(crate) non_atomic_partial_rows_committed: u64,
93}
94
95impl EventOps {
96    #[must_use]
97    pub const fn load_calls(&self) -> u64 {
98        self.load_calls
99    }
100
101    #[must_use]
102    pub const fn save_calls(&self) -> u64 {
103        self.save_calls
104    }
105
106    #[must_use]
107    pub const fn delete_calls(&self) -> u64 {
108        self.delete_calls
109    }
110
111    #[must_use]
112    pub const fn save_insert_calls(&self) -> u64 {
113        self.save_insert_calls
114    }
115
116    #[must_use]
117    pub const fn save_update_calls(&self) -> u64 {
118        self.save_update_calls
119    }
120
121    #[must_use]
122    pub const fn save_replace_calls(&self) -> u64 {
123        self.save_replace_calls
124    }
125
126    #[must_use]
127    pub const fn exec_success(&self) -> u64 {
128        self.exec_success
129    }
130
131    #[must_use]
132    pub const fn exec_error_corruption(&self) -> u64 {
133        self.exec_error_corruption
134    }
135
136    #[must_use]
137    pub const fn exec_error_incompatible_persisted_format(&self) -> u64 {
138        self.exec_error_incompatible_persisted_format
139    }
140
141    #[must_use]
142    pub const fn exec_error_not_found(&self) -> u64 {
143        self.exec_error_not_found
144    }
145
146    #[must_use]
147    pub const fn exec_error_internal(&self) -> u64 {
148        self.exec_error_internal
149    }
150
151    #[must_use]
152    pub const fn exec_error_conflict(&self) -> u64 {
153        self.exec_error_conflict
154    }
155
156    #[must_use]
157    pub const fn exec_error_unsupported(&self) -> u64 {
158        self.exec_error_unsupported
159    }
160
161    #[must_use]
162    pub const fn exec_error_invariant_violation(&self) -> u64 {
163        self.exec_error_invariant_violation
164    }
165
166    #[must_use]
167    pub const fn exec_aborted(&self) -> u64 {
168        self.exec_aborted
169    }
170
171    #[must_use]
172    pub const fn plan_index(&self) -> u64 {
173        self.plan_index
174    }
175
176    #[must_use]
177    pub const fn plan_keys(&self) -> u64 {
178        self.plan_keys
179    }
180
181    #[must_use]
182    pub const fn plan_range(&self) -> u64 {
183        self.plan_range
184    }
185
186    #[must_use]
187    pub const fn plan_full_scan(&self) -> u64 {
188        self.plan_full_scan
189    }
190
191    #[must_use]
192    pub const fn plan_by_key(&self) -> u64 {
193        self.plan_by_key
194    }
195
196    #[must_use]
197    pub const fn plan_by_keys(&self) -> u64 {
198        self.plan_by_keys
199    }
200
201    #[must_use]
202    pub const fn plan_key_range(&self) -> u64 {
203        self.plan_key_range
204    }
205
206    #[must_use]
207    pub const fn plan_index_prefix(&self) -> u64 {
208        self.plan_index_prefix
209    }
210
211    #[must_use]
212    pub const fn plan_index_multi_lookup(&self) -> u64 {
213        self.plan_index_multi_lookup
214    }
215
216    #[must_use]
217    pub const fn plan_index_range(&self) -> u64 {
218        self.plan_index_range
219    }
220
221    #[must_use]
222    pub const fn plan_explicit_full_scan(&self) -> u64 {
223        self.plan_explicit_full_scan
224    }
225
226    #[must_use]
227    pub const fn plan_union(&self) -> u64 {
228        self.plan_union
229    }
230
231    #[must_use]
232    pub const fn plan_intersection(&self) -> u64 {
233        self.plan_intersection
234    }
235
236    #[must_use]
237    pub const fn plan_grouped_hash_materialized(&self) -> u64 {
238        self.plan_grouped_hash_materialized
239    }
240
241    #[must_use]
242    pub const fn plan_grouped_ordered_materialized(&self) -> u64 {
243        self.plan_grouped_ordered_materialized
244    }
245
246    #[must_use]
247    pub const fn rows_loaded(&self) -> u64 {
248        self.rows_loaded
249    }
250
251    #[must_use]
252    pub const fn rows_saved(&self) -> u64 {
253        self.rows_saved
254    }
255
256    #[must_use]
257    pub const fn rows_inserted(&self) -> u64 {
258        self.rows_inserted
259    }
260
261    #[must_use]
262    pub const fn rows_updated(&self) -> u64 {
263        self.rows_updated
264    }
265
266    #[must_use]
267    pub const fn rows_replaced(&self) -> u64 {
268        self.rows_replaced
269    }
270
271    #[must_use]
272    pub const fn rows_scanned(&self) -> u64 {
273        self.rows_scanned
274    }
275
276    #[must_use]
277    pub const fn rows_filtered(&self) -> u64 {
278        self.rows_filtered
279    }
280
281    #[must_use]
282    pub const fn rows_aggregated(&self) -> u64 {
283        self.rows_aggregated
284    }
285
286    #[must_use]
287    pub const fn rows_emitted(&self) -> u64 {
288        self.rows_emitted
289    }
290
291    #[must_use]
292    pub const fn rows_deleted(&self) -> u64 {
293        self.rows_deleted
294    }
295
296    #[must_use]
297    pub const fn index_inserts(&self) -> u64 {
298        self.index_inserts
299    }
300
301    #[must_use]
302    pub const fn index_removes(&self) -> u64 {
303        self.index_removes
304    }
305
306    #[must_use]
307    pub const fn reverse_index_inserts(&self) -> u64 {
308        self.reverse_index_inserts
309    }
310
311    #[must_use]
312    pub const fn reverse_index_removes(&self) -> u64 {
313        self.reverse_index_removes
314    }
315
316    #[must_use]
317    pub const fn relation_reverse_lookups(&self) -> u64 {
318        self.relation_reverse_lookups
319    }
320
321    #[must_use]
322    pub const fn relation_delete_blocks(&self) -> u64 {
323        self.relation_delete_blocks
324    }
325
326    #[must_use]
327    pub const fn unique_violations(&self) -> u64 {
328        self.unique_violations
329    }
330
331    #[must_use]
332    pub const fn non_atomic_partial_commits(&self) -> u64 {
333        self.non_atomic_partial_commits
334    }
335
336    #[must_use]
337    pub const fn non_atomic_partial_rows_committed(&self) -> u64 {
338        self.non_atomic_partial_rows_committed
339    }
340}
341
342#[derive(Clone, Debug, Default)]
343pub(crate) struct EntityCounters {
344    pub(crate) load_calls: u64,
345    pub(crate) save_calls: u64,
346    pub(crate) delete_calls: u64,
347    pub(crate) save_insert_calls: u64,
348    pub(crate) save_update_calls: u64,
349    pub(crate) save_replace_calls: u64,
350    pub(crate) exec_success: u64,
351    pub(crate) exec_error_corruption: u64,
352    pub(crate) exec_error_incompatible_persisted_format: u64,
353    pub(crate) exec_error_not_found: u64,
354    pub(crate) exec_error_internal: u64,
355    pub(crate) exec_error_conflict: u64,
356    pub(crate) exec_error_unsupported: u64,
357    pub(crate) exec_error_invariant_violation: u64,
358    pub(crate) exec_aborted: u64,
359    pub(crate) plan_index: u64,
360    pub(crate) plan_keys: u64,
361    pub(crate) plan_range: u64,
362    pub(crate) plan_full_scan: u64,
363    pub(crate) plan_by_key: u64,
364    pub(crate) plan_by_keys: u64,
365    pub(crate) plan_key_range: u64,
366    pub(crate) plan_index_prefix: u64,
367    pub(crate) plan_index_multi_lookup: u64,
368    pub(crate) plan_index_range: u64,
369    pub(crate) plan_explicit_full_scan: u64,
370    pub(crate) plan_union: u64,
371    pub(crate) plan_intersection: u64,
372    pub(crate) plan_grouped_hash_materialized: u64,
373    pub(crate) plan_grouped_ordered_materialized: u64,
374    pub(crate) rows_loaded: u64,
375    pub(crate) rows_saved: u64,
376    pub(crate) rows_inserted: u64,
377    pub(crate) rows_updated: u64,
378    pub(crate) rows_replaced: u64,
379    pub(crate) rows_scanned: u64,
380    pub(crate) rows_filtered: u64,
381    pub(crate) rows_aggregated: u64,
382    pub(crate) rows_emitted: u64,
383    pub(crate) rows_deleted: u64,
384    pub(crate) index_inserts: u64,
385    pub(crate) index_removes: u64,
386    pub(crate) reverse_index_inserts: u64,
387    pub(crate) reverse_index_removes: u64,
388    pub(crate) relation_reverse_lookups: u64,
389    pub(crate) relation_delete_blocks: u64,
390    pub(crate) unique_violations: u64,
391    pub(crate) non_atomic_partial_commits: u64,
392    pub(crate) non_atomic_partial_rows_committed: u64,
393}
394
395#[cfg_attr(doc, doc = "EventPerf\n\nInstruction totals and maxima.")]
396#[derive(CandidType, Clone, Debug, Default, Deserialize)]
397pub struct EventPerf {
398    // Instruction totals per executor (ic_cdk::api::performance_counter(1))
399    pub(crate) load_inst_total: u128,
400    pub(crate) save_inst_total: u128,
401    pub(crate) delete_inst_total: u128,
402
403    // Maximum observed instruction deltas
404    pub(crate) load_inst_max: u64,
405    pub(crate) save_inst_max: u64,
406    pub(crate) delete_inst_max: u64,
407}
408
409impl EventPerf {
410    #[must_use]
411    pub const fn new(
412        load_inst_total: u128,
413        save_inst_total: u128,
414        delete_inst_total: u128,
415        load_inst_max: u64,
416        save_inst_max: u64,
417        delete_inst_max: u64,
418    ) -> Self {
419        Self {
420            load_inst_total,
421            save_inst_total,
422            delete_inst_total,
423            load_inst_max,
424            save_inst_max,
425            delete_inst_max,
426        }
427    }
428
429    #[must_use]
430    pub const fn load_inst_total(&self) -> u128 {
431        self.load_inst_total
432    }
433
434    #[must_use]
435    pub const fn save_inst_total(&self) -> u128 {
436        self.save_inst_total
437    }
438
439    #[must_use]
440    pub const fn delete_inst_total(&self) -> u128 {
441        self.delete_inst_total
442    }
443
444    #[must_use]
445    pub const fn load_inst_max(&self) -> u64 {
446        self.load_inst_max
447    }
448
449    #[must_use]
450    pub const fn save_inst_max(&self) -> u64 {
451        self.save_inst_max
452    }
453
454    #[must_use]
455    pub const fn delete_inst_max(&self) -> u64 {
456        self.delete_inst_max
457    }
458}
459
460thread_local! {
461    static EVENT_STATE: RefCell<EventState> = RefCell::new(EventState::default());
462}
463
464// Borrow metrics immutably.
465pub(crate) fn with_state<R>(f: impl FnOnce(&EventState) -> R) -> R {
466    EVENT_STATE.with(|m| f(&m.borrow()))
467}
468
469// Borrow metrics mutably.
470pub(crate) fn with_state_mut<R>(f: impl FnOnce(&mut EventState) -> R) -> R {
471    EVENT_STATE.with(|m| f(&mut m.borrow_mut()))
472}
473
474// Reset all counters (useful in tests).
475pub(super) fn reset() {
476    with_state_mut(|m| *m = EventState::default());
477}
478
479// Reset all event state: counters, perf, and serialize counters.
480pub(crate) fn reset_all() {
481    reset();
482}
483
484// Accumulate instruction counts and track a max.
485pub(super) fn add_instructions(total: &mut u128, max: &mut u64, delta_inst: u64) {
486    *total = total.saturating_add(u128::from(delta_inst));
487    if delta_inst > *max {
488        *max = delta_inst;
489    }
490}
491
492#[cfg_attr(doc, doc = "EventReport\n\nMetrics query payload.")]
493#[derive(CandidType, Clone, Debug, Default, Deserialize)]
494pub struct EventReport {
495    counters: Option<EventCounters>,
496    entity_counters: Vec<EntitySummary>,
497    window_filter_matched: bool,
498    requested_window_start_ms: Option<u64>,
499    active_window_start_ms: u64,
500}
501
502impl EventReport {
503    #[must_use]
504    pub(crate) const fn new(
505        counters: Option<EventCounters>,
506        entity_counters: Vec<EntitySummary>,
507        window_filter_matched: bool,
508        requested_window_start_ms: Option<u64>,
509        active_window_start_ms: u64,
510    ) -> Self {
511        Self {
512            counters,
513            entity_counters,
514            window_filter_matched,
515            requested_window_start_ms,
516            active_window_start_ms,
517        }
518    }
519
520    #[must_use]
521    pub const fn counters(&self) -> Option<&EventCounters> {
522        self.counters.as_ref()
523    }
524
525    #[must_use]
526    pub fn entity_counters(&self) -> &[EntitySummary] {
527        &self.entity_counters
528    }
529
530    #[must_use]
531    pub const fn window_filter_matched(&self) -> bool {
532        self.window_filter_matched
533    }
534
535    #[must_use]
536    pub const fn requested_window_start_ms(&self) -> Option<u64> {
537        self.requested_window_start_ms
538    }
539
540    #[must_use]
541    pub const fn active_window_start_ms(&self) -> u64 {
542        self.active_window_start_ms
543    }
544
545    #[must_use]
546    pub fn into_counters(self) -> Option<EventCounters> {
547        self.counters
548    }
549
550    #[must_use]
551    pub fn into_entity_counters(self) -> Vec<EntitySummary> {
552        self.entity_counters
553    }
554}
555
556//
557// EventCounters
558//
559// Top-level metrics counters returned by `icydb_metrics()`.
560// This keeps aggregate ops/perf totals while leaving per-entity detail to the
561// separate `entity_counters` payload.
562//
563
564#[derive(CandidType, Clone, Debug, Default, Deserialize)]
565pub struct EventCounters {
566    pub(crate) ops: EventOps,
567    pub(crate) perf: EventPerf,
568    pub(crate) window_start_ms: u64,
569    pub(crate) window_end_ms: u64,
570    pub(crate) window_duration_ms: u64,
571}
572
573impl EventCounters {
574    #[must_use]
575    pub(crate) const fn new(
576        ops: EventOps,
577        perf: EventPerf,
578        window_start_ms: u64,
579        window_end_ms: u64,
580    ) -> Self {
581        Self {
582            ops,
583            perf,
584            window_start_ms,
585            window_end_ms,
586            window_duration_ms: window_end_ms.saturating_sub(window_start_ms),
587        }
588    }
589
590    #[must_use]
591    pub const fn ops(&self) -> &EventOps {
592        &self.ops
593    }
594
595    #[must_use]
596    pub const fn perf(&self) -> &EventPerf {
597        &self.perf
598    }
599
600    #[must_use]
601    pub const fn window_start_ms(&self) -> u64 {
602        self.window_start_ms
603    }
604
605    #[must_use]
606    pub const fn window_end_ms(&self) -> u64 {
607        self.window_end_ms
608    }
609
610    #[must_use]
611    pub const fn window_duration_ms(&self) -> u64 {
612        self.window_duration_ms
613    }
614}
615
616#[cfg_attr(doc, doc = "EntitySummary\n\nPer-entity metrics summary.")]
617#[derive(CandidType, Clone, Debug, Default, Deserialize)]
618pub struct EntitySummary {
619    path: String,
620    load_calls: u64,
621    save_calls: u64,
622    delete_calls: u64,
623    save_insert_calls: u64,
624    save_update_calls: u64,
625    save_replace_calls: u64,
626    exec_success: u64,
627    exec_error_corruption: u64,
628    exec_error_incompatible_persisted_format: u64,
629    exec_error_not_found: u64,
630    exec_error_internal: u64,
631    exec_error_conflict: u64,
632    exec_error_unsupported: u64,
633    exec_error_invariant_violation: u64,
634    exec_aborted: u64,
635    plan_index: u64,
636    plan_keys: u64,
637    plan_range: u64,
638    plan_full_scan: u64,
639    plan_by_key: u64,
640    plan_by_keys: u64,
641    plan_key_range: u64,
642    plan_index_prefix: u64,
643    plan_index_multi_lookup: u64,
644    plan_index_range: u64,
645    plan_explicit_full_scan: u64,
646    plan_union: u64,
647    plan_intersection: u64,
648    plan_grouped_hash_materialized: u64,
649    plan_grouped_ordered_materialized: u64,
650    rows_loaded: u64,
651    rows_saved: u64,
652    rows_inserted: u64,
653    rows_updated: u64,
654    rows_replaced: u64,
655    rows_scanned: u64,
656    rows_filtered: u64,
657    rows_aggregated: u64,
658    rows_emitted: u64,
659    rows_deleted: u64,
660    index_inserts: u64,
661    index_removes: u64,
662    reverse_index_inserts: u64,
663    reverse_index_removes: u64,
664    relation_reverse_lookups: u64,
665    relation_delete_blocks: u64,
666    unique_violations: u64,
667    non_atomic_partial_commits: u64,
668    non_atomic_partial_rows_committed: u64,
669}
670
671impl EntitySummary {
672    #[must_use]
673    pub const fn path(&self) -> &str {
674        self.path.as_str()
675    }
676
677    #[must_use]
678    pub const fn load_calls(&self) -> u64 {
679        self.load_calls
680    }
681
682    #[must_use]
683    pub const fn save_calls(&self) -> u64 {
684        self.save_calls
685    }
686
687    #[must_use]
688    pub const fn delete_calls(&self) -> u64 {
689        self.delete_calls
690    }
691
692    #[must_use]
693    pub const fn save_insert_calls(&self) -> u64 {
694        self.save_insert_calls
695    }
696
697    #[must_use]
698    pub const fn save_update_calls(&self) -> u64 {
699        self.save_update_calls
700    }
701
702    #[must_use]
703    pub const fn save_replace_calls(&self) -> u64 {
704        self.save_replace_calls
705    }
706
707    #[must_use]
708    pub const fn exec_success(&self) -> u64 {
709        self.exec_success
710    }
711
712    #[must_use]
713    pub const fn exec_error_corruption(&self) -> u64 {
714        self.exec_error_corruption
715    }
716
717    #[must_use]
718    pub const fn exec_error_incompatible_persisted_format(&self) -> u64 {
719        self.exec_error_incompatible_persisted_format
720    }
721
722    #[must_use]
723    pub const fn exec_error_not_found(&self) -> u64 {
724        self.exec_error_not_found
725    }
726
727    #[must_use]
728    pub const fn exec_error_internal(&self) -> u64 {
729        self.exec_error_internal
730    }
731
732    #[must_use]
733    pub const fn exec_error_conflict(&self) -> u64 {
734        self.exec_error_conflict
735    }
736
737    #[must_use]
738    pub const fn exec_error_unsupported(&self) -> u64 {
739        self.exec_error_unsupported
740    }
741
742    #[must_use]
743    pub const fn exec_error_invariant_violation(&self) -> u64 {
744        self.exec_error_invariant_violation
745    }
746
747    #[must_use]
748    pub const fn exec_aborted(&self) -> u64 {
749        self.exec_aborted
750    }
751
752    #[must_use]
753    pub const fn plan_index(&self) -> u64 {
754        self.plan_index
755    }
756
757    #[must_use]
758    pub const fn plan_keys(&self) -> u64 {
759        self.plan_keys
760    }
761
762    #[must_use]
763    pub const fn plan_range(&self) -> u64 {
764        self.plan_range
765    }
766
767    #[must_use]
768    pub const fn plan_full_scan(&self) -> u64 {
769        self.plan_full_scan
770    }
771
772    #[must_use]
773    pub const fn plan_by_key(&self) -> u64 {
774        self.plan_by_key
775    }
776
777    #[must_use]
778    pub const fn plan_by_keys(&self) -> u64 {
779        self.plan_by_keys
780    }
781
782    #[must_use]
783    pub const fn plan_key_range(&self) -> u64 {
784        self.plan_key_range
785    }
786
787    #[must_use]
788    pub const fn plan_index_prefix(&self) -> u64 {
789        self.plan_index_prefix
790    }
791
792    #[must_use]
793    pub const fn plan_index_multi_lookup(&self) -> u64 {
794        self.plan_index_multi_lookup
795    }
796
797    #[must_use]
798    pub const fn plan_index_range(&self) -> u64 {
799        self.plan_index_range
800    }
801
802    #[must_use]
803    pub const fn plan_explicit_full_scan(&self) -> u64 {
804        self.plan_explicit_full_scan
805    }
806
807    #[must_use]
808    pub const fn plan_union(&self) -> u64 {
809        self.plan_union
810    }
811
812    #[must_use]
813    pub const fn plan_intersection(&self) -> u64 {
814        self.plan_intersection
815    }
816
817    #[must_use]
818    pub const fn plan_grouped_hash_materialized(&self) -> u64 {
819        self.plan_grouped_hash_materialized
820    }
821
822    #[must_use]
823    pub const fn plan_grouped_ordered_materialized(&self) -> u64 {
824        self.plan_grouped_ordered_materialized
825    }
826
827    #[must_use]
828    pub const fn rows_loaded(&self) -> u64 {
829        self.rows_loaded
830    }
831
832    #[must_use]
833    pub const fn rows_saved(&self) -> u64 {
834        self.rows_saved
835    }
836
837    #[must_use]
838    pub const fn rows_inserted(&self) -> u64 {
839        self.rows_inserted
840    }
841
842    #[must_use]
843    pub const fn rows_updated(&self) -> u64 {
844        self.rows_updated
845    }
846
847    #[must_use]
848    pub const fn rows_replaced(&self) -> u64 {
849        self.rows_replaced
850    }
851
852    #[must_use]
853    pub const fn rows_scanned(&self) -> u64 {
854        self.rows_scanned
855    }
856
857    #[must_use]
858    pub const fn rows_filtered(&self) -> u64 {
859        self.rows_filtered
860    }
861
862    #[must_use]
863    pub const fn rows_aggregated(&self) -> u64 {
864        self.rows_aggregated
865    }
866
867    #[must_use]
868    pub const fn rows_emitted(&self) -> u64 {
869        self.rows_emitted
870    }
871
872    #[must_use]
873    pub const fn rows_deleted(&self) -> u64 {
874        self.rows_deleted
875    }
876
877    #[must_use]
878    pub const fn index_inserts(&self) -> u64 {
879        self.index_inserts
880    }
881
882    #[must_use]
883    pub const fn index_removes(&self) -> u64 {
884        self.index_removes
885    }
886
887    #[must_use]
888    pub const fn reverse_index_inserts(&self) -> u64 {
889        self.reverse_index_inserts
890    }
891
892    #[must_use]
893    pub const fn reverse_index_removes(&self) -> u64 {
894        self.reverse_index_removes
895    }
896
897    #[must_use]
898    pub const fn relation_reverse_lookups(&self) -> u64 {
899        self.relation_reverse_lookups
900    }
901
902    #[must_use]
903    pub const fn relation_delete_blocks(&self) -> u64 {
904        self.relation_delete_blocks
905    }
906
907    #[must_use]
908    pub const fn unique_violations(&self) -> u64 {
909        self.unique_violations
910    }
911
912    #[must_use]
913    pub const fn non_atomic_partial_commits(&self) -> u64 {
914        self.non_atomic_partial_commits
915    }
916
917    #[must_use]
918    pub const fn non_atomic_partial_rows_committed(&self) -> u64 {
919        self.non_atomic_partial_rows_committed
920    }
921
922    // Rank entity summaries by all visible activity so write-heavy or
923    // maintenance-heavy entities are not hidden below read-heavy entities.
924    const fn activity_score(&self) -> u64 {
925        self.load_calls
926            .saturating_add(self.save_calls)
927            .saturating_add(self.delete_calls)
928            .saturating_add(self.save_insert_calls)
929            .saturating_add(self.save_update_calls)
930            .saturating_add(self.save_replace_calls)
931            .saturating_add(self.exec_success)
932            .saturating_add(self.exec_error_corruption)
933            .saturating_add(self.exec_error_incompatible_persisted_format)
934            .saturating_add(self.exec_error_not_found)
935            .saturating_add(self.exec_error_internal)
936            .saturating_add(self.exec_error_conflict)
937            .saturating_add(self.exec_error_unsupported)
938            .saturating_add(self.exec_error_invariant_violation)
939            .saturating_add(self.exec_aborted)
940            .saturating_add(self.plan_index)
941            .saturating_add(self.plan_keys)
942            .saturating_add(self.plan_range)
943            .saturating_add(self.plan_full_scan)
944            .saturating_add(self.plan_by_key)
945            .saturating_add(self.plan_by_keys)
946            .saturating_add(self.plan_key_range)
947            .saturating_add(self.plan_index_prefix)
948            .saturating_add(self.plan_index_multi_lookup)
949            .saturating_add(self.plan_index_range)
950            .saturating_add(self.plan_explicit_full_scan)
951            .saturating_add(self.plan_union)
952            .saturating_add(self.plan_intersection)
953            .saturating_add(self.plan_grouped_hash_materialized)
954            .saturating_add(self.plan_grouped_ordered_materialized)
955            .saturating_add(self.rows_loaded)
956            .saturating_add(self.rows_saved)
957            .saturating_add(self.rows_inserted)
958            .saturating_add(self.rows_updated)
959            .saturating_add(self.rows_replaced)
960            .saturating_add(self.rows_scanned)
961            .saturating_add(self.rows_filtered)
962            .saturating_add(self.rows_aggregated)
963            .saturating_add(self.rows_emitted)
964            .saturating_add(self.rows_deleted)
965            .saturating_add(self.index_inserts)
966            .saturating_add(self.index_removes)
967            .saturating_add(self.reverse_index_inserts)
968            .saturating_add(self.reverse_index_removes)
969            .saturating_add(self.relation_reverse_lookups)
970            .saturating_add(self.relation_delete_blocks)
971            .saturating_add(self.unique_violations)
972            .saturating_add(self.non_atomic_partial_commits)
973            .saturating_add(self.non_atomic_partial_rows_committed)
974    }
975}
976
977// Build a metrics report gated by `window_start_ms`.
978//
979// This is a window-start filter:
980// - If `window_start_ms` is `None`, return the current window.
981// - If `window_start_ms <= state.window_start_ms`, return the current window.
982// - If `window_start_ms > state.window_start_ms`, return an empty report.
983//
984// IcyDB stores aggregate counters only, so it cannot produce a precise
985// sub-window report after `state.window_start_ms`.
986#[must_use]
987pub(super) fn report_window_start(window_start_ms: Option<u64>) -> EventReport {
988    let snap = with_state(Clone::clone);
989    if let Some(requested_window_start_ms) = window_start_ms
990        && requested_window_start_ms > snap.window_start_ms
991    {
992        return EventReport::new(
993            None,
994            Vec::new(),
995            false,
996            window_start_ms,
997            snap.window_start_ms,
998        );
999    }
1000
1001    let mut entity_counters: Vec<EntitySummary> = Vec::new();
1002    for (path, ops) in &snap.entities {
1003        entity_counters.push(EntitySummary {
1004            path: path.clone(),
1005            load_calls: ops.load_calls,
1006            save_calls: ops.save_calls,
1007            delete_calls: ops.delete_calls,
1008            save_insert_calls: ops.save_insert_calls,
1009            save_update_calls: ops.save_update_calls,
1010            save_replace_calls: ops.save_replace_calls,
1011            exec_success: ops.exec_success,
1012            exec_error_corruption: ops.exec_error_corruption,
1013            exec_error_incompatible_persisted_format: ops.exec_error_incompatible_persisted_format,
1014            exec_error_not_found: ops.exec_error_not_found,
1015            exec_error_internal: ops.exec_error_internal,
1016            exec_error_conflict: ops.exec_error_conflict,
1017            exec_error_unsupported: ops.exec_error_unsupported,
1018            exec_error_invariant_violation: ops.exec_error_invariant_violation,
1019            exec_aborted: ops.exec_aborted,
1020            plan_index: ops.plan_index,
1021            plan_keys: ops.plan_keys,
1022            plan_range: ops.plan_range,
1023            plan_full_scan: ops.plan_full_scan,
1024            plan_by_key: ops.plan_by_key,
1025            plan_by_keys: ops.plan_by_keys,
1026            plan_key_range: ops.plan_key_range,
1027            plan_index_prefix: ops.plan_index_prefix,
1028            plan_index_multi_lookup: ops.plan_index_multi_lookup,
1029            plan_index_range: ops.plan_index_range,
1030            plan_explicit_full_scan: ops.plan_explicit_full_scan,
1031            plan_union: ops.plan_union,
1032            plan_intersection: ops.plan_intersection,
1033            plan_grouped_hash_materialized: ops.plan_grouped_hash_materialized,
1034            plan_grouped_ordered_materialized: ops.plan_grouped_ordered_materialized,
1035            rows_loaded: ops.rows_loaded,
1036            rows_saved: ops.rows_saved,
1037            rows_inserted: ops.rows_inserted,
1038            rows_updated: ops.rows_updated,
1039            rows_replaced: ops.rows_replaced,
1040            rows_scanned: ops.rows_scanned,
1041            rows_filtered: ops.rows_filtered,
1042            rows_aggregated: ops.rows_aggregated,
1043            rows_emitted: ops.rows_emitted,
1044            rows_deleted: ops.rows_deleted,
1045            index_inserts: ops.index_inserts,
1046            index_removes: ops.index_removes,
1047            reverse_index_inserts: ops.reverse_index_inserts,
1048            reverse_index_removes: ops.reverse_index_removes,
1049            relation_reverse_lookups: ops.relation_reverse_lookups,
1050            relation_delete_blocks: ops.relation_delete_blocks,
1051            unique_violations: ops.unique_violations,
1052            non_atomic_partial_commits: ops.non_atomic_partial_commits,
1053            non_atomic_partial_rows_committed: ops.non_atomic_partial_rows_committed,
1054        });
1055    }
1056
1057    entity_counters.sort_by(|a, b| {
1058        b.activity_score()
1059            .cmp(&a.activity_score())
1060            .then_with(|| b.rows_loaded.cmp(&a.rows_loaded))
1061            .then_with(|| b.rows_saved.cmp(&a.rows_saved))
1062            .then_with(|| b.rows_scanned.cmp(&a.rows_scanned))
1063            .then_with(|| b.rows_deleted.cmp(&a.rows_deleted))
1064            .then_with(|| a.path.cmp(&b.path))
1065    });
1066
1067    EventReport::new(
1068        Some(EventCounters::new(
1069            snap.ops.clone(),
1070            snap.perf.clone(),
1071            snap.window_start_ms,
1072            now_millis(),
1073        )),
1074        entity_counters,
1075        true,
1076        window_start_ms,
1077        snap.window_start_ms,
1078    )
1079}