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