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