1use crate::traits::EntityKind;
2use candid::CandidType;
3use canic::{cdk::api::performance_counter, utils::time};
4use serde::{Deserialize, Serialize};
5use std::cmp::Ordering;
6use std::{cell::RefCell, collections::BTreeMap, marker::PhantomData};
7
8#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
14pub struct EventState {
15 pub ops: EventOps,
16 pub perf: EventPerf,
17 pub entities: BTreeMap<String, EntityCounters>,
18 pub since_ms: u64,
19}
20
21impl Default for EventState {
22 fn default() -> Self {
23 Self {
24 ops: EventOps::default(),
25 perf: EventPerf::default(),
26 entities: BTreeMap::new(),
27 since_ms: time::now_millis(),
28 }
29 }
30}
31
32#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
37pub struct EventOps {
38 pub load_calls: u64,
40 pub save_calls: u64,
41 pub delete_calls: u64,
42
43 pub plan_index: u64,
45 pub plan_keys: u64,
46 pub plan_range: u64,
47
48 pub rows_loaded: u64,
50 pub rows_deleted: u64,
51
52 pub index_inserts: u64,
54 pub index_removes: u64,
55 pub unique_violations: u64,
56}
57
58#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
63pub struct EntityCounters {
64 pub load_calls: u64,
65 pub save_calls: u64,
66 pub delete_calls: u64,
67 pub rows_loaded: u64,
68 pub rows_deleted: u64,
69 pub index_inserts: u64,
70 pub index_removes: u64,
71 pub unique_violations: u64,
72}
73
74#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
79pub struct EventPerf {
80 pub load_inst_total: u128,
82 pub save_inst_total: u128,
83 pub delete_inst_total: u128,
84
85 pub load_inst_max: u64,
87 pub save_inst_max: u64,
88 pub delete_inst_max: u64,
89}
90
91thread_local! {
92 static EVENT_STATE: RefCell<EventState> = RefCell::new(EventState::default());
93}
94
95pub fn with_state<R>(f: impl FnOnce(&EventState) -> R) -> R {
97 EVENT_STATE.with(|m| f(&m.borrow()))
98}
99
100pub fn with_state_mut<R>(f: impl FnOnce(&mut EventState) -> R) -> R {
102 EVENT_STATE.with(|m| f(&mut m.borrow_mut()))
103}
104
105pub fn reset() {
107 with_state_mut(|m| *m = EventState::default());
108}
109
110pub fn reset_all() {
112 reset();
113}
114
115#[allow(clippy::missing_const_for_fn)]
117pub fn add_instructions(total: &mut u128, max: &mut u64, delta_inst: u64) {
118 *total = total.saturating_add(u128::from(delta_inst));
119 if delta_inst > *max {
120 *max = delta_inst;
121 }
122}
123
124#[derive(Clone, Copy, Debug)]
129pub enum ExecKind {
130 Load,
131 Save,
132 Delete,
133}
134
135#[must_use]
138pub fn exec_start(kind: ExecKind) -> u64 {
139 with_state_mut(|m| match kind {
140 ExecKind::Load => m.ops.load_calls = m.ops.load_calls.saturating_add(1),
141 ExecKind::Save => m.ops.save_calls = m.ops.save_calls.saturating_add(1),
142 ExecKind::Delete => m.ops.delete_calls = m.ops.delete_calls.saturating_add(1),
143 });
144
145 performance_counter(1)
147}
148
149pub fn exec_finish(kind: ExecKind, start_inst: u64, rows_touched: u64) {
151 let now = performance_counter(1);
152 let delta = now.saturating_sub(start_inst);
153
154 with_state_mut(|m| match kind {
155 ExecKind::Load => {
156 m.ops.rows_loaded = m.ops.rows_loaded.saturating_add(rows_touched);
157 add_instructions(
158 &mut m.perf.load_inst_total,
159 &mut m.perf.load_inst_max,
160 delta,
161 );
162 }
163 ExecKind::Save => {
164 add_instructions(
165 &mut m.perf.save_inst_total,
166 &mut m.perf.save_inst_max,
167 delta,
168 );
169 }
170 ExecKind::Delete => {
171 m.ops.rows_deleted = m.ops.rows_deleted.saturating_add(rows_touched);
172 add_instructions(
173 &mut m.perf.delete_inst_total,
174 &mut m.perf.delete_inst_max,
175 delta,
176 );
177 }
178 });
179}
180
181#[must_use]
183pub fn exec_start_for<E>(kind: ExecKind) -> u64
184where
185 E: EntityKind,
186{
187 let start = exec_start(kind);
188 with_state_mut(|m| {
189 let entry = m.entities.entry(E::PATH.to_string()).or_default();
190 match kind {
191 ExecKind::Load => entry.load_calls = entry.load_calls.saturating_add(1),
192 ExecKind::Save => entry.save_calls = entry.save_calls.saturating_add(1),
193 ExecKind::Delete => entry.delete_calls = entry.delete_calls.saturating_add(1),
194 }
195 });
196 start
197}
198
199pub fn exec_finish_for<E>(kind: ExecKind, start_inst: u64, rows_touched: u64)
200where
201 E: EntityKind,
202{
203 exec_finish(kind, start_inst, rows_touched);
204 with_state_mut(|m| {
205 let entry = m.entities.entry(E::PATH.to_string()).or_default();
206 match kind {
207 ExecKind::Load => entry.rows_loaded = entry.rows_loaded.saturating_add(rows_touched),
208 ExecKind::Delete => {
209 entry.rows_deleted = entry.rows_deleted.saturating_add(rows_touched);
210 }
211 ExecKind::Save => {}
212 }
213 });
214}
215
216pub struct Span<E: EntityKind> {
222 kind: ExecKind,
223 start: u64,
224 rows: u64,
225 finished: bool,
226 _marker: PhantomData<E>,
227}
228
229impl<E: EntityKind> Span<E> {
230 #[must_use]
231 pub fn new(kind: ExecKind) -> Self {
232 Self {
233 kind,
234 start: exec_start_for::<E>(kind),
235 rows: 0,
236 finished: false,
237 _marker: PhantomData,
238 }
239 }
240
241 pub const fn set_rows(&mut self, rows: u64) {
242 self.rows = rows;
243 }
244
245 pub const fn add_rows(&mut self, rows: u64) {
246 self.rows = self.rows.saturating_add(rows);
247 }
248
249 pub fn finish(mut self) {
250 if !self.finished {
251 exec_finish_for::<E>(self.kind, self.start, self.rows);
252 self.finished = true;
253 }
254 }
255}
256
257impl<E: EntityKind> Drop for Span<E> {
258 fn drop(&mut self) {
259 if !self.finished {
260 exec_finish_for::<E>(self.kind, self.start, self.rows);
261 self.finished = true;
262 }
263 }
264}
265
266#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
272pub struct EventReport {
273 pub counters: Option<EventState>,
275 pub entity_counters: Vec<EntitySummary>,
277}
278
279#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
284pub struct EntitySummary {
285 pub path: String,
286 pub load_calls: u64,
287 pub delete_calls: u64,
288 pub rows_loaded: u64,
289 pub rows_deleted: u64,
290 pub avg_rows_per_load: f64,
291 pub avg_rows_per_delete: f64,
292 pub index_inserts: u64,
293 pub index_removes: u64,
294 pub unique_violations: u64,
295}
296
297pub fn record_unique_violation_for<E>(m: &mut EventState)
299where
300 E: crate::traits::EntityKind,
301{
302 m.ops.unique_violations = m.ops.unique_violations.saturating_add(1);
303 let entry = m.entities.entry(E::PATH.to_string()).or_default();
304 entry.unique_violations = entry.unique_violations.saturating_add(1);
305}
306
307#[derive(CandidType, Clone, Copy, Debug, Deserialize, Serialize)]
313#[allow(clippy::struct_excessive_bools)]
314pub struct EventSelect {
315 pub data: bool,
316 pub index: bool,
317 pub counters: bool,
318 pub entities: bool,
319}
320
321impl EventSelect {
322 #[must_use]
323 pub const fn all() -> Self {
324 Self {
325 data: true,
326 index: true,
327 counters: true,
328 entities: true,
329 }
330 }
331}
332
333impl Default for EventSelect {
334 fn default() -> Self {
335 Self::all()
336 }
337}
338
339#[must_use]
341#[allow(clippy::cast_precision_loss)]
342pub fn report() -> EventReport {
343 let snap = with_state(Clone::clone);
344
345 let mut entity_counters: Vec<EntitySummary> = Vec::new();
346 for (path, ops) in &snap.entities {
347 let avg_load = if ops.load_calls > 0 {
348 ops.rows_loaded as f64 / ops.load_calls as f64
349 } else {
350 0.0
351 };
352 let avg_delete = if ops.delete_calls > 0 {
353 ops.rows_deleted as f64 / ops.delete_calls as f64
354 } else {
355 0.0
356 };
357
358 entity_counters.push(EntitySummary {
359 path: path.clone(),
360 load_calls: ops.load_calls,
361 delete_calls: ops.delete_calls,
362 rows_loaded: ops.rows_loaded,
363 rows_deleted: ops.rows_deleted,
364 avg_rows_per_load: avg_load,
365 avg_rows_per_delete: avg_delete,
366 index_inserts: ops.index_inserts,
367 index_removes: ops.index_removes,
368 unique_violations: ops.unique_violations,
369 });
370 }
371
372 entity_counters.sort_by(|a, b| {
373 match b
374 .avg_rows_per_load
375 .partial_cmp(&a.avg_rows_per_load)
376 .unwrap_or(Ordering::Equal)
377 {
378 Ordering::Equal => match b.rows_loaded.cmp(&a.rows_loaded) {
379 Ordering::Equal => a.path.cmp(&b.path),
380 other => other,
381 },
382 other => other,
383 }
384 });
385
386 EventReport {
387 counters: Some(snap),
388 entity_counters,
389 }
390}