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///
35/// MetricRatio
36///
37/// MetricRatio carries a derived metric as an exact raw numerator and
38/// denominator pair. Callers can choose their own decimal rendering policy
39/// without losing precision inside the canister metrics layer.
40///
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub struct MetricRatio {
43    numerator: u64,
44    denominator: u64,
45}
46
47impl MetricRatio {
48    /// Returns the ratio numerator.
49    #[must_use]
50    pub const fn numerator(&self) -> u64 {
51        self.numerator
52    }
53
54    /// Returns the ratio denominator.
55    #[must_use]
56    pub const fn denominator(&self) -> u64 {
57        self.denominator
58    }
59
60    /// Returns the raw ratio pair.
61    #[must_use]
62    pub const fn into_parts(self) -> (u64, u64) {
63        (self.numerator, self.denominator)
64    }
65}
66
67// Convert raw counter pairs into optional ratio values without encoding a
68// sentinel for "no activity". Consumers can distinguish absent denominators
69// from legitimate zero-valued work.
70const fn ratio(numerator: u64, denominator: u64) -> Option<MetricRatio> {
71    if denominator == 0 {
72        return None;
73    }
74
75    Some(MetricRatio {
76        numerator,
77        denominator,
78    })
79}
80
81#[cfg_attr(doc, doc = "EventOps\n\nOperation counters.")]
82#[derive(CandidType, Clone, Debug, Default, Deserialize)]
83pub struct EventOps {
84    // Executor entrypoints
85    pub(crate) load_calls: u64,
86    pub(crate) save_calls: u64,
87    pub(crate) delete_calls: u64,
88    pub(crate) save_insert_calls: u64,
89    pub(crate) save_update_calls: u64,
90    pub(crate) save_replace_calls: u64,
91    pub(crate) exec_success: u64,
92    pub(crate) exec_error_corruption: u64,
93    pub(crate) exec_error_incompatible_persisted_format: u64,
94    pub(crate) exec_error_not_found: u64,
95    pub(crate) exec_error_internal: u64,
96    pub(crate) exec_error_conflict: u64,
97    pub(crate) exec_error_unsupported: u64,
98    pub(crate) exec_error_invariant_violation: u64,
99    pub(crate) exec_aborted: u64,
100    pub(crate) cache_shared_query_plan_hits: u64,
101    pub(crate) cache_shared_query_plan_misses: u64,
102    pub(crate) cache_shared_query_plan_inserts: u64,
103    pub(crate) cache_shared_query_plan_entries: u64,
104    pub(crate) cache_sql_compiled_command_hits: u64,
105    pub(crate) cache_sql_compiled_command_misses: u64,
106    pub(crate) cache_sql_compiled_command_inserts: u64,
107    pub(crate) cache_sql_compiled_command_entries: u64,
108
109    // Planner kinds
110    pub(crate) plan_index: u64,
111    pub(crate) plan_keys: u64,
112    pub(crate) plan_range: u64,
113    pub(crate) plan_full_scan: u64,
114    pub(crate) plan_by_key: u64,
115    pub(crate) plan_by_keys: u64,
116    pub(crate) plan_key_range: u64,
117    pub(crate) plan_index_prefix: u64,
118    pub(crate) plan_index_multi_lookup: u64,
119    pub(crate) plan_index_range: u64,
120    pub(crate) plan_explicit_full_scan: u64,
121    pub(crate) plan_union: u64,
122    pub(crate) plan_intersection: u64,
123    pub(crate) plan_grouped_hash_materialized: u64,
124    pub(crate) plan_grouped_ordered_materialized: u64,
125
126    // Rows touched
127    pub(crate) rows_loaded: u64,
128    pub(crate) rows_saved: u64,
129    pub(crate) rows_inserted: u64,
130    pub(crate) rows_updated: u64,
131    pub(crate) rows_replaced: u64,
132    pub(crate) rows_scanned: u64,
133    pub(crate) rows_filtered: u64,
134    pub(crate) rows_aggregated: u64,
135    pub(crate) rows_emitted: u64,
136    pub(crate) load_candidate_rows_scanned: u64,
137    pub(crate) load_candidate_rows_filtered: u64,
138    pub(crate) load_result_rows_emitted: u64,
139    pub(crate) rows_deleted: u64,
140    pub(crate) sql_insert_calls: u64,
141    pub(crate) sql_insert_select_calls: u64,
142    pub(crate) sql_update_calls: u64,
143    pub(crate) sql_delete_calls: u64,
144    pub(crate) sql_write_matched_rows: u64,
145    pub(crate) sql_write_mutated_rows: u64,
146    pub(crate) sql_write_returning_rows: u64,
147    pub(crate) sql_write_error_insert: u64,
148    pub(crate) sql_write_error_insert_select: u64,
149    pub(crate) sql_write_error_update: u64,
150    pub(crate) sql_write_error_delete: u64,
151    pub(crate) sql_write_error_corruption: u64,
152    pub(crate) sql_write_error_incompatible_persisted_format: u64,
153    pub(crate) sql_write_error_not_found: u64,
154    pub(crate) sql_write_error_internal: u64,
155    pub(crate) sql_write_error_conflict: u64,
156    pub(crate) sql_write_error_unsupported: u64,
157    pub(crate) sql_write_error_invariant_violation: u64,
158
159    // Index maintenance
160    pub(crate) index_inserts: u64,
161    pub(crate) index_removes: u64,
162    pub(crate) reverse_index_inserts: u64,
163    pub(crate) reverse_index_removes: u64,
164    pub(crate) relation_reverse_lookups: u64,
165    pub(crate) relation_delete_blocks: u64,
166    pub(crate) write_rows_touched: u64,
167    pub(crate) write_index_entries_changed: u64,
168    pub(crate) write_reverse_index_entries_changed: u64,
169    pub(crate) write_relation_checks: u64,
170    pub(crate) unique_violations: u64,
171    pub(crate) non_atomic_partial_commits: u64,
172    pub(crate) non_atomic_partial_rows_committed: u64,
173}
174
175impl EventOps {
176    #[must_use]
177    pub const fn load_calls(&self) -> u64 {
178        self.load_calls
179    }
180
181    #[must_use]
182    pub const fn save_calls(&self) -> u64 {
183        self.save_calls
184    }
185
186    #[must_use]
187    pub const fn delete_calls(&self) -> u64 {
188        self.delete_calls
189    }
190
191    #[must_use]
192    pub const fn save_insert_calls(&self) -> u64 {
193        self.save_insert_calls
194    }
195
196    #[must_use]
197    pub const fn save_update_calls(&self) -> u64 {
198        self.save_update_calls
199    }
200
201    #[must_use]
202    pub const fn save_replace_calls(&self) -> u64 {
203        self.save_replace_calls
204    }
205
206    #[must_use]
207    pub const fn exec_success(&self) -> u64 {
208        self.exec_success
209    }
210
211    #[must_use]
212    pub const fn exec_error_corruption(&self) -> u64 {
213        self.exec_error_corruption
214    }
215
216    #[must_use]
217    pub const fn exec_error_incompatible_persisted_format(&self) -> u64 {
218        self.exec_error_incompatible_persisted_format
219    }
220
221    #[must_use]
222    pub const fn exec_error_not_found(&self) -> u64 {
223        self.exec_error_not_found
224    }
225
226    #[must_use]
227    pub const fn exec_error_internal(&self) -> u64 {
228        self.exec_error_internal
229    }
230
231    #[must_use]
232    pub const fn exec_error_conflict(&self) -> u64 {
233        self.exec_error_conflict
234    }
235
236    #[must_use]
237    pub const fn exec_error_unsupported(&self) -> u64 {
238        self.exec_error_unsupported
239    }
240
241    #[must_use]
242    pub const fn exec_error_invariant_violation(&self) -> u64 {
243        self.exec_error_invariant_violation
244    }
245
246    #[must_use]
247    pub const fn exec_aborted(&self) -> u64 {
248        self.exec_aborted
249    }
250
251    #[must_use]
252    pub const fn cache_shared_query_plan_hits(&self) -> u64 {
253        self.cache_shared_query_plan_hits
254    }
255
256    #[must_use]
257    pub const fn cache_shared_query_plan_misses(&self) -> u64 {
258        self.cache_shared_query_plan_misses
259    }
260
261    #[must_use]
262    pub const fn cache_shared_query_plan_inserts(&self) -> u64 {
263        self.cache_shared_query_plan_inserts
264    }
265
266    #[must_use]
267    pub const fn cache_shared_query_plan_entries(&self) -> u64 {
268        self.cache_shared_query_plan_entries
269    }
270
271    #[must_use]
272    pub const fn cache_sql_compiled_command_hits(&self) -> u64 {
273        self.cache_sql_compiled_command_hits
274    }
275
276    #[must_use]
277    pub const fn cache_sql_compiled_command_misses(&self) -> u64 {
278        self.cache_sql_compiled_command_misses
279    }
280
281    #[must_use]
282    pub const fn cache_sql_compiled_command_inserts(&self) -> u64 {
283        self.cache_sql_compiled_command_inserts
284    }
285
286    #[must_use]
287    pub const fn cache_sql_compiled_command_entries(&self) -> u64 {
288        self.cache_sql_compiled_command_entries
289    }
290
291    #[must_use]
292    pub const fn plan_index(&self) -> u64 {
293        self.plan_index
294    }
295
296    #[must_use]
297    pub const fn plan_keys(&self) -> u64 {
298        self.plan_keys
299    }
300
301    #[must_use]
302    pub const fn plan_range(&self) -> u64 {
303        self.plan_range
304    }
305
306    #[must_use]
307    pub const fn plan_full_scan(&self) -> u64 {
308        self.plan_full_scan
309    }
310
311    #[must_use]
312    pub const fn plan_by_key(&self) -> u64 {
313        self.plan_by_key
314    }
315
316    #[must_use]
317    pub const fn plan_by_keys(&self) -> u64 {
318        self.plan_by_keys
319    }
320
321    #[must_use]
322    pub const fn plan_key_range(&self) -> u64 {
323        self.plan_key_range
324    }
325
326    #[must_use]
327    pub const fn plan_index_prefix(&self) -> u64 {
328        self.plan_index_prefix
329    }
330
331    #[must_use]
332    pub const fn plan_index_multi_lookup(&self) -> u64 {
333        self.plan_index_multi_lookup
334    }
335
336    #[must_use]
337    pub const fn plan_index_range(&self) -> u64 {
338        self.plan_index_range
339    }
340
341    #[must_use]
342    pub const fn plan_explicit_full_scan(&self) -> u64 {
343        self.plan_explicit_full_scan
344    }
345
346    #[must_use]
347    pub const fn plan_union(&self) -> u64 {
348        self.plan_union
349    }
350
351    #[must_use]
352    pub const fn plan_intersection(&self) -> u64 {
353        self.plan_intersection
354    }
355
356    #[must_use]
357    pub const fn plan_grouped_hash_materialized(&self) -> u64 {
358        self.plan_grouped_hash_materialized
359    }
360
361    #[must_use]
362    pub const fn plan_grouped_ordered_materialized(&self) -> u64 {
363        self.plan_grouped_ordered_materialized
364    }
365
366    #[must_use]
367    pub const fn rows_loaded(&self) -> u64 {
368        self.rows_loaded
369    }
370
371    #[must_use]
372    pub const fn rows_saved(&self) -> u64 {
373        self.rows_saved
374    }
375
376    #[must_use]
377    pub const fn rows_inserted(&self) -> u64 {
378        self.rows_inserted
379    }
380
381    #[must_use]
382    pub const fn rows_updated(&self) -> u64 {
383        self.rows_updated
384    }
385
386    #[must_use]
387    pub const fn rows_replaced(&self) -> u64 {
388        self.rows_replaced
389    }
390
391    #[must_use]
392    pub const fn rows_scanned(&self) -> u64 {
393        self.rows_scanned
394    }
395
396    #[must_use]
397    pub const fn rows_filtered(&self) -> u64 {
398        self.rows_filtered
399    }
400
401    #[must_use]
402    pub const fn rows_aggregated(&self) -> u64 {
403        self.rows_aggregated
404    }
405
406    #[must_use]
407    pub const fn rows_emitted(&self) -> u64 {
408        self.rows_emitted
409    }
410
411    #[must_use]
412    pub const fn load_candidate_rows_scanned(&self) -> u64 {
413        self.load_candidate_rows_scanned
414    }
415
416    #[must_use]
417    pub const fn load_candidate_rows_filtered(&self) -> u64 {
418        self.load_candidate_rows_filtered
419    }
420
421    #[must_use]
422    pub const fn load_result_rows_emitted(&self) -> u64 {
423        self.load_result_rows_emitted
424    }
425
426    #[must_use]
427    pub const fn rows_deleted(&self) -> u64 {
428        self.rows_deleted
429    }
430
431    #[must_use]
432    pub const fn sql_insert_calls(&self) -> u64 {
433        self.sql_insert_calls
434    }
435
436    #[must_use]
437    pub const fn sql_insert_select_calls(&self) -> u64 {
438        self.sql_insert_select_calls
439    }
440
441    #[must_use]
442    pub const fn sql_update_calls(&self) -> u64 {
443        self.sql_update_calls
444    }
445
446    #[must_use]
447    pub const fn sql_delete_calls(&self) -> u64 {
448        self.sql_delete_calls
449    }
450
451    #[must_use]
452    pub const fn sql_write_matched_rows(&self) -> u64 {
453        self.sql_write_matched_rows
454    }
455
456    #[must_use]
457    pub const fn sql_write_mutated_rows(&self) -> u64 {
458        self.sql_write_mutated_rows
459    }
460
461    #[must_use]
462    pub const fn sql_write_returning_rows(&self) -> u64 {
463        self.sql_write_returning_rows
464    }
465
466    #[must_use]
467    pub const fn sql_write_error_insert(&self) -> u64 {
468        self.sql_write_error_insert
469    }
470
471    #[must_use]
472    pub const fn sql_write_error_insert_select(&self) -> u64 {
473        self.sql_write_error_insert_select
474    }
475
476    #[must_use]
477    pub const fn sql_write_error_update(&self) -> u64 {
478        self.sql_write_error_update
479    }
480
481    #[must_use]
482    pub const fn sql_write_error_delete(&self) -> u64 {
483        self.sql_write_error_delete
484    }
485
486    #[must_use]
487    pub const fn sql_write_error_corruption(&self) -> u64 {
488        self.sql_write_error_corruption
489    }
490
491    #[must_use]
492    pub const fn sql_write_error_incompatible_persisted_format(&self) -> u64 {
493        self.sql_write_error_incompatible_persisted_format
494    }
495
496    #[must_use]
497    pub const fn sql_write_error_not_found(&self) -> u64 {
498        self.sql_write_error_not_found
499    }
500
501    #[must_use]
502    pub const fn sql_write_error_internal(&self) -> u64 {
503        self.sql_write_error_internal
504    }
505
506    #[must_use]
507    pub const fn sql_write_error_conflict(&self) -> u64 {
508        self.sql_write_error_conflict
509    }
510
511    #[must_use]
512    pub const fn sql_write_error_unsupported(&self) -> u64 {
513        self.sql_write_error_unsupported
514    }
515
516    #[must_use]
517    pub const fn sql_write_error_invariant_violation(&self) -> u64 {
518        self.sql_write_error_invariant_violation
519    }
520
521    #[must_use]
522    pub const fn index_inserts(&self) -> u64 {
523        self.index_inserts
524    }
525
526    #[must_use]
527    pub const fn index_removes(&self) -> u64 {
528        self.index_removes
529    }
530
531    #[must_use]
532    pub const fn reverse_index_inserts(&self) -> u64 {
533        self.reverse_index_inserts
534    }
535
536    #[must_use]
537    pub const fn reverse_index_removes(&self) -> u64 {
538        self.reverse_index_removes
539    }
540
541    #[must_use]
542    pub const fn relation_reverse_lookups(&self) -> u64 {
543        self.relation_reverse_lookups
544    }
545
546    #[must_use]
547    pub const fn relation_delete_blocks(&self) -> u64 {
548        self.relation_delete_blocks
549    }
550
551    #[must_use]
552    pub const fn write_rows_touched(&self) -> u64 {
553        self.write_rows_touched
554    }
555
556    #[must_use]
557    pub const fn write_index_entries_changed(&self) -> u64 {
558        self.write_index_entries_changed
559    }
560
561    #[must_use]
562    pub const fn write_reverse_index_entries_changed(&self) -> u64 {
563        self.write_reverse_index_entries_changed
564    }
565
566    #[must_use]
567    pub const fn write_relation_checks(&self) -> u64 {
568        self.write_relation_checks
569    }
570
571    #[must_use]
572    pub const fn unique_violations(&self) -> u64 {
573        self.unique_violations
574    }
575
576    #[must_use]
577    pub const fn non_atomic_partial_commits(&self) -> u64 {
578        self.non_atomic_partial_commits
579    }
580
581    #[must_use]
582    pub const fn non_atomic_partial_rows_committed(&self) -> u64 {
583        self.non_atomic_partial_rows_committed
584    }
585
586    /// Returns result rows emitted per load candidate row scanned.
587    #[must_use]
588    pub const fn load_selectivity_ratio(&self) -> Option<MetricRatio> {
589        ratio(
590            self.load_result_rows_emitted,
591            self.load_candidate_rows_scanned,
592        )
593    }
594
595    /// Returns candidate rows filtered per load candidate row scanned.
596    #[must_use]
597    pub const fn load_filter_ratio(&self) -> Option<MetricRatio> {
598        ratio(
599            self.load_candidate_rows_filtered,
600            self.load_candidate_rows_scanned,
601        )
602    }
603
604    /// Returns SQL-mutated rows per SQL-matched row.
605    #[must_use]
606    pub const fn sql_write_mutation_ratio(&self) -> Option<MetricRatio> {
607        ratio(self.sql_write_mutated_rows, self.sql_write_matched_rows)
608    }
609
610    /// Returns SQL `RETURNING` rows per SQL-mutated row.
611    #[must_use]
612    pub const fn sql_write_returning_ratio(&self) -> Option<MetricRatio> {
613        ratio(self.sql_write_returning_rows, self.sql_write_mutated_rows)
614    }
615
616    /// Returns primary index entries changed per write row touched.
617    #[must_use]
618    pub const fn write_index_entries_per_row(&self) -> Option<MetricRatio> {
619        ratio(self.write_index_entries_changed, self.write_rows_touched)
620    }
621
622    /// Returns reverse-index entries changed per write row touched.
623    #[must_use]
624    pub const fn write_reverse_index_entries_per_row(&self) -> Option<MetricRatio> {
625        ratio(
626            self.write_reverse_index_entries_changed,
627            self.write_rows_touched,
628        )
629    }
630
631    /// Returns relation checks performed per write row touched.
632    #[must_use]
633    pub const fn write_relation_checks_per_row(&self) -> Option<MetricRatio> {
634        ratio(self.write_relation_checks, self.write_rows_touched)
635    }
636}
637
638#[derive(Clone, Debug, Default)]
639pub(crate) struct EntityCounters {
640    pub(crate) load_calls: u64,
641    pub(crate) save_calls: u64,
642    pub(crate) delete_calls: u64,
643    pub(crate) save_insert_calls: u64,
644    pub(crate) save_update_calls: u64,
645    pub(crate) save_replace_calls: u64,
646    pub(crate) exec_success: u64,
647    pub(crate) exec_error_corruption: u64,
648    pub(crate) exec_error_incompatible_persisted_format: u64,
649    pub(crate) exec_error_not_found: u64,
650    pub(crate) exec_error_internal: u64,
651    pub(crate) exec_error_conflict: u64,
652    pub(crate) exec_error_unsupported: u64,
653    pub(crate) exec_error_invariant_violation: u64,
654    pub(crate) exec_aborted: u64,
655    pub(crate) cache_shared_query_plan_hits: u64,
656    pub(crate) cache_shared_query_plan_misses: u64,
657    pub(crate) cache_shared_query_plan_inserts: u64,
658    pub(crate) cache_sql_compiled_command_hits: u64,
659    pub(crate) cache_sql_compiled_command_misses: u64,
660    pub(crate) cache_sql_compiled_command_inserts: u64,
661    pub(crate) plan_index: u64,
662    pub(crate) plan_keys: u64,
663    pub(crate) plan_range: u64,
664    pub(crate) plan_full_scan: u64,
665    pub(crate) plan_by_key: u64,
666    pub(crate) plan_by_keys: u64,
667    pub(crate) plan_key_range: u64,
668    pub(crate) plan_index_prefix: u64,
669    pub(crate) plan_index_multi_lookup: u64,
670    pub(crate) plan_index_range: u64,
671    pub(crate) plan_explicit_full_scan: u64,
672    pub(crate) plan_union: u64,
673    pub(crate) plan_intersection: u64,
674    pub(crate) plan_grouped_hash_materialized: u64,
675    pub(crate) plan_grouped_ordered_materialized: u64,
676    pub(crate) rows_loaded: u64,
677    pub(crate) rows_saved: u64,
678    pub(crate) rows_inserted: u64,
679    pub(crate) rows_updated: u64,
680    pub(crate) rows_replaced: u64,
681    pub(crate) rows_scanned: u64,
682    pub(crate) rows_filtered: u64,
683    pub(crate) rows_aggregated: u64,
684    pub(crate) rows_emitted: u64,
685    pub(crate) load_candidate_rows_scanned: u64,
686    pub(crate) load_candidate_rows_filtered: u64,
687    pub(crate) load_result_rows_emitted: u64,
688    pub(crate) rows_deleted: u64,
689    pub(crate) sql_insert_calls: u64,
690    pub(crate) sql_insert_select_calls: u64,
691    pub(crate) sql_update_calls: u64,
692    pub(crate) sql_delete_calls: u64,
693    pub(crate) sql_write_matched_rows: u64,
694    pub(crate) sql_write_mutated_rows: u64,
695    pub(crate) sql_write_returning_rows: u64,
696    pub(crate) sql_write_error_insert: u64,
697    pub(crate) sql_write_error_insert_select: u64,
698    pub(crate) sql_write_error_update: u64,
699    pub(crate) sql_write_error_delete: u64,
700    pub(crate) sql_write_error_corruption: u64,
701    pub(crate) sql_write_error_incompatible_persisted_format: u64,
702    pub(crate) sql_write_error_not_found: u64,
703    pub(crate) sql_write_error_internal: u64,
704    pub(crate) sql_write_error_conflict: u64,
705    pub(crate) sql_write_error_unsupported: u64,
706    pub(crate) sql_write_error_invariant_violation: u64,
707    pub(crate) index_inserts: u64,
708    pub(crate) index_removes: u64,
709    pub(crate) reverse_index_inserts: u64,
710    pub(crate) reverse_index_removes: u64,
711    pub(crate) relation_reverse_lookups: u64,
712    pub(crate) relation_delete_blocks: u64,
713    pub(crate) write_rows_touched: u64,
714    pub(crate) write_index_entries_changed: u64,
715    pub(crate) write_reverse_index_entries_changed: u64,
716    pub(crate) write_relation_checks: u64,
717    pub(crate) unique_violations: u64,
718    pub(crate) non_atomic_partial_commits: u64,
719    pub(crate) non_atomic_partial_rows_committed: u64,
720}
721
722#[cfg_attr(doc, doc = "EventPerf\n\nInstruction totals and maxima.")]
723#[derive(CandidType, Clone, Debug, Default, Deserialize)]
724pub struct EventPerf {
725    // Instruction totals per executor (ic_cdk::api::performance_counter(1))
726    pub(crate) load_inst_total: u128,
727    pub(crate) save_inst_total: u128,
728    pub(crate) delete_inst_total: u128,
729
730    // Maximum observed instruction deltas
731    pub(crate) load_inst_max: u64,
732    pub(crate) save_inst_max: u64,
733    pub(crate) delete_inst_max: u64,
734}
735
736impl EventPerf {
737    #[must_use]
738    pub const fn new(
739        load_inst_total: u128,
740        save_inst_total: u128,
741        delete_inst_total: u128,
742        load_inst_max: u64,
743        save_inst_max: u64,
744        delete_inst_max: u64,
745    ) -> Self {
746        Self {
747            load_inst_total,
748            save_inst_total,
749            delete_inst_total,
750            load_inst_max,
751            save_inst_max,
752            delete_inst_max,
753        }
754    }
755
756    #[must_use]
757    pub const fn load_inst_total(&self) -> u128 {
758        self.load_inst_total
759    }
760
761    #[must_use]
762    pub const fn save_inst_total(&self) -> u128 {
763        self.save_inst_total
764    }
765
766    #[must_use]
767    pub const fn delete_inst_total(&self) -> u128 {
768        self.delete_inst_total
769    }
770
771    #[must_use]
772    pub const fn load_inst_max(&self) -> u64 {
773        self.load_inst_max
774    }
775
776    #[must_use]
777    pub const fn save_inst_max(&self) -> u64 {
778        self.save_inst_max
779    }
780
781    #[must_use]
782    pub const fn delete_inst_max(&self) -> u64 {
783        self.delete_inst_max
784    }
785}
786
787thread_local! {
788    static EVENT_STATE: RefCell<EventState> = RefCell::new(EventState::default());
789}
790
791// Borrow metrics immutably.
792pub(crate) fn with_state<R>(f: impl FnOnce(&EventState) -> R) -> R {
793    EVENT_STATE.with(|m| f(&m.borrow()))
794}
795
796// Borrow metrics mutably.
797pub(crate) fn with_state_mut<R>(f: impl FnOnce(&mut EventState) -> R) -> R {
798    EVENT_STATE.with(|m| f(&mut m.borrow_mut()))
799}
800
801// Reset all counters (useful in tests).
802pub(super) fn reset() {
803    with_state_mut(|m| *m = EventState::default());
804}
805
806// Reset all event state: counters, perf, and serialize counters.
807pub(crate) fn reset_all() {
808    reset();
809}
810
811// Accumulate instruction counts and track a max.
812pub(super) fn add_instructions(total: &mut u128, max: &mut u64, delta_inst: u64) {
813    *total = total.saturating_add(u128::from(delta_inst));
814    if delta_inst > *max {
815        *max = delta_inst;
816    }
817}
818
819#[cfg_attr(doc, doc = "EventReport\n\nMetrics query payload.")]
820#[derive(CandidType, Clone, Debug, Default, Deserialize)]
821pub struct EventReport {
822    counters: Option<EventCounters>,
823    entity_counters: Vec<EntitySummary>,
824    window_filter_matched: bool,
825    requested_window_start_ms: Option<u64>,
826    active_window_start_ms: u64,
827}
828
829impl EventReport {
830    #[must_use]
831    pub(crate) const fn new(
832        counters: Option<EventCounters>,
833        entity_counters: Vec<EntitySummary>,
834        window_filter_matched: bool,
835        requested_window_start_ms: Option<u64>,
836        active_window_start_ms: u64,
837    ) -> Self {
838        Self {
839            counters,
840            entity_counters,
841            window_filter_matched,
842            requested_window_start_ms,
843            active_window_start_ms,
844        }
845    }
846
847    #[must_use]
848    pub const fn counters(&self) -> Option<&EventCounters> {
849        self.counters.as_ref()
850    }
851
852    #[must_use]
853    pub fn entity_counters(&self) -> &[EntitySummary] {
854        &self.entity_counters
855    }
856
857    #[must_use]
858    pub const fn window_filter_matched(&self) -> bool {
859        self.window_filter_matched
860    }
861
862    #[must_use]
863    pub const fn requested_window_start_ms(&self) -> Option<u64> {
864        self.requested_window_start_ms
865    }
866
867    #[must_use]
868    pub const fn active_window_start_ms(&self) -> u64 {
869        self.active_window_start_ms
870    }
871
872    #[must_use]
873    pub fn into_counters(self) -> Option<EventCounters> {
874        self.counters
875    }
876
877    #[must_use]
878    pub fn into_entity_counters(self) -> Vec<EntitySummary> {
879        self.entity_counters
880    }
881}
882
883//
884// EventCounters
885//
886// Top-level metrics counters returned by `icydb_metrics()`.
887// This keeps aggregate ops/perf totals while leaving per-entity detail to the
888// separate `entity_counters` payload.
889//
890
891#[derive(CandidType, Clone, Debug, Default, Deserialize)]
892pub struct EventCounters {
893    pub(crate) ops: EventOps,
894    pub(crate) perf: EventPerf,
895    pub(crate) window_start_ms: u64,
896    pub(crate) window_end_ms: u64,
897    pub(crate) window_duration_ms: u64,
898}
899
900impl EventCounters {
901    #[must_use]
902    pub(crate) const fn new(
903        ops: EventOps,
904        perf: EventPerf,
905        window_start_ms: u64,
906        window_end_ms: u64,
907    ) -> Self {
908        Self {
909            ops,
910            perf,
911            window_start_ms,
912            window_end_ms,
913            window_duration_ms: window_end_ms.saturating_sub(window_start_ms),
914        }
915    }
916
917    #[must_use]
918    pub const fn ops(&self) -> &EventOps {
919        &self.ops
920    }
921
922    #[must_use]
923    pub const fn perf(&self) -> &EventPerf {
924        &self.perf
925    }
926
927    #[must_use]
928    pub const fn window_start_ms(&self) -> u64 {
929        self.window_start_ms
930    }
931
932    #[must_use]
933    pub const fn window_end_ms(&self) -> u64 {
934        self.window_end_ms
935    }
936
937    #[must_use]
938    pub const fn window_duration_ms(&self) -> u64 {
939        self.window_duration_ms
940    }
941}
942
943#[cfg_attr(doc, doc = "EntitySummary\n\nPer-entity metrics summary.")]
944#[derive(CandidType, Clone, Debug, Default, Deserialize)]
945pub struct EntitySummary {
946    path: String,
947    load_calls: u64,
948    save_calls: u64,
949    delete_calls: u64,
950    save_insert_calls: u64,
951    save_update_calls: u64,
952    save_replace_calls: u64,
953    exec_success: u64,
954    exec_error_corruption: u64,
955    exec_error_incompatible_persisted_format: u64,
956    exec_error_not_found: u64,
957    exec_error_internal: u64,
958    exec_error_conflict: u64,
959    exec_error_unsupported: u64,
960    exec_error_invariant_violation: u64,
961    exec_aborted: u64,
962    cache_shared_query_plan_hits: u64,
963    cache_shared_query_plan_misses: u64,
964    cache_shared_query_plan_inserts: u64,
965    cache_sql_compiled_command_hits: u64,
966    cache_sql_compiled_command_misses: u64,
967    cache_sql_compiled_command_inserts: u64,
968    plan_index: u64,
969    plan_keys: u64,
970    plan_range: u64,
971    plan_full_scan: u64,
972    plan_by_key: u64,
973    plan_by_keys: u64,
974    plan_key_range: u64,
975    plan_index_prefix: u64,
976    plan_index_multi_lookup: u64,
977    plan_index_range: u64,
978    plan_explicit_full_scan: u64,
979    plan_union: u64,
980    plan_intersection: u64,
981    plan_grouped_hash_materialized: u64,
982    plan_grouped_ordered_materialized: u64,
983    rows_loaded: u64,
984    rows_saved: u64,
985    rows_inserted: u64,
986    rows_updated: u64,
987    rows_replaced: u64,
988    rows_scanned: u64,
989    rows_filtered: u64,
990    rows_aggregated: u64,
991    rows_emitted: u64,
992    load_candidate_rows_scanned: u64,
993    load_candidate_rows_filtered: u64,
994    load_result_rows_emitted: u64,
995    rows_deleted: u64,
996    sql_insert_calls: u64,
997    sql_insert_select_calls: u64,
998    sql_update_calls: u64,
999    sql_delete_calls: u64,
1000    sql_write_matched_rows: u64,
1001    sql_write_mutated_rows: u64,
1002    sql_write_returning_rows: u64,
1003    sql_write_error_insert: u64,
1004    sql_write_error_insert_select: u64,
1005    sql_write_error_update: u64,
1006    sql_write_error_delete: u64,
1007    sql_write_error_corruption: u64,
1008    sql_write_error_incompatible_persisted_format: u64,
1009    sql_write_error_not_found: u64,
1010    sql_write_error_internal: u64,
1011    sql_write_error_conflict: u64,
1012    sql_write_error_unsupported: u64,
1013    sql_write_error_invariant_violation: u64,
1014    index_inserts: u64,
1015    index_removes: u64,
1016    reverse_index_inserts: u64,
1017    reverse_index_removes: u64,
1018    relation_reverse_lookups: u64,
1019    relation_delete_blocks: u64,
1020    write_rows_touched: u64,
1021    write_index_entries_changed: u64,
1022    write_reverse_index_entries_changed: u64,
1023    write_relation_checks: u64,
1024    unique_violations: u64,
1025    non_atomic_partial_commits: u64,
1026    non_atomic_partial_rows_committed: u64,
1027}
1028
1029impl EntitySummary {
1030    #[must_use]
1031    pub const fn path(&self) -> &str {
1032        self.path.as_str()
1033    }
1034
1035    #[must_use]
1036    pub const fn load_calls(&self) -> u64 {
1037        self.load_calls
1038    }
1039
1040    #[must_use]
1041    pub const fn save_calls(&self) -> u64 {
1042        self.save_calls
1043    }
1044
1045    #[must_use]
1046    pub const fn delete_calls(&self) -> u64 {
1047        self.delete_calls
1048    }
1049
1050    #[must_use]
1051    pub const fn save_insert_calls(&self) -> u64 {
1052        self.save_insert_calls
1053    }
1054
1055    #[must_use]
1056    pub const fn save_update_calls(&self) -> u64 {
1057        self.save_update_calls
1058    }
1059
1060    #[must_use]
1061    pub const fn save_replace_calls(&self) -> u64 {
1062        self.save_replace_calls
1063    }
1064
1065    #[must_use]
1066    pub const fn exec_success(&self) -> u64 {
1067        self.exec_success
1068    }
1069
1070    #[must_use]
1071    pub const fn exec_error_corruption(&self) -> u64 {
1072        self.exec_error_corruption
1073    }
1074
1075    #[must_use]
1076    pub const fn exec_error_incompatible_persisted_format(&self) -> u64 {
1077        self.exec_error_incompatible_persisted_format
1078    }
1079
1080    #[must_use]
1081    pub const fn exec_error_not_found(&self) -> u64 {
1082        self.exec_error_not_found
1083    }
1084
1085    #[must_use]
1086    pub const fn exec_error_internal(&self) -> u64 {
1087        self.exec_error_internal
1088    }
1089
1090    #[must_use]
1091    pub const fn exec_error_conflict(&self) -> u64 {
1092        self.exec_error_conflict
1093    }
1094
1095    #[must_use]
1096    pub const fn exec_error_unsupported(&self) -> u64 {
1097        self.exec_error_unsupported
1098    }
1099
1100    #[must_use]
1101    pub const fn exec_error_invariant_violation(&self) -> u64 {
1102        self.exec_error_invariant_violation
1103    }
1104
1105    #[must_use]
1106    pub const fn exec_aborted(&self) -> u64 {
1107        self.exec_aborted
1108    }
1109
1110    #[must_use]
1111    pub const fn cache_shared_query_plan_hits(&self) -> u64 {
1112        self.cache_shared_query_plan_hits
1113    }
1114
1115    #[must_use]
1116    pub const fn cache_shared_query_plan_misses(&self) -> u64 {
1117        self.cache_shared_query_plan_misses
1118    }
1119
1120    #[must_use]
1121    pub const fn cache_shared_query_plan_inserts(&self) -> u64 {
1122        self.cache_shared_query_plan_inserts
1123    }
1124
1125    #[must_use]
1126    pub const fn cache_sql_compiled_command_hits(&self) -> u64 {
1127        self.cache_sql_compiled_command_hits
1128    }
1129
1130    #[must_use]
1131    pub const fn cache_sql_compiled_command_misses(&self) -> u64 {
1132        self.cache_sql_compiled_command_misses
1133    }
1134
1135    #[must_use]
1136    pub const fn cache_sql_compiled_command_inserts(&self) -> u64 {
1137        self.cache_sql_compiled_command_inserts
1138    }
1139
1140    #[must_use]
1141    pub const fn plan_index(&self) -> u64 {
1142        self.plan_index
1143    }
1144
1145    #[must_use]
1146    pub const fn plan_keys(&self) -> u64 {
1147        self.plan_keys
1148    }
1149
1150    #[must_use]
1151    pub const fn plan_range(&self) -> u64 {
1152        self.plan_range
1153    }
1154
1155    #[must_use]
1156    pub const fn plan_full_scan(&self) -> u64 {
1157        self.plan_full_scan
1158    }
1159
1160    #[must_use]
1161    pub const fn plan_by_key(&self) -> u64 {
1162        self.plan_by_key
1163    }
1164
1165    #[must_use]
1166    pub const fn plan_by_keys(&self) -> u64 {
1167        self.plan_by_keys
1168    }
1169
1170    #[must_use]
1171    pub const fn plan_key_range(&self) -> u64 {
1172        self.plan_key_range
1173    }
1174
1175    #[must_use]
1176    pub const fn plan_index_prefix(&self) -> u64 {
1177        self.plan_index_prefix
1178    }
1179
1180    #[must_use]
1181    pub const fn plan_index_multi_lookup(&self) -> u64 {
1182        self.plan_index_multi_lookup
1183    }
1184
1185    #[must_use]
1186    pub const fn plan_index_range(&self) -> u64 {
1187        self.plan_index_range
1188    }
1189
1190    #[must_use]
1191    pub const fn plan_explicit_full_scan(&self) -> u64 {
1192        self.plan_explicit_full_scan
1193    }
1194
1195    #[must_use]
1196    pub const fn plan_union(&self) -> u64 {
1197        self.plan_union
1198    }
1199
1200    #[must_use]
1201    pub const fn plan_intersection(&self) -> u64 {
1202        self.plan_intersection
1203    }
1204
1205    #[must_use]
1206    pub const fn plan_grouped_hash_materialized(&self) -> u64 {
1207        self.plan_grouped_hash_materialized
1208    }
1209
1210    #[must_use]
1211    pub const fn plan_grouped_ordered_materialized(&self) -> u64 {
1212        self.plan_grouped_ordered_materialized
1213    }
1214
1215    #[must_use]
1216    pub const fn rows_loaded(&self) -> u64 {
1217        self.rows_loaded
1218    }
1219
1220    #[must_use]
1221    pub const fn rows_saved(&self) -> u64 {
1222        self.rows_saved
1223    }
1224
1225    #[must_use]
1226    pub const fn rows_inserted(&self) -> u64 {
1227        self.rows_inserted
1228    }
1229
1230    #[must_use]
1231    pub const fn rows_updated(&self) -> u64 {
1232        self.rows_updated
1233    }
1234
1235    #[must_use]
1236    pub const fn rows_replaced(&self) -> u64 {
1237        self.rows_replaced
1238    }
1239
1240    #[must_use]
1241    pub const fn rows_scanned(&self) -> u64 {
1242        self.rows_scanned
1243    }
1244
1245    #[must_use]
1246    pub const fn rows_filtered(&self) -> u64 {
1247        self.rows_filtered
1248    }
1249
1250    #[must_use]
1251    pub const fn rows_aggregated(&self) -> u64 {
1252        self.rows_aggregated
1253    }
1254
1255    #[must_use]
1256    pub const fn rows_emitted(&self) -> u64 {
1257        self.rows_emitted
1258    }
1259
1260    #[must_use]
1261    pub const fn load_candidate_rows_scanned(&self) -> u64 {
1262        self.load_candidate_rows_scanned
1263    }
1264
1265    #[must_use]
1266    pub const fn load_candidate_rows_filtered(&self) -> u64 {
1267        self.load_candidate_rows_filtered
1268    }
1269
1270    #[must_use]
1271    pub const fn load_result_rows_emitted(&self) -> u64 {
1272        self.load_result_rows_emitted
1273    }
1274
1275    #[must_use]
1276    pub const fn rows_deleted(&self) -> u64 {
1277        self.rows_deleted
1278    }
1279
1280    #[must_use]
1281    pub const fn sql_insert_calls(&self) -> u64 {
1282        self.sql_insert_calls
1283    }
1284
1285    #[must_use]
1286    pub const fn sql_insert_select_calls(&self) -> u64 {
1287        self.sql_insert_select_calls
1288    }
1289
1290    #[must_use]
1291    pub const fn sql_update_calls(&self) -> u64 {
1292        self.sql_update_calls
1293    }
1294
1295    #[must_use]
1296    pub const fn sql_delete_calls(&self) -> u64 {
1297        self.sql_delete_calls
1298    }
1299
1300    #[must_use]
1301    pub const fn sql_write_matched_rows(&self) -> u64 {
1302        self.sql_write_matched_rows
1303    }
1304
1305    #[must_use]
1306    pub const fn sql_write_mutated_rows(&self) -> u64 {
1307        self.sql_write_mutated_rows
1308    }
1309
1310    #[must_use]
1311    pub const fn sql_write_returning_rows(&self) -> u64 {
1312        self.sql_write_returning_rows
1313    }
1314
1315    #[must_use]
1316    pub const fn sql_write_error_insert(&self) -> u64 {
1317        self.sql_write_error_insert
1318    }
1319
1320    #[must_use]
1321    pub const fn sql_write_error_insert_select(&self) -> u64 {
1322        self.sql_write_error_insert_select
1323    }
1324
1325    #[must_use]
1326    pub const fn sql_write_error_update(&self) -> u64 {
1327        self.sql_write_error_update
1328    }
1329
1330    #[must_use]
1331    pub const fn sql_write_error_delete(&self) -> u64 {
1332        self.sql_write_error_delete
1333    }
1334
1335    #[must_use]
1336    pub const fn sql_write_error_corruption(&self) -> u64 {
1337        self.sql_write_error_corruption
1338    }
1339
1340    #[must_use]
1341    pub const fn sql_write_error_incompatible_persisted_format(&self) -> u64 {
1342        self.sql_write_error_incompatible_persisted_format
1343    }
1344
1345    #[must_use]
1346    pub const fn sql_write_error_not_found(&self) -> u64 {
1347        self.sql_write_error_not_found
1348    }
1349
1350    #[must_use]
1351    pub const fn sql_write_error_internal(&self) -> u64 {
1352        self.sql_write_error_internal
1353    }
1354
1355    #[must_use]
1356    pub const fn sql_write_error_conflict(&self) -> u64 {
1357        self.sql_write_error_conflict
1358    }
1359
1360    #[must_use]
1361    pub const fn sql_write_error_unsupported(&self) -> u64 {
1362        self.sql_write_error_unsupported
1363    }
1364
1365    #[must_use]
1366    pub const fn sql_write_error_invariant_violation(&self) -> u64 {
1367        self.sql_write_error_invariant_violation
1368    }
1369
1370    #[must_use]
1371    pub const fn index_inserts(&self) -> u64 {
1372        self.index_inserts
1373    }
1374
1375    #[must_use]
1376    pub const fn index_removes(&self) -> u64 {
1377        self.index_removes
1378    }
1379
1380    #[must_use]
1381    pub const fn reverse_index_inserts(&self) -> u64 {
1382        self.reverse_index_inserts
1383    }
1384
1385    #[must_use]
1386    pub const fn reverse_index_removes(&self) -> u64 {
1387        self.reverse_index_removes
1388    }
1389
1390    #[must_use]
1391    pub const fn relation_reverse_lookups(&self) -> u64 {
1392        self.relation_reverse_lookups
1393    }
1394
1395    #[must_use]
1396    pub const fn relation_delete_blocks(&self) -> u64 {
1397        self.relation_delete_blocks
1398    }
1399
1400    #[must_use]
1401    pub const fn write_rows_touched(&self) -> u64 {
1402        self.write_rows_touched
1403    }
1404
1405    #[must_use]
1406    pub const fn write_index_entries_changed(&self) -> u64 {
1407        self.write_index_entries_changed
1408    }
1409
1410    #[must_use]
1411    pub const fn write_reverse_index_entries_changed(&self) -> u64 {
1412        self.write_reverse_index_entries_changed
1413    }
1414
1415    #[must_use]
1416    pub const fn write_relation_checks(&self) -> u64 {
1417        self.write_relation_checks
1418    }
1419
1420    #[must_use]
1421    pub const fn unique_violations(&self) -> u64 {
1422        self.unique_violations
1423    }
1424
1425    #[must_use]
1426    pub const fn non_atomic_partial_commits(&self) -> u64 {
1427        self.non_atomic_partial_commits
1428    }
1429
1430    #[must_use]
1431    pub const fn non_atomic_partial_rows_committed(&self) -> u64 {
1432        self.non_atomic_partial_rows_committed
1433    }
1434
1435    /// Returns result rows emitted per load candidate row scanned.
1436    #[must_use]
1437    pub const fn load_selectivity_ratio(&self) -> Option<MetricRatio> {
1438        ratio(
1439            self.load_result_rows_emitted,
1440            self.load_candidate_rows_scanned,
1441        )
1442    }
1443
1444    /// Returns candidate rows filtered per load candidate row scanned.
1445    #[must_use]
1446    pub const fn load_filter_ratio(&self) -> Option<MetricRatio> {
1447        ratio(
1448            self.load_candidate_rows_filtered,
1449            self.load_candidate_rows_scanned,
1450        )
1451    }
1452
1453    /// Returns SQL-mutated rows per SQL-matched row.
1454    #[must_use]
1455    pub const fn sql_write_mutation_ratio(&self) -> Option<MetricRatio> {
1456        ratio(self.sql_write_mutated_rows, self.sql_write_matched_rows)
1457    }
1458
1459    /// Returns SQL `RETURNING` rows per SQL-mutated row.
1460    #[must_use]
1461    pub const fn sql_write_returning_ratio(&self) -> Option<MetricRatio> {
1462        ratio(self.sql_write_returning_rows, self.sql_write_mutated_rows)
1463    }
1464
1465    /// Returns primary index entries changed per write row touched.
1466    #[must_use]
1467    pub const fn write_index_entries_per_row(&self) -> Option<MetricRatio> {
1468        ratio(self.write_index_entries_changed, self.write_rows_touched)
1469    }
1470
1471    /// Returns reverse-index entries changed per write row touched.
1472    #[must_use]
1473    pub const fn write_reverse_index_entries_per_row(&self) -> Option<MetricRatio> {
1474        ratio(
1475            self.write_reverse_index_entries_changed,
1476            self.write_rows_touched,
1477        )
1478    }
1479
1480    /// Returns relation checks performed per write row touched.
1481    #[must_use]
1482    pub const fn write_relation_checks_per_row(&self) -> Option<MetricRatio> {
1483        ratio(self.write_relation_checks, self.write_rows_touched)
1484    }
1485
1486    // Rank entity summaries by all visible activity so write-heavy or
1487    // maintenance-heavy entities are not hidden below read-heavy entities.
1488    const fn activity_score(&self) -> u64 {
1489        self.load_calls
1490            .saturating_add(self.save_calls)
1491            .saturating_add(self.delete_calls)
1492            .saturating_add(self.save_insert_calls)
1493            .saturating_add(self.save_update_calls)
1494            .saturating_add(self.save_replace_calls)
1495            .saturating_add(self.exec_success)
1496            .saturating_add(self.exec_error_corruption)
1497            .saturating_add(self.exec_error_incompatible_persisted_format)
1498            .saturating_add(self.exec_error_not_found)
1499            .saturating_add(self.exec_error_internal)
1500            .saturating_add(self.exec_error_conflict)
1501            .saturating_add(self.exec_error_unsupported)
1502            .saturating_add(self.exec_error_invariant_violation)
1503            .saturating_add(self.exec_aborted)
1504            .saturating_add(self.cache_shared_query_plan_hits)
1505            .saturating_add(self.cache_shared_query_plan_misses)
1506            .saturating_add(self.cache_shared_query_plan_inserts)
1507            .saturating_add(self.cache_sql_compiled_command_hits)
1508            .saturating_add(self.cache_sql_compiled_command_misses)
1509            .saturating_add(self.cache_sql_compiled_command_inserts)
1510            .saturating_add(self.plan_index)
1511            .saturating_add(self.plan_keys)
1512            .saturating_add(self.plan_range)
1513            .saturating_add(self.plan_full_scan)
1514            .saturating_add(self.plan_by_key)
1515            .saturating_add(self.plan_by_keys)
1516            .saturating_add(self.plan_key_range)
1517            .saturating_add(self.plan_index_prefix)
1518            .saturating_add(self.plan_index_multi_lookup)
1519            .saturating_add(self.plan_index_range)
1520            .saturating_add(self.plan_explicit_full_scan)
1521            .saturating_add(self.plan_union)
1522            .saturating_add(self.plan_intersection)
1523            .saturating_add(self.plan_grouped_hash_materialized)
1524            .saturating_add(self.plan_grouped_ordered_materialized)
1525            .saturating_add(self.rows_loaded)
1526            .saturating_add(self.rows_saved)
1527            .saturating_add(self.rows_inserted)
1528            .saturating_add(self.rows_updated)
1529            .saturating_add(self.rows_replaced)
1530            .saturating_add(self.rows_scanned)
1531            .saturating_add(self.rows_filtered)
1532            .saturating_add(self.rows_aggregated)
1533            .saturating_add(self.rows_emitted)
1534            .saturating_add(self.load_candidate_rows_scanned)
1535            .saturating_add(self.load_candidate_rows_filtered)
1536            .saturating_add(self.load_result_rows_emitted)
1537            .saturating_add(self.rows_deleted)
1538            .saturating_add(self.sql_insert_calls)
1539            .saturating_add(self.sql_insert_select_calls)
1540            .saturating_add(self.sql_update_calls)
1541            .saturating_add(self.sql_delete_calls)
1542            .saturating_add(self.sql_write_matched_rows)
1543            .saturating_add(self.sql_write_mutated_rows)
1544            .saturating_add(self.sql_write_returning_rows)
1545            .saturating_add(self.sql_write_error_insert)
1546            .saturating_add(self.sql_write_error_insert_select)
1547            .saturating_add(self.sql_write_error_update)
1548            .saturating_add(self.sql_write_error_delete)
1549            .saturating_add(self.sql_write_error_corruption)
1550            .saturating_add(self.sql_write_error_incompatible_persisted_format)
1551            .saturating_add(self.sql_write_error_not_found)
1552            .saturating_add(self.sql_write_error_internal)
1553            .saturating_add(self.sql_write_error_conflict)
1554            .saturating_add(self.sql_write_error_unsupported)
1555            .saturating_add(self.sql_write_error_invariant_violation)
1556            .saturating_add(self.index_inserts)
1557            .saturating_add(self.index_removes)
1558            .saturating_add(self.reverse_index_inserts)
1559            .saturating_add(self.reverse_index_removes)
1560            .saturating_add(self.relation_reverse_lookups)
1561            .saturating_add(self.relation_delete_blocks)
1562            .saturating_add(self.write_rows_touched)
1563            .saturating_add(self.write_index_entries_changed)
1564            .saturating_add(self.write_reverse_index_entries_changed)
1565            .saturating_add(self.write_relation_checks)
1566            .saturating_add(self.unique_violations)
1567            .saturating_add(self.non_atomic_partial_commits)
1568            .saturating_add(self.non_atomic_partial_rows_committed)
1569    }
1570}
1571
1572// Project mutable per-entity counters into the stable report DTO.
1573//
1574// Keeping this projection out of `report_window_start` leaves the window
1575// filtering logic readable while still making every report field explicit.
1576fn entity_summary_from_counters(path: &str, ops: &EntityCounters) -> EntitySummary {
1577    EntitySummary {
1578        path: path.to_string(),
1579        load_calls: ops.load_calls,
1580        save_calls: ops.save_calls,
1581        delete_calls: ops.delete_calls,
1582        save_insert_calls: ops.save_insert_calls,
1583        save_update_calls: ops.save_update_calls,
1584        save_replace_calls: ops.save_replace_calls,
1585        exec_success: ops.exec_success,
1586        exec_error_corruption: ops.exec_error_corruption,
1587        exec_error_incompatible_persisted_format: ops.exec_error_incompatible_persisted_format,
1588        exec_error_not_found: ops.exec_error_not_found,
1589        exec_error_internal: ops.exec_error_internal,
1590        exec_error_conflict: ops.exec_error_conflict,
1591        exec_error_unsupported: ops.exec_error_unsupported,
1592        exec_error_invariant_violation: ops.exec_error_invariant_violation,
1593        exec_aborted: ops.exec_aborted,
1594        cache_shared_query_plan_hits: ops.cache_shared_query_plan_hits,
1595        cache_shared_query_plan_misses: ops.cache_shared_query_plan_misses,
1596        cache_shared_query_plan_inserts: ops.cache_shared_query_plan_inserts,
1597        cache_sql_compiled_command_hits: ops.cache_sql_compiled_command_hits,
1598        cache_sql_compiled_command_misses: ops.cache_sql_compiled_command_misses,
1599        cache_sql_compiled_command_inserts: ops.cache_sql_compiled_command_inserts,
1600        plan_index: ops.plan_index,
1601        plan_keys: ops.plan_keys,
1602        plan_range: ops.plan_range,
1603        plan_full_scan: ops.plan_full_scan,
1604        plan_by_key: ops.plan_by_key,
1605        plan_by_keys: ops.plan_by_keys,
1606        plan_key_range: ops.plan_key_range,
1607        plan_index_prefix: ops.plan_index_prefix,
1608        plan_index_multi_lookup: ops.plan_index_multi_lookup,
1609        plan_index_range: ops.plan_index_range,
1610        plan_explicit_full_scan: ops.plan_explicit_full_scan,
1611        plan_union: ops.plan_union,
1612        plan_intersection: ops.plan_intersection,
1613        plan_grouped_hash_materialized: ops.plan_grouped_hash_materialized,
1614        plan_grouped_ordered_materialized: ops.plan_grouped_ordered_materialized,
1615        rows_loaded: ops.rows_loaded,
1616        rows_saved: ops.rows_saved,
1617        rows_inserted: ops.rows_inserted,
1618        rows_updated: ops.rows_updated,
1619        rows_replaced: ops.rows_replaced,
1620        rows_scanned: ops.rows_scanned,
1621        rows_filtered: ops.rows_filtered,
1622        rows_aggregated: ops.rows_aggregated,
1623        rows_emitted: ops.rows_emitted,
1624        load_candidate_rows_scanned: ops.load_candidate_rows_scanned,
1625        load_candidate_rows_filtered: ops.load_candidate_rows_filtered,
1626        load_result_rows_emitted: ops.load_result_rows_emitted,
1627        rows_deleted: ops.rows_deleted,
1628        sql_insert_calls: ops.sql_insert_calls,
1629        sql_insert_select_calls: ops.sql_insert_select_calls,
1630        sql_update_calls: ops.sql_update_calls,
1631        sql_delete_calls: ops.sql_delete_calls,
1632        sql_write_matched_rows: ops.sql_write_matched_rows,
1633        sql_write_mutated_rows: ops.sql_write_mutated_rows,
1634        sql_write_returning_rows: ops.sql_write_returning_rows,
1635        sql_write_error_insert: ops.sql_write_error_insert,
1636        sql_write_error_insert_select: ops.sql_write_error_insert_select,
1637        sql_write_error_update: ops.sql_write_error_update,
1638        sql_write_error_delete: ops.sql_write_error_delete,
1639        sql_write_error_corruption: ops.sql_write_error_corruption,
1640        sql_write_error_incompatible_persisted_format: ops
1641            .sql_write_error_incompatible_persisted_format,
1642        sql_write_error_not_found: ops.sql_write_error_not_found,
1643        sql_write_error_internal: ops.sql_write_error_internal,
1644        sql_write_error_conflict: ops.sql_write_error_conflict,
1645        sql_write_error_unsupported: ops.sql_write_error_unsupported,
1646        sql_write_error_invariant_violation: ops.sql_write_error_invariant_violation,
1647        index_inserts: ops.index_inserts,
1648        index_removes: ops.index_removes,
1649        reverse_index_inserts: ops.reverse_index_inserts,
1650        reverse_index_removes: ops.reverse_index_removes,
1651        relation_reverse_lookups: ops.relation_reverse_lookups,
1652        relation_delete_blocks: ops.relation_delete_blocks,
1653        write_rows_touched: ops.write_rows_touched,
1654        write_index_entries_changed: ops.write_index_entries_changed,
1655        write_reverse_index_entries_changed: ops.write_reverse_index_entries_changed,
1656        write_relation_checks: ops.write_relation_checks,
1657        unique_violations: ops.unique_violations,
1658        non_atomic_partial_commits: ops.non_atomic_partial_commits,
1659        non_atomic_partial_rows_committed: ops.non_atomic_partial_rows_committed,
1660    }
1661}
1662
1663// Build a metrics report gated by `window_start_ms`.
1664//
1665// This is a window-start filter:
1666// - If `window_start_ms` is `None`, return the current window.
1667// - If `window_start_ms <= state.window_start_ms`, return the current window.
1668// - If `window_start_ms > state.window_start_ms`, return an empty report.
1669//
1670// IcyDB stores aggregate counters only, so it cannot produce a precise
1671// sub-window report after `state.window_start_ms`.
1672#[must_use]
1673pub(super) fn report_window_start(window_start_ms: Option<u64>) -> EventReport {
1674    let snap = with_state(Clone::clone);
1675    if let Some(requested_window_start_ms) = window_start_ms
1676        && requested_window_start_ms > snap.window_start_ms
1677    {
1678        return EventReport::new(
1679            None,
1680            Vec::new(),
1681            false,
1682            window_start_ms,
1683            snap.window_start_ms,
1684        );
1685    }
1686
1687    let mut entity_counters: Vec<EntitySummary> = Vec::new();
1688    for (path, ops) in &snap.entities {
1689        entity_counters.push(entity_summary_from_counters(path, ops));
1690    }
1691
1692    entity_counters.sort_by(|a, b| {
1693        b.activity_score()
1694            .cmp(&a.activity_score())
1695            .then_with(|| b.rows_loaded.cmp(&a.rows_loaded))
1696            .then_with(|| b.rows_saved.cmp(&a.rows_saved))
1697            .then_with(|| b.rows_scanned.cmp(&a.rows_scanned))
1698            .then_with(|| b.rows_deleted.cmp(&a.rows_deleted))
1699            .then_with(|| a.path.cmp(&b.path))
1700    });
1701
1702    EventReport::new(
1703        Some(EventCounters::new(
1704            snap.ops.clone(),
1705            snap.perf.clone(),
1706            snap.window_start_ms,
1707            now_millis(),
1708        )),
1709        entity_counters,
1710        true,
1711        window_start_ms,
1712        snap.window_start_ms,
1713    )
1714}