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