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