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