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