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    pub(crate) cache_shared_query_plan_hits: u64,
54    pub(crate) cache_shared_query_plan_misses: u64,
55    pub(crate) cache_shared_query_plan_inserts: u64,
56    pub(crate) cache_shared_query_plan_entries: u64,
57    pub(crate) cache_sql_compiled_command_hits: u64,
58    pub(crate) cache_sql_compiled_command_misses: u64,
59    pub(crate) cache_sql_compiled_command_inserts: u64,
60    pub(crate) cache_sql_compiled_command_entries: u64,
61
62    // Planner kinds
63    pub(crate) plan_index: u64,
64    pub(crate) plan_keys: u64,
65    pub(crate) plan_range: u64,
66    pub(crate) plan_full_scan: u64,
67    pub(crate) plan_by_key: u64,
68    pub(crate) plan_by_keys: u64,
69    pub(crate) plan_key_range: u64,
70    pub(crate) plan_index_prefix: u64,
71    pub(crate) plan_index_multi_lookup: u64,
72    pub(crate) plan_index_range: u64,
73    pub(crate) plan_explicit_full_scan: u64,
74    pub(crate) plan_union: u64,
75    pub(crate) plan_intersection: u64,
76    pub(crate) plan_grouped_hash_materialized: u64,
77    pub(crate) plan_grouped_ordered_materialized: u64,
78
79    // Rows touched
80    pub(crate) rows_loaded: u64,
81    pub(crate) rows_saved: u64,
82    pub(crate) rows_inserted: u64,
83    pub(crate) rows_updated: u64,
84    pub(crate) rows_replaced: u64,
85    pub(crate) rows_scanned: u64,
86    pub(crate) rows_filtered: u64,
87    pub(crate) rows_aggregated: u64,
88    pub(crate) rows_emitted: u64,
89    pub(crate) load_candidate_rows_scanned: u64,
90    pub(crate) load_candidate_rows_filtered: u64,
91    pub(crate) load_result_rows_emitted: u64,
92    pub(crate) rows_deleted: u64,
93    pub(crate) sql_insert_calls: u64,
94    pub(crate) sql_insert_select_calls: u64,
95    pub(crate) sql_update_calls: u64,
96    pub(crate) sql_delete_calls: u64,
97    pub(crate) sql_write_matched_rows: u64,
98    pub(crate) sql_write_mutated_rows: u64,
99    pub(crate) sql_write_returning_rows: u64,
100
101    // Index maintenance
102    pub(crate) index_inserts: u64,
103    pub(crate) index_removes: u64,
104    pub(crate) reverse_index_inserts: u64,
105    pub(crate) reverse_index_removes: u64,
106    pub(crate) relation_reverse_lookups: u64,
107    pub(crate) relation_delete_blocks: u64,
108    pub(crate) write_rows_touched: u64,
109    pub(crate) write_index_entries_changed: u64,
110    pub(crate) write_reverse_index_entries_changed: u64,
111    pub(crate) write_relation_checks: 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 save_insert_calls(&self) -> u64 {
135        self.save_insert_calls
136    }
137
138    #[must_use]
139    pub const fn save_update_calls(&self) -> u64 {
140        self.save_update_calls
141    }
142
143    #[must_use]
144    pub const fn save_replace_calls(&self) -> u64 {
145        self.save_replace_calls
146    }
147
148    #[must_use]
149    pub const fn exec_success(&self) -> u64 {
150        self.exec_success
151    }
152
153    #[must_use]
154    pub const fn exec_error_corruption(&self) -> u64 {
155        self.exec_error_corruption
156    }
157
158    #[must_use]
159    pub const fn exec_error_incompatible_persisted_format(&self) -> u64 {
160        self.exec_error_incompatible_persisted_format
161    }
162
163    #[must_use]
164    pub const fn exec_error_not_found(&self) -> u64 {
165        self.exec_error_not_found
166    }
167
168    #[must_use]
169    pub const fn exec_error_internal(&self) -> u64 {
170        self.exec_error_internal
171    }
172
173    #[must_use]
174    pub const fn exec_error_conflict(&self) -> u64 {
175        self.exec_error_conflict
176    }
177
178    #[must_use]
179    pub const fn exec_error_unsupported(&self) -> u64 {
180        self.exec_error_unsupported
181    }
182
183    #[must_use]
184    pub const fn exec_error_invariant_violation(&self) -> u64 {
185        self.exec_error_invariant_violation
186    }
187
188    #[must_use]
189    pub const fn exec_aborted(&self) -> u64 {
190        self.exec_aborted
191    }
192
193    #[must_use]
194    pub const fn cache_shared_query_plan_hits(&self) -> u64 {
195        self.cache_shared_query_plan_hits
196    }
197
198    #[must_use]
199    pub const fn cache_shared_query_plan_misses(&self) -> u64 {
200        self.cache_shared_query_plan_misses
201    }
202
203    #[must_use]
204    pub const fn cache_shared_query_plan_inserts(&self) -> u64 {
205        self.cache_shared_query_plan_inserts
206    }
207
208    #[must_use]
209    pub const fn cache_shared_query_plan_entries(&self) -> u64 {
210        self.cache_shared_query_plan_entries
211    }
212
213    #[must_use]
214    pub const fn cache_sql_compiled_command_hits(&self) -> u64 {
215        self.cache_sql_compiled_command_hits
216    }
217
218    #[must_use]
219    pub const fn cache_sql_compiled_command_misses(&self) -> u64 {
220        self.cache_sql_compiled_command_misses
221    }
222
223    #[must_use]
224    pub const fn cache_sql_compiled_command_inserts(&self) -> u64 {
225        self.cache_sql_compiled_command_inserts
226    }
227
228    #[must_use]
229    pub const fn cache_sql_compiled_command_entries(&self) -> u64 {
230        self.cache_sql_compiled_command_entries
231    }
232
233    #[must_use]
234    pub const fn plan_index(&self) -> u64 {
235        self.plan_index
236    }
237
238    #[must_use]
239    pub const fn plan_keys(&self) -> u64 {
240        self.plan_keys
241    }
242
243    #[must_use]
244    pub const fn plan_range(&self) -> u64 {
245        self.plan_range
246    }
247
248    #[must_use]
249    pub const fn plan_full_scan(&self) -> u64 {
250        self.plan_full_scan
251    }
252
253    #[must_use]
254    pub const fn plan_by_key(&self) -> u64 {
255        self.plan_by_key
256    }
257
258    #[must_use]
259    pub const fn plan_by_keys(&self) -> u64 {
260        self.plan_by_keys
261    }
262
263    #[must_use]
264    pub const fn plan_key_range(&self) -> u64 {
265        self.plan_key_range
266    }
267
268    #[must_use]
269    pub const fn plan_index_prefix(&self) -> u64 {
270        self.plan_index_prefix
271    }
272
273    #[must_use]
274    pub const fn plan_index_multi_lookup(&self) -> u64 {
275        self.plan_index_multi_lookup
276    }
277
278    #[must_use]
279    pub const fn plan_index_range(&self) -> u64 {
280        self.plan_index_range
281    }
282
283    #[must_use]
284    pub const fn plan_explicit_full_scan(&self) -> u64 {
285        self.plan_explicit_full_scan
286    }
287
288    #[must_use]
289    pub const fn plan_union(&self) -> u64 {
290        self.plan_union
291    }
292
293    #[must_use]
294    pub const fn plan_intersection(&self) -> u64 {
295        self.plan_intersection
296    }
297
298    #[must_use]
299    pub const fn plan_grouped_hash_materialized(&self) -> u64 {
300        self.plan_grouped_hash_materialized
301    }
302
303    #[must_use]
304    pub const fn plan_grouped_ordered_materialized(&self) -> u64 {
305        self.plan_grouped_ordered_materialized
306    }
307
308    #[must_use]
309    pub const fn rows_loaded(&self) -> u64 {
310        self.rows_loaded
311    }
312
313    #[must_use]
314    pub const fn rows_saved(&self) -> u64 {
315        self.rows_saved
316    }
317
318    #[must_use]
319    pub const fn rows_inserted(&self) -> u64 {
320        self.rows_inserted
321    }
322
323    #[must_use]
324    pub const fn rows_updated(&self) -> u64 {
325        self.rows_updated
326    }
327
328    #[must_use]
329    pub const fn rows_replaced(&self) -> u64 {
330        self.rows_replaced
331    }
332
333    #[must_use]
334    pub const fn rows_scanned(&self) -> u64 {
335        self.rows_scanned
336    }
337
338    #[must_use]
339    pub const fn rows_filtered(&self) -> u64 {
340        self.rows_filtered
341    }
342
343    #[must_use]
344    pub const fn rows_aggregated(&self) -> u64 {
345        self.rows_aggregated
346    }
347
348    #[must_use]
349    pub const fn rows_emitted(&self) -> u64 {
350        self.rows_emitted
351    }
352
353    #[must_use]
354    pub const fn load_candidate_rows_scanned(&self) -> u64 {
355        self.load_candidate_rows_scanned
356    }
357
358    #[must_use]
359    pub const fn load_candidate_rows_filtered(&self) -> u64 {
360        self.load_candidate_rows_filtered
361    }
362
363    #[must_use]
364    pub const fn load_result_rows_emitted(&self) -> u64 {
365        self.load_result_rows_emitted
366    }
367
368    #[must_use]
369    pub const fn rows_deleted(&self) -> u64 {
370        self.rows_deleted
371    }
372
373    #[must_use]
374    pub const fn sql_insert_calls(&self) -> u64 {
375        self.sql_insert_calls
376    }
377
378    #[must_use]
379    pub const fn sql_insert_select_calls(&self) -> u64 {
380        self.sql_insert_select_calls
381    }
382
383    #[must_use]
384    pub const fn sql_update_calls(&self) -> u64 {
385        self.sql_update_calls
386    }
387
388    #[must_use]
389    pub const fn sql_delete_calls(&self) -> u64 {
390        self.sql_delete_calls
391    }
392
393    #[must_use]
394    pub const fn sql_write_matched_rows(&self) -> u64 {
395        self.sql_write_matched_rows
396    }
397
398    #[must_use]
399    pub const fn sql_write_mutated_rows(&self) -> u64 {
400        self.sql_write_mutated_rows
401    }
402
403    #[must_use]
404    pub const fn sql_write_returning_rows(&self) -> u64 {
405        self.sql_write_returning_rows
406    }
407
408    #[must_use]
409    pub const fn index_inserts(&self) -> u64 {
410        self.index_inserts
411    }
412
413    #[must_use]
414    pub const fn index_removes(&self) -> u64 {
415        self.index_removes
416    }
417
418    #[must_use]
419    pub const fn reverse_index_inserts(&self) -> u64 {
420        self.reverse_index_inserts
421    }
422
423    #[must_use]
424    pub const fn reverse_index_removes(&self) -> u64 {
425        self.reverse_index_removes
426    }
427
428    #[must_use]
429    pub const fn relation_reverse_lookups(&self) -> u64 {
430        self.relation_reverse_lookups
431    }
432
433    #[must_use]
434    pub const fn relation_delete_blocks(&self) -> u64 {
435        self.relation_delete_blocks
436    }
437
438    #[must_use]
439    pub const fn write_rows_touched(&self) -> u64 {
440        self.write_rows_touched
441    }
442
443    #[must_use]
444    pub const fn write_index_entries_changed(&self) -> u64 {
445        self.write_index_entries_changed
446    }
447
448    #[must_use]
449    pub const fn write_reverse_index_entries_changed(&self) -> u64 {
450        self.write_reverse_index_entries_changed
451    }
452
453    #[must_use]
454    pub const fn write_relation_checks(&self) -> u64 {
455        self.write_relation_checks
456    }
457
458    #[must_use]
459    pub const fn unique_violations(&self) -> u64 {
460        self.unique_violations
461    }
462
463    #[must_use]
464    pub const fn non_atomic_partial_commits(&self) -> u64 {
465        self.non_atomic_partial_commits
466    }
467
468    #[must_use]
469    pub const fn non_atomic_partial_rows_committed(&self) -> u64 {
470        self.non_atomic_partial_rows_committed
471    }
472}
473
474#[derive(Clone, Debug, Default)]
475pub(crate) struct EntityCounters {
476    pub(crate) load_calls: u64,
477    pub(crate) save_calls: u64,
478    pub(crate) delete_calls: u64,
479    pub(crate) save_insert_calls: u64,
480    pub(crate) save_update_calls: u64,
481    pub(crate) save_replace_calls: u64,
482    pub(crate) exec_success: u64,
483    pub(crate) exec_error_corruption: u64,
484    pub(crate) exec_error_incompatible_persisted_format: u64,
485    pub(crate) exec_error_not_found: u64,
486    pub(crate) exec_error_internal: u64,
487    pub(crate) exec_error_conflict: u64,
488    pub(crate) exec_error_unsupported: u64,
489    pub(crate) exec_error_invariant_violation: u64,
490    pub(crate) exec_aborted: u64,
491    pub(crate) cache_shared_query_plan_hits: u64,
492    pub(crate) cache_shared_query_plan_misses: u64,
493    pub(crate) cache_shared_query_plan_inserts: u64,
494    pub(crate) cache_sql_compiled_command_hits: u64,
495    pub(crate) cache_sql_compiled_command_misses: u64,
496    pub(crate) cache_sql_compiled_command_inserts: u64,
497    pub(crate) plan_index: u64,
498    pub(crate) plan_keys: u64,
499    pub(crate) plan_range: u64,
500    pub(crate) plan_full_scan: u64,
501    pub(crate) plan_by_key: u64,
502    pub(crate) plan_by_keys: u64,
503    pub(crate) plan_key_range: u64,
504    pub(crate) plan_index_prefix: u64,
505    pub(crate) plan_index_multi_lookup: u64,
506    pub(crate) plan_index_range: u64,
507    pub(crate) plan_explicit_full_scan: u64,
508    pub(crate) plan_union: u64,
509    pub(crate) plan_intersection: u64,
510    pub(crate) plan_grouped_hash_materialized: u64,
511    pub(crate) plan_grouped_ordered_materialized: u64,
512    pub(crate) rows_loaded: u64,
513    pub(crate) rows_saved: u64,
514    pub(crate) rows_inserted: u64,
515    pub(crate) rows_updated: u64,
516    pub(crate) rows_replaced: u64,
517    pub(crate) rows_scanned: u64,
518    pub(crate) rows_filtered: u64,
519    pub(crate) rows_aggregated: u64,
520    pub(crate) rows_emitted: u64,
521    pub(crate) load_candidate_rows_scanned: u64,
522    pub(crate) load_candidate_rows_filtered: u64,
523    pub(crate) load_result_rows_emitted: u64,
524    pub(crate) rows_deleted: u64,
525    pub(crate) sql_insert_calls: u64,
526    pub(crate) sql_insert_select_calls: u64,
527    pub(crate) sql_update_calls: u64,
528    pub(crate) sql_delete_calls: u64,
529    pub(crate) sql_write_matched_rows: u64,
530    pub(crate) sql_write_mutated_rows: u64,
531    pub(crate) sql_write_returning_rows: u64,
532    pub(crate) index_inserts: u64,
533    pub(crate) index_removes: u64,
534    pub(crate) reverse_index_inserts: u64,
535    pub(crate) reverse_index_removes: u64,
536    pub(crate) relation_reverse_lookups: u64,
537    pub(crate) relation_delete_blocks: u64,
538    pub(crate) write_rows_touched: u64,
539    pub(crate) write_index_entries_changed: u64,
540    pub(crate) write_reverse_index_entries_changed: u64,
541    pub(crate) write_relation_checks: u64,
542    pub(crate) unique_violations: u64,
543    pub(crate) non_atomic_partial_commits: u64,
544    pub(crate) non_atomic_partial_rows_committed: u64,
545}
546
547#[cfg_attr(doc, doc = "EventPerf\n\nInstruction totals and maxima.")]
548#[derive(CandidType, Clone, Debug, Default, Deserialize)]
549pub struct EventPerf {
550    // Instruction totals per executor (ic_cdk::api::performance_counter(1))
551    pub(crate) load_inst_total: u128,
552    pub(crate) save_inst_total: u128,
553    pub(crate) delete_inst_total: u128,
554
555    // Maximum observed instruction deltas
556    pub(crate) load_inst_max: u64,
557    pub(crate) save_inst_max: u64,
558    pub(crate) delete_inst_max: u64,
559}
560
561impl EventPerf {
562    #[must_use]
563    pub const fn new(
564        load_inst_total: u128,
565        save_inst_total: u128,
566        delete_inst_total: u128,
567        load_inst_max: u64,
568        save_inst_max: u64,
569        delete_inst_max: u64,
570    ) -> Self {
571        Self {
572            load_inst_total,
573            save_inst_total,
574            delete_inst_total,
575            load_inst_max,
576            save_inst_max,
577            delete_inst_max,
578        }
579    }
580
581    #[must_use]
582    pub const fn load_inst_total(&self) -> u128 {
583        self.load_inst_total
584    }
585
586    #[must_use]
587    pub const fn save_inst_total(&self) -> u128 {
588        self.save_inst_total
589    }
590
591    #[must_use]
592    pub const fn delete_inst_total(&self) -> u128 {
593        self.delete_inst_total
594    }
595
596    #[must_use]
597    pub const fn load_inst_max(&self) -> u64 {
598        self.load_inst_max
599    }
600
601    #[must_use]
602    pub const fn save_inst_max(&self) -> u64 {
603        self.save_inst_max
604    }
605
606    #[must_use]
607    pub const fn delete_inst_max(&self) -> u64 {
608        self.delete_inst_max
609    }
610}
611
612thread_local! {
613    static EVENT_STATE: RefCell<EventState> = RefCell::new(EventState::default());
614}
615
616// Borrow metrics immutably.
617pub(crate) fn with_state<R>(f: impl FnOnce(&EventState) -> R) -> R {
618    EVENT_STATE.with(|m| f(&m.borrow()))
619}
620
621// Borrow metrics mutably.
622pub(crate) fn with_state_mut<R>(f: impl FnOnce(&mut EventState) -> R) -> R {
623    EVENT_STATE.with(|m| f(&mut m.borrow_mut()))
624}
625
626// Reset all counters (useful in tests).
627pub(super) fn reset() {
628    with_state_mut(|m| *m = EventState::default());
629}
630
631// Reset all event state: counters, perf, and serialize counters.
632pub(crate) fn reset_all() {
633    reset();
634}
635
636// Accumulate instruction counts and track a max.
637pub(super) fn add_instructions(total: &mut u128, max: &mut u64, delta_inst: u64) {
638    *total = total.saturating_add(u128::from(delta_inst));
639    if delta_inst > *max {
640        *max = delta_inst;
641    }
642}
643
644#[cfg_attr(doc, doc = "EventReport\n\nMetrics query payload.")]
645#[derive(CandidType, Clone, Debug, Default, Deserialize)]
646pub struct EventReport {
647    counters: Option<EventCounters>,
648    entity_counters: Vec<EntitySummary>,
649    window_filter_matched: bool,
650    requested_window_start_ms: Option<u64>,
651    active_window_start_ms: u64,
652}
653
654impl EventReport {
655    #[must_use]
656    pub(crate) const fn new(
657        counters: Option<EventCounters>,
658        entity_counters: Vec<EntitySummary>,
659        window_filter_matched: bool,
660        requested_window_start_ms: Option<u64>,
661        active_window_start_ms: u64,
662    ) -> Self {
663        Self {
664            counters,
665            entity_counters,
666            window_filter_matched,
667            requested_window_start_ms,
668            active_window_start_ms,
669        }
670    }
671
672    #[must_use]
673    pub const fn counters(&self) -> Option<&EventCounters> {
674        self.counters.as_ref()
675    }
676
677    #[must_use]
678    pub fn entity_counters(&self) -> &[EntitySummary] {
679        &self.entity_counters
680    }
681
682    #[must_use]
683    pub const fn window_filter_matched(&self) -> bool {
684        self.window_filter_matched
685    }
686
687    #[must_use]
688    pub const fn requested_window_start_ms(&self) -> Option<u64> {
689        self.requested_window_start_ms
690    }
691
692    #[must_use]
693    pub const fn active_window_start_ms(&self) -> u64 {
694        self.active_window_start_ms
695    }
696
697    #[must_use]
698    pub fn into_counters(self) -> Option<EventCounters> {
699        self.counters
700    }
701
702    #[must_use]
703    pub fn into_entity_counters(self) -> Vec<EntitySummary> {
704        self.entity_counters
705    }
706}
707
708//
709// EventCounters
710//
711// Top-level metrics counters returned by `icydb_metrics()`.
712// This keeps aggregate ops/perf totals while leaving per-entity detail to the
713// separate `entity_counters` payload.
714//
715
716#[derive(CandidType, Clone, Debug, Default, Deserialize)]
717pub struct EventCounters {
718    pub(crate) ops: EventOps,
719    pub(crate) perf: EventPerf,
720    pub(crate) window_start_ms: u64,
721    pub(crate) window_end_ms: u64,
722    pub(crate) window_duration_ms: u64,
723}
724
725impl EventCounters {
726    #[must_use]
727    pub(crate) const fn new(
728        ops: EventOps,
729        perf: EventPerf,
730        window_start_ms: u64,
731        window_end_ms: u64,
732    ) -> Self {
733        Self {
734            ops,
735            perf,
736            window_start_ms,
737            window_end_ms,
738            window_duration_ms: window_end_ms.saturating_sub(window_start_ms),
739        }
740    }
741
742    #[must_use]
743    pub const fn ops(&self) -> &EventOps {
744        &self.ops
745    }
746
747    #[must_use]
748    pub const fn perf(&self) -> &EventPerf {
749        &self.perf
750    }
751
752    #[must_use]
753    pub const fn window_start_ms(&self) -> u64 {
754        self.window_start_ms
755    }
756
757    #[must_use]
758    pub const fn window_end_ms(&self) -> u64 {
759        self.window_end_ms
760    }
761
762    #[must_use]
763    pub const fn window_duration_ms(&self) -> u64 {
764        self.window_duration_ms
765    }
766}
767
768#[cfg_attr(doc, doc = "EntitySummary\n\nPer-entity metrics summary.")]
769#[derive(CandidType, Clone, Debug, Default, Deserialize)]
770pub struct EntitySummary {
771    path: String,
772    load_calls: u64,
773    save_calls: u64,
774    delete_calls: u64,
775    save_insert_calls: u64,
776    save_update_calls: u64,
777    save_replace_calls: u64,
778    exec_success: u64,
779    exec_error_corruption: u64,
780    exec_error_incompatible_persisted_format: u64,
781    exec_error_not_found: u64,
782    exec_error_internal: u64,
783    exec_error_conflict: u64,
784    exec_error_unsupported: u64,
785    exec_error_invariant_violation: u64,
786    exec_aborted: u64,
787    cache_shared_query_plan_hits: u64,
788    cache_shared_query_plan_misses: u64,
789    cache_shared_query_plan_inserts: u64,
790    cache_sql_compiled_command_hits: u64,
791    cache_sql_compiled_command_misses: u64,
792    cache_sql_compiled_command_inserts: u64,
793    plan_index: u64,
794    plan_keys: u64,
795    plan_range: u64,
796    plan_full_scan: u64,
797    plan_by_key: u64,
798    plan_by_keys: u64,
799    plan_key_range: u64,
800    plan_index_prefix: u64,
801    plan_index_multi_lookup: u64,
802    plan_index_range: u64,
803    plan_explicit_full_scan: u64,
804    plan_union: u64,
805    plan_intersection: u64,
806    plan_grouped_hash_materialized: u64,
807    plan_grouped_ordered_materialized: u64,
808    rows_loaded: u64,
809    rows_saved: u64,
810    rows_inserted: u64,
811    rows_updated: u64,
812    rows_replaced: u64,
813    rows_scanned: u64,
814    rows_filtered: u64,
815    rows_aggregated: u64,
816    rows_emitted: u64,
817    load_candidate_rows_scanned: u64,
818    load_candidate_rows_filtered: u64,
819    load_result_rows_emitted: u64,
820    rows_deleted: u64,
821    sql_insert_calls: u64,
822    sql_insert_select_calls: u64,
823    sql_update_calls: u64,
824    sql_delete_calls: u64,
825    sql_write_matched_rows: u64,
826    sql_write_mutated_rows: u64,
827    sql_write_returning_rows: u64,
828    index_inserts: u64,
829    index_removes: u64,
830    reverse_index_inserts: u64,
831    reverse_index_removes: u64,
832    relation_reverse_lookups: u64,
833    relation_delete_blocks: u64,
834    write_rows_touched: u64,
835    write_index_entries_changed: u64,
836    write_reverse_index_entries_changed: u64,
837    write_relation_checks: u64,
838    unique_violations: u64,
839    non_atomic_partial_commits: u64,
840    non_atomic_partial_rows_committed: u64,
841}
842
843impl EntitySummary {
844    #[must_use]
845    pub const fn path(&self) -> &str {
846        self.path.as_str()
847    }
848
849    #[must_use]
850    pub const fn load_calls(&self) -> u64 {
851        self.load_calls
852    }
853
854    #[must_use]
855    pub const fn save_calls(&self) -> u64 {
856        self.save_calls
857    }
858
859    #[must_use]
860    pub const fn delete_calls(&self) -> u64 {
861        self.delete_calls
862    }
863
864    #[must_use]
865    pub const fn save_insert_calls(&self) -> u64 {
866        self.save_insert_calls
867    }
868
869    #[must_use]
870    pub const fn save_update_calls(&self) -> u64 {
871        self.save_update_calls
872    }
873
874    #[must_use]
875    pub const fn save_replace_calls(&self) -> u64 {
876        self.save_replace_calls
877    }
878
879    #[must_use]
880    pub const fn exec_success(&self) -> u64 {
881        self.exec_success
882    }
883
884    #[must_use]
885    pub const fn exec_error_corruption(&self) -> u64 {
886        self.exec_error_corruption
887    }
888
889    #[must_use]
890    pub const fn exec_error_incompatible_persisted_format(&self) -> u64 {
891        self.exec_error_incompatible_persisted_format
892    }
893
894    #[must_use]
895    pub const fn exec_error_not_found(&self) -> u64 {
896        self.exec_error_not_found
897    }
898
899    #[must_use]
900    pub const fn exec_error_internal(&self) -> u64 {
901        self.exec_error_internal
902    }
903
904    #[must_use]
905    pub const fn exec_error_conflict(&self) -> u64 {
906        self.exec_error_conflict
907    }
908
909    #[must_use]
910    pub const fn exec_error_unsupported(&self) -> u64 {
911        self.exec_error_unsupported
912    }
913
914    #[must_use]
915    pub const fn exec_error_invariant_violation(&self) -> u64 {
916        self.exec_error_invariant_violation
917    }
918
919    #[must_use]
920    pub const fn exec_aborted(&self) -> u64 {
921        self.exec_aborted
922    }
923
924    #[must_use]
925    pub const fn cache_shared_query_plan_hits(&self) -> u64 {
926        self.cache_shared_query_plan_hits
927    }
928
929    #[must_use]
930    pub const fn cache_shared_query_plan_misses(&self) -> u64 {
931        self.cache_shared_query_plan_misses
932    }
933
934    #[must_use]
935    pub const fn cache_shared_query_plan_inserts(&self) -> u64 {
936        self.cache_shared_query_plan_inserts
937    }
938
939    #[must_use]
940    pub const fn cache_sql_compiled_command_hits(&self) -> u64 {
941        self.cache_sql_compiled_command_hits
942    }
943
944    #[must_use]
945    pub const fn cache_sql_compiled_command_misses(&self) -> u64 {
946        self.cache_sql_compiled_command_misses
947    }
948
949    #[must_use]
950    pub const fn cache_sql_compiled_command_inserts(&self) -> u64 {
951        self.cache_sql_compiled_command_inserts
952    }
953
954    #[must_use]
955    pub const fn plan_index(&self) -> u64 {
956        self.plan_index
957    }
958
959    #[must_use]
960    pub const fn plan_keys(&self) -> u64 {
961        self.plan_keys
962    }
963
964    #[must_use]
965    pub const fn plan_range(&self) -> u64 {
966        self.plan_range
967    }
968
969    #[must_use]
970    pub const fn plan_full_scan(&self) -> u64 {
971        self.plan_full_scan
972    }
973
974    #[must_use]
975    pub const fn plan_by_key(&self) -> u64 {
976        self.plan_by_key
977    }
978
979    #[must_use]
980    pub const fn plan_by_keys(&self) -> u64 {
981        self.plan_by_keys
982    }
983
984    #[must_use]
985    pub const fn plan_key_range(&self) -> u64 {
986        self.plan_key_range
987    }
988
989    #[must_use]
990    pub const fn plan_index_prefix(&self) -> u64 {
991        self.plan_index_prefix
992    }
993
994    #[must_use]
995    pub const fn plan_index_multi_lookup(&self) -> u64 {
996        self.plan_index_multi_lookup
997    }
998
999    #[must_use]
1000    pub const fn plan_index_range(&self) -> u64 {
1001        self.plan_index_range
1002    }
1003
1004    #[must_use]
1005    pub const fn plan_explicit_full_scan(&self) -> u64 {
1006        self.plan_explicit_full_scan
1007    }
1008
1009    #[must_use]
1010    pub const fn plan_union(&self) -> u64 {
1011        self.plan_union
1012    }
1013
1014    #[must_use]
1015    pub const fn plan_intersection(&self) -> u64 {
1016        self.plan_intersection
1017    }
1018
1019    #[must_use]
1020    pub const fn plan_grouped_hash_materialized(&self) -> u64 {
1021        self.plan_grouped_hash_materialized
1022    }
1023
1024    #[must_use]
1025    pub const fn plan_grouped_ordered_materialized(&self) -> u64 {
1026        self.plan_grouped_ordered_materialized
1027    }
1028
1029    #[must_use]
1030    pub const fn rows_loaded(&self) -> u64 {
1031        self.rows_loaded
1032    }
1033
1034    #[must_use]
1035    pub const fn rows_saved(&self) -> u64 {
1036        self.rows_saved
1037    }
1038
1039    #[must_use]
1040    pub const fn rows_inserted(&self) -> u64 {
1041        self.rows_inserted
1042    }
1043
1044    #[must_use]
1045    pub const fn rows_updated(&self) -> u64 {
1046        self.rows_updated
1047    }
1048
1049    #[must_use]
1050    pub const fn rows_replaced(&self) -> u64 {
1051        self.rows_replaced
1052    }
1053
1054    #[must_use]
1055    pub const fn rows_scanned(&self) -> u64 {
1056        self.rows_scanned
1057    }
1058
1059    #[must_use]
1060    pub const fn rows_filtered(&self) -> u64 {
1061        self.rows_filtered
1062    }
1063
1064    #[must_use]
1065    pub const fn rows_aggregated(&self) -> u64 {
1066        self.rows_aggregated
1067    }
1068
1069    #[must_use]
1070    pub const fn rows_emitted(&self) -> u64 {
1071        self.rows_emitted
1072    }
1073
1074    #[must_use]
1075    pub const fn load_candidate_rows_scanned(&self) -> u64 {
1076        self.load_candidate_rows_scanned
1077    }
1078
1079    #[must_use]
1080    pub const fn load_candidate_rows_filtered(&self) -> u64 {
1081        self.load_candidate_rows_filtered
1082    }
1083
1084    #[must_use]
1085    pub const fn load_result_rows_emitted(&self) -> u64 {
1086        self.load_result_rows_emitted
1087    }
1088
1089    #[must_use]
1090    pub const fn rows_deleted(&self) -> u64 {
1091        self.rows_deleted
1092    }
1093
1094    #[must_use]
1095    pub const fn sql_insert_calls(&self) -> u64 {
1096        self.sql_insert_calls
1097    }
1098
1099    #[must_use]
1100    pub const fn sql_insert_select_calls(&self) -> u64 {
1101        self.sql_insert_select_calls
1102    }
1103
1104    #[must_use]
1105    pub const fn sql_update_calls(&self) -> u64 {
1106        self.sql_update_calls
1107    }
1108
1109    #[must_use]
1110    pub const fn sql_delete_calls(&self) -> u64 {
1111        self.sql_delete_calls
1112    }
1113
1114    #[must_use]
1115    pub const fn sql_write_matched_rows(&self) -> u64 {
1116        self.sql_write_matched_rows
1117    }
1118
1119    #[must_use]
1120    pub const fn sql_write_mutated_rows(&self) -> u64 {
1121        self.sql_write_mutated_rows
1122    }
1123
1124    #[must_use]
1125    pub const fn sql_write_returning_rows(&self) -> u64 {
1126        self.sql_write_returning_rows
1127    }
1128
1129    #[must_use]
1130    pub const fn index_inserts(&self) -> u64 {
1131        self.index_inserts
1132    }
1133
1134    #[must_use]
1135    pub const fn index_removes(&self) -> u64 {
1136        self.index_removes
1137    }
1138
1139    #[must_use]
1140    pub const fn reverse_index_inserts(&self) -> u64 {
1141        self.reverse_index_inserts
1142    }
1143
1144    #[must_use]
1145    pub const fn reverse_index_removes(&self) -> u64 {
1146        self.reverse_index_removes
1147    }
1148
1149    #[must_use]
1150    pub const fn relation_reverse_lookups(&self) -> u64 {
1151        self.relation_reverse_lookups
1152    }
1153
1154    #[must_use]
1155    pub const fn relation_delete_blocks(&self) -> u64 {
1156        self.relation_delete_blocks
1157    }
1158
1159    #[must_use]
1160    pub const fn write_rows_touched(&self) -> u64 {
1161        self.write_rows_touched
1162    }
1163
1164    #[must_use]
1165    pub const fn write_index_entries_changed(&self) -> u64 {
1166        self.write_index_entries_changed
1167    }
1168
1169    #[must_use]
1170    pub const fn write_reverse_index_entries_changed(&self) -> u64 {
1171        self.write_reverse_index_entries_changed
1172    }
1173
1174    #[must_use]
1175    pub const fn write_relation_checks(&self) -> u64 {
1176        self.write_relation_checks
1177    }
1178
1179    #[must_use]
1180    pub const fn unique_violations(&self) -> u64 {
1181        self.unique_violations
1182    }
1183
1184    #[must_use]
1185    pub const fn non_atomic_partial_commits(&self) -> u64 {
1186        self.non_atomic_partial_commits
1187    }
1188
1189    #[must_use]
1190    pub const fn non_atomic_partial_rows_committed(&self) -> u64 {
1191        self.non_atomic_partial_rows_committed
1192    }
1193
1194    // Rank entity summaries by all visible activity so write-heavy or
1195    // maintenance-heavy entities are not hidden below read-heavy entities.
1196    const fn activity_score(&self) -> u64 {
1197        self.load_calls
1198            .saturating_add(self.save_calls)
1199            .saturating_add(self.delete_calls)
1200            .saturating_add(self.save_insert_calls)
1201            .saturating_add(self.save_update_calls)
1202            .saturating_add(self.save_replace_calls)
1203            .saturating_add(self.exec_success)
1204            .saturating_add(self.exec_error_corruption)
1205            .saturating_add(self.exec_error_incompatible_persisted_format)
1206            .saturating_add(self.exec_error_not_found)
1207            .saturating_add(self.exec_error_internal)
1208            .saturating_add(self.exec_error_conflict)
1209            .saturating_add(self.exec_error_unsupported)
1210            .saturating_add(self.exec_error_invariant_violation)
1211            .saturating_add(self.exec_aborted)
1212            .saturating_add(self.cache_shared_query_plan_hits)
1213            .saturating_add(self.cache_shared_query_plan_misses)
1214            .saturating_add(self.cache_shared_query_plan_inserts)
1215            .saturating_add(self.cache_sql_compiled_command_hits)
1216            .saturating_add(self.cache_sql_compiled_command_misses)
1217            .saturating_add(self.cache_sql_compiled_command_inserts)
1218            .saturating_add(self.plan_index)
1219            .saturating_add(self.plan_keys)
1220            .saturating_add(self.plan_range)
1221            .saturating_add(self.plan_full_scan)
1222            .saturating_add(self.plan_by_key)
1223            .saturating_add(self.plan_by_keys)
1224            .saturating_add(self.plan_key_range)
1225            .saturating_add(self.plan_index_prefix)
1226            .saturating_add(self.plan_index_multi_lookup)
1227            .saturating_add(self.plan_index_range)
1228            .saturating_add(self.plan_explicit_full_scan)
1229            .saturating_add(self.plan_union)
1230            .saturating_add(self.plan_intersection)
1231            .saturating_add(self.plan_grouped_hash_materialized)
1232            .saturating_add(self.plan_grouped_ordered_materialized)
1233            .saturating_add(self.rows_loaded)
1234            .saturating_add(self.rows_saved)
1235            .saturating_add(self.rows_inserted)
1236            .saturating_add(self.rows_updated)
1237            .saturating_add(self.rows_replaced)
1238            .saturating_add(self.rows_scanned)
1239            .saturating_add(self.rows_filtered)
1240            .saturating_add(self.rows_aggregated)
1241            .saturating_add(self.rows_emitted)
1242            .saturating_add(self.load_candidate_rows_scanned)
1243            .saturating_add(self.load_candidate_rows_filtered)
1244            .saturating_add(self.load_result_rows_emitted)
1245            .saturating_add(self.rows_deleted)
1246            .saturating_add(self.sql_insert_calls)
1247            .saturating_add(self.sql_insert_select_calls)
1248            .saturating_add(self.sql_update_calls)
1249            .saturating_add(self.sql_delete_calls)
1250            .saturating_add(self.sql_write_matched_rows)
1251            .saturating_add(self.sql_write_mutated_rows)
1252            .saturating_add(self.sql_write_returning_rows)
1253            .saturating_add(self.index_inserts)
1254            .saturating_add(self.index_removes)
1255            .saturating_add(self.reverse_index_inserts)
1256            .saturating_add(self.reverse_index_removes)
1257            .saturating_add(self.relation_reverse_lookups)
1258            .saturating_add(self.relation_delete_blocks)
1259            .saturating_add(self.write_rows_touched)
1260            .saturating_add(self.write_index_entries_changed)
1261            .saturating_add(self.write_reverse_index_entries_changed)
1262            .saturating_add(self.write_relation_checks)
1263            .saturating_add(self.unique_violations)
1264            .saturating_add(self.non_atomic_partial_commits)
1265            .saturating_add(self.non_atomic_partial_rows_committed)
1266    }
1267}
1268
1269// Project mutable per-entity counters into the stable report DTO.
1270//
1271// Keeping this projection out of `report_window_start` leaves the window
1272// filtering logic readable while still making every report field explicit.
1273fn entity_summary_from_counters(path: &str, ops: &EntityCounters) -> EntitySummary {
1274    EntitySummary {
1275        path: path.to_string(),
1276        load_calls: ops.load_calls,
1277        save_calls: ops.save_calls,
1278        delete_calls: ops.delete_calls,
1279        save_insert_calls: ops.save_insert_calls,
1280        save_update_calls: ops.save_update_calls,
1281        save_replace_calls: ops.save_replace_calls,
1282        exec_success: ops.exec_success,
1283        exec_error_corruption: ops.exec_error_corruption,
1284        exec_error_incompatible_persisted_format: ops.exec_error_incompatible_persisted_format,
1285        exec_error_not_found: ops.exec_error_not_found,
1286        exec_error_internal: ops.exec_error_internal,
1287        exec_error_conflict: ops.exec_error_conflict,
1288        exec_error_unsupported: ops.exec_error_unsupported,
1289        exec_error_invariant_violation: ops.exec_error_invariant_violation,
1290        exec_aborted: ops.exec_aborted,
1291        cache_shared_query_plan_hits: ops.cache_shared_query_plan_hits,
1292        cache_shared_query_plan_misses: ops.cache_shared_query_plan_misses,
1293        cache_shared_query_plan_inserts: ops.cache_shared_query_plan_inserts,
1294        cache_sql_compiled_command_hits: ops.cache_sql_compiled_command_hits,
1295        cache_sql_compiled_command_misses: ops.cache_sql_compiled_command_misses,
1296        cache_sql_compiled_command_inserts: ops.cache_sql_compiled_command_inserts,
1297        plan_index: ops.plan_index,
1298        plan_keys: ops.plan_keys,
1299        plan_range: ops.plan_range,
1300        plan_full_scan: ops.plan_full_scan,
1301        plan_by_key: ops.plan_by_key,
1302        plan_by_keys: ops.plan_by_keys,
1303        plan_key_range: ops.plan_key_range,
1304        plan_index_prefix: ops.plan_index_prefix,
1305        plan_index_multi_lookup: ops.plan_index_multi_lookup,
1306        plan_index_range: ops.plan_index_range,
1307        plan_explicit_full_scan: ops.plan_explicit_full_scan,
1308        plan_union: ops.plan_union,
1309        plan_intersection: ops.plan_intersection,
1310        plan_grouped_hash_materialized: ops.plan_grouped_hash_materialized,
1311        plan_grouped_ordered_materialized: ops.plan_grouped_ordered_materialized,
1312        rows_loaded: ops.rows_loaded,
1313        rows_saved: ops.rows_saved,
1314        rows_inserted: ops.rows_inserted,
1315        rows_updated: ops.rows_updated,
1316        rows_replaced: ops.rows_replaced,
1317        rows_scanned: ops.rows_scanned,
1318        rows_filtered: ops.rows_filtered,
1319        rows_aggregated: ops.rows_aggregated,
1320        rows_emitted: ops.rows_emitted,
1321        load_candidate_rows_scanned: ops.load_candidate_rows_scanned,
1322        load_candidate_rows_filtered: ops.load_candidate_rows_filtered,
1323        load_result_rows_emitted: ops.load_result_rows_emitted,
1324        rows_deleted: ops.rows_deleted,
1325        sql_insert_calls: ops.sql_insert_calls,
1326        sql_insert_select_calls: ops.sql_insert_select_calls,
1327        sql_update_calls: ops.sql_update_calls,
1328        sql_delete_calls: ops.sql_delete_calls,
1329        sql_write_matched_rows: ops.sql_write_matched_rows,
1330        sql_write_mutated_rows: ops.sql_write_mutated_rows,
1331        sql_write_returning_rows: ops.sql_write_returning_rows,
1332        index_inserts: ops.index_inserts,
1333        index_removes: ops.index_removes,
1334        reverse_index_inserts: ops.reverse_index_inserts,
1335        reverse_index_removes: ops.reverse_index_removes,
1336        relation_reverse_lookups: ops.relation_reverse_lookups,
1337        relation_delete_blocks: ops.relation_delete_blocks,
1338        write_rows_touched: ops.write_rows_touched,
1339        write_index_entries_changed: ops.write_index_entries_changed,
1340        write_reverse_index_entries_changed: ops.write_reverse_index_entries_changed,
1341        write_relation_checks: ops.write_relation_checks,
1342        unique_violations: ops.unique_violations,
1343        non_atomic_partial_commits: ops.non_atomic_partial_commits,
1344        non_atomic_partial_rows_committed: ops.non_atomic_partial_rows_committed,
1345    }
1346}
1347
1348// Build a metrics report gated by `window_start_ms`.
1349//
1350// This is a window-start filter:
1351// - If `window_start_ms` is `None`, return the current window.
1352// - If `window_start_ms <= state.window_start_ms`, return the current window.
1353// - If `window_start_ms > state.window_start_ms`, return an empty report.
1354//
1355// IcyDB stores aggregate counters only, so it cannot produce a precise
1356// sub-window report after `state.window_start_ms`.
1357#[must_use]
1358pub(super) fn report_window_start(window_start_ms: Option<u64>) -> EventReport {
1359    let snap = with_state(Clone::clone);
1360    if let Some(requested_window_start_ms) = window_start_ms
1361        && requested_window_start_ms > snap.window_start_ms
1362    {
1363        return EventReport::new(
1364            None,
1365            Vec::new(),
1366            false,
1367            window_start_ms,
1368            snap.window_start_ms,
1369        );
1370    }
1371
1372    let mut entity_counters: Vec<EntitySummary> = Vec::new();
1373    for (path, ops) in &snap.entities {
1374        entity_counters.push(entity_summary_from_counters(path, ops));
1375    }
1376
1377    entity_counters.sort_by(|a, b| {
1378        b.activity_score()
1379            .cmp(&a.activity_score())
1380            .then_with(|| b.rows_loaded.cmp(&a.rows_loaded))
1381            .then_with(|| b.rows_saved.cmp(&a.rows_saved))
1382            .then_with(|| b.rows_scanned.cmp(&a.rows_scanned))
1383            .then_with(|| b.rows_deleted.cmp(&a.rows_deleted))
1384            .then_with(|| a.path.cmp(&b.path))
1385    });
1386
1387    EventReport::new(
1388        Some(EventCounters::new(
1389            snap.ops.clone(),
1390            snap.perf.clone(),
1391            snap.window_start_ms,
1392            now_millis(),
1393        )),
1394        entity_counters,
1395        true,
1396        window_start_ms,
1397        snap.window_start_ms,
1398    )
1399}