1use candid::CandidType;
11use canic_cdk::utils::time::now_millis;
12use serde::Deserialize;
13use std::{cell::RefCell, collections::BTreeMap};
14
15#[derive(Clone, Debug)]
16pub(crate) struct EventState {
17 pub(crate) ops: EventOps,
18 pub(crate) perf: EventPerf,
19 pub(crate) entities: BTreeMap<String, EntityCounters>,
20 pub(crate) window_start_ms: u64,
21}
22
23impl Default for EventState {
24 fn default() -> Self {
25 Self {
26 ops: EventOps::default(),
27 perf: EventPerf::default(),
28 entities: BTreeMap::new(),
29 window_start_ms: now_millis(),
30 }
31 }
32}
33
34#[cfg_attr(doc, doc = "EventOps\n\nOperation counters.")]
35#[derive(CandidType, Clone, Debug, Default, Deserialize)]
36pub struct EventOps {
37 pub(crate) load_calls: u64,
39 pub(crate) save_calls: u64,
40 pub(crate) delete_calls: u64,
41
42 pub(crate) plan_index: u64,
44 pub(crate) plan_keys: u64,
45 pub(crate) plan_range: u64,
46 pub(crate) plan_full_scan: u64,
47 pub(crate) plan_grouped_hash_materialized: u64,
48 pub(crate) plan_grouped_ordered_materialized: u64,
49
50 pub(crate) rows_loaded: u64,
52 pub(crate) rows_scanned: u64,
53 pub(crate) rows_filtered: u64,
54 pub(crate) rows_aggregated: u64,
55 pub(crate) rows_emitted: u64,
56 pub(crate) rows_deleted: u64,
57
58 pub(crate) index_inserts: u64,
60 pub(crate) index_removes: u64,
61 pub(crate) reverse_index_inserts: u64,
62 pub(crate) reverse_index_removes: u64,
63 pub(crate) relation_reverse_lookups: u64,
64 pub(crate) relation_delete_blocks: u64,
65 pub(crate) unique_violations: u64,
66 pub(crate) non_atomic_partial_commits: u64,
67 pub(crate) non_atomic_partial_rows_committed: u64,
68}
69
70impl EventOps {
71 #[must_use]
72 pub const fn load_calls(&self) -> u64 {
73 self.load_calls
74 }
75
76 #[must_use]
77 pub const fn save_calls(&self) -> u64 {
78 self.save_calls
79 }
80
81 #[must_use]
82 pub const fn delete_calls(&self) -> u64 {
83 self.delete_calls
84 }
85
86 #[must_use]
87 pub const fn plan_index(&self) -> u64 {
88 self.plan_index
89 }
90
91 #[must_use]
92 pub const fn plan_keys(&self) -> u64 {
93 self.plan_keys
94 }
95
96 #[must_use]
97 pub const fn plan_range(&self) -> u64 {
98 self.plan_range
99 }
100
101 #[must_use]
102 pub const fn plan_full_scan(&self) -> u64 {
103 self.plan_full_scan
104 }
105
106 #[must_use]
107 pub const fn plan_grouped_hash_materialized(&self) -> u64 {
108 self.plan_grouped_hash_materialized
109 }
110
111 #[must_use]
112 pub const fn plan_grouped_ordered_materialized(&self) -> u64 {
113 self.plan_grouped_ordered_materialized
114 }
115
116 #[must_use]
117 pub const fn rows_loaded(&self) -> u64 {
118 self.rows_loaded
119 }
120
121 #[must_use]
122 pub const fn rows_scanned(&self) -> u64 {
123 self.rows_scanned
124 }
125
126 #[must_use]
127 pub const fn rows_filtered(&self) -> u64 {
128 self.rows_filtered
129 }
130
131 #[must_use]
132 pub const fn rows_aggregated(&self) -> u64 {
133 self.rows_aggregated
134 }
135
136 #[must_use]
137 pub const fn rows_emitted(&self) -> u64 {
138 self.rows_emitted
139 }
140
141 #[must_use]
142 pub const fn rows_deleted(&self) -> u64 {
143 self.rows_deleted
144 }
145
146 #[must_use]
147 pub const fn index_inserts(&self) -> u64 {
148 self.index_inserts
149 }
150
151 #[must_use]
152 pub const fn index_removes(&self) -> u64 {
153 self.index_removes
154 }
155
156 #[must_use]
157 pub const fn reverse_index_inserts(&self) -> u64 {
158 self.reverse_index_inserts
159 }
160
161 #[must_use]
162 pub const fn reverse_index_removes(&self) -> u64 {
163 self.reverse_index_removes
164 }
165
166 #[must_use]
167 pub const fn relation_reverse_lookups(&self) -> u64 {
168 self.relation_reverse_lookups
169 }
170
171 #[must_use]
172 pub const fn relation_delete_blocks(&self) -> u64 {
173 self.relation_delete_blocks
174 }
175
176 #[must_use]
177 pub const fn unique_violations(&self) -> u64 {
178 self.unique_violations
179 }
180
181 #[must_use]
182 pub const fn non_atomic_partial_commits(&self) -> u64 {
183 self.non_atomic_partial_commits
184 }
185
186 #[must_use]
187 pub const fn non_atomic_partial_rows_committed(&self) -> u64 {
188 self.non_atomic_partial_rows_committed
189 }
190}
191
192#[derive(Clone, Debug, Default)]
193pub(crate) struct EntityCounters {
194 pub(crate) load_calls: u64,
195 pub(crate) save_calls: u64,
196 pub(crate) delete_calls: u64,
197 pub(crate) rows_loaded: u64,
198 pub(crate) rows_scanned: u64,
199 pub(crate) rows_filtered: u64,
200 pub(crate) rows_aggregated: u64,
201 pub(crate) rows_emitted: u64,
202 pub(crate) rows_deleted: u64,
203 pub(crate) index_inserts: u64,
204 pub(crate) index_removes: u64,
205 pub(crate) reverse_index_inserts: u64,
206 pub(crate) reverse_index_removes: u64,
207 pub(crate) relation_reverse_lookups: u64,
208 pub(crate) relation_delete_blocks: u64,
209 pub(crate) unique_violations: u64,
210 pub(crate) non_atomic_partial_commits: u64,
211 pub(crate) non_atomic_partial_rows_committed: u64,
212}
213
214#[cfg_attr(doc, doc = "EventPerf\n\nInstruction totals and maxima.")]
215#[derive(CandidType, Clone, Debug, Default, Deserialize)]
216pub struct EventPerf {
217 pub(crate) load_inst_total: u128,
219 pub(crate) save_inst_total: u128,
220 pub(crate) delete_inst_total: u128,
221
222 pub(crate) load_inst_max: u64,
224 pub(crate) save_inst_max: u64,
225 pub(crate) delete_inst_max: u64,
226}
227
228impl EventPerf {
229 #[must_use]
230 pub const fn new(
231 load_inst_total: u128,
232 save_inst_total: u128,
233 delete_inst_total: u128,
234 load_inst_max: u64,
235 save_inst_max: u64,
236 delete_inst_max: u64,
237 ) -> Self {
238 Self {
239 load_inst_total,
240 save_inst_total,
241 delete_inst_total,
242 load_inst_max,
243 save_inst_max,
244 delete_inst_max,
245 }
246 }
247
248 #[must_use]
249 pub const fn load_inst_total(&self) -> u128 {
250 self.load_inst_total
251 }
252
253 #[must_use]
254 pub const fn save_inst_total(&self) -> u128 {
255 self.save_inst_total
256 }
257
258 #[must_use]
259 pub const fn delete_inst_total(&self) -> u128 {
260 self.delete_inst_total
261 }
262
263 #[must_use]
264 pub const fn load_inst_max(&self) -> u64 {
265 self.load_inst_max
266 }
267
268 #[must_use]
269 pub const fn save_inst_max(&self) -> u64 {
270 self.save_inst_max
271 }
272
273 #[must_use]
274 pub const fn delete_inst_max(&self) -> u64 {
275 self.delete_inst_max
276 }
277}
278
279thread_local! {
280 static EVENT_STATE: RefCell<EventState> = RefCell::new(EventState::default());
281}
282
283pub(crate) fn with_state<R>(f: impl FnOnce(&EventState) -> R) -> R {
285 EVENT_STATE.with(|m| f(&m.borrow()))
286}
287
288pub(crate) fn with_state_mut<R>(f: impl FnOnce(&mut EventState) -> R) -> R {
290 EVENT_STATE.with(|m| f(&mut m.borrow_mut()))
291}
292
293pub(super) fn reset() {
295 with_state_mut(|m| *m = EventState::default());
296}
297
298pub(crate) fn reset_all() {
300 reset();
301}
302
303pub(super) fn add_instructions(total: &mut u128, max: &mut u64, delta_inst: u64) {
305 *total = total.saturating_add(u128::from(delta_inst));
306 if delta_inst > *max {
307 *max = delta_inst;
308 }
309}
310
311#[cfg_attr(doc, doc = "EventReport\n\nMetrics query payload.")]
312#[derive(CandidType, Clone, Debug, Default, Deserialize)]
313pub struct EventReport {
314 counters: Option<EventCounters>,
315 entity_counters: Vec<EntitySummary>,
316}
317
318impl EventReport {
319 #[must_use]
320 pub(crate) const fn new(
321 counters: Option<EventCounters>,
322 entity_counters: Vec<EntitySummary>,
323 ) -> Self {
324 Self {
325 counters,
326 entity_counters,
327 }
328 }
329
330 #[must_use]
331 pub const fn counters(&self) -> Option<&EventCounters> {
332 self.counters.as_ref()
333 }
334
335 #[must_use]
336 pub fn entity_counters(&self) -> &[EntitySummary] {
337 &self.entity_counters
338 }
339
340 #[must_use]
341 pub fn into_counters(self) -> Option<EventCounters> {
342 self.counters
343 }
344
345 #[must_use]
346 pub fn into_entity_counters(self) -> Vec<EntitySummary> {
347 self.entity_counters
348 }
349}
350
351#[derive(CandidType, Clone, Debug, Default, Deserialize)]
360pub struct EventCounters {
361 pub(crate) ops: EventOps,
362 pub(crate) perf: EventPerf,
363 pub(crate) window_start_ms: u64,
364}
365
366impl EventCounters {
367 #[must_use]
368 pub(crate) const fn new(ops: EventOps, perf: EventPerf, window_start_ms: u64) -> Self {
369 Self {
370 ops,
371 perf,
372 window_start_ms,
373 }
374 }
375
376 #[must_use]
377 pub const fn ops(&self) -> &EventOps {
378 &self.ops
379 }
380
381 #[must_use]
382 pub const fn perf(&self) -> &EventPerf {
383 &self.perf
384 }
385
386 #[must_use]
387 pub const fn window_start_ms(&self) -> u64 {
388 self.window_start_ms
389 }
390}
391
392#[cfg_attr(doc, doc = "EntitySummary\n\nPer-entity metrics summary.")]
393#[derive(CandidType, Clone, Debug, Default, Deserialize)]
394pub struct EntitySummary {
395 path: String,
396 load_calls: u64,
397 delete_calls: u64,
398 rows_loaded: u64,
399 rows_scanned: u64,
400 rows_deleted: u64,
401}
402
403impl EntitySummary {
404 #[must_use]
405 pub const fn path(&self) -> &str {
406 self.path.as_str()
407 }
408
409 #[must_use]
410 pub const fn load_calls(&self) -> u64 {
411 self.load_calls
412 }
413
414 #[must_use]
415 pub const fn delete_calls(&self) -> u64 {
416 self.delete_calls
417 }
418
419 #[must_use]
420 pub const fn rows_loaded(&self) -> u64 {
421 self.rows_loaded
422 }
423
424 #[must_use]
425 pub const fn rows_scanned(&self) -> u64 {
426 self.rows_scanned
427 }
428
429 #[must_use]
430 pub const fn rows_deleted(&self) -> u64 {
431 self.rows_deleted
432 }
433}
434
435#[must_use]
445pub(super) fn report_window_start(window_start_ms: Option<u64>) -> EventReport {
446 let snap = with_state(Clone::clone);
447 if let Some(requested_window_start_ms) = window_start_ms
448 && requested_window_start_ms > snap.window_start_ms
449 {
450 return EventReport::default();
451 }
452
453 let mut entity_counters: Vec<EntitySummary> = Vec::new();
454 for (path, ops) in &snap.entities {
455 entity_counters.push(EntitySummary {
456 path: path.clone(),
457 load_calls: ops.load_calls,
458 delete_calls: ops.delete_calls,
459 rows_loaded: ops.rows_loaded,
460 rows_scanned: ops.rows_scanned,
461 rows_deleted: ops.rows_deleted,
462 });
463 }
464
465 entity_counters.sort_by(|a, b| {
466 b.rows_loaded
467 .cmp(&a.rows_loaded)
468 .then_with(|| b.rows_scanned.cmp(&a.rows_scanned))
469 .then_with(|| b.rows_deleted.cmp(&a.rows_deleted))
470 .then_with(|| a.path.cmp(&b.path))
471 });
472
473 EventReport::new(
474 Some(EventCounters::new(
475 snap.ops.clone(),
476 snap.perf.clone(),
477 snap.window_start_ms,
478 )),
479 entity_counters,
480 )
481}