1use candid::CandidType;
6use canic_cdk::utils::time::now_millis;
7use serde::{Deserialize, Serialize};
8use std::{cell::RefCell, cmp::Ordering, collections::BTreeMap};
9
10#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
15pub struct EventState {
16 pub ops: EventOps,
17 pub perf: EventPerf,
18 pub entities: BTreeMap<String, EntityCounters>,
19 pub since_ms: u64,
20}
21
22impl Default for EventState {
23 fn default() -> Self {
24 Self {
25 ops: EventOps::default(),
26 perf: EventPerf::default(),
27 entities: BTreeMap::new(),
28 since_ms: now_millis(),
29 }
30 }
31}
32
33#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
40pub struct EventOps {
41 pub load_calls: u64,
43 pub exists_calls: u64,
44 pub save_calls: u64,
45 pub delete_calls: u64,
46
47 pub plan_index: u64,
49 pub plan_keys: u64,
50 pub plan_range: u64,
51 pub plan_full_scan: u64,
52
53 pub rows_loaded: u64,
55 pub rows_scanned: u64,
56 pub rows_deleted: u64,
57
58 pub index_inserts: u64,
60 pub index_removes: u64,
61 pub unique_violations: u64,
62}
63
64#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
69pub struct EntityCounters {
70 pub load_calls: u64,
71 pub exists_calls: u64,
72 pub save_calls: u64,
73 pub delete_calls: u64,
74 pub rows_loaded: u64,
75 pub rows_scanned: u64,
76 pub rows_deleted: u64,
77 pub index_inserts: u64,
78 pub index_removes: u64,
79 pub unique_violations: u64,
80}
81
82#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
89pub struct EventPerf {
90 pub load_inst_total: u128,
92 pub save_inst_total: u128,
93 pub delete_inst_total: u128,
94
95 pub load_inst_max: u64,
97 pub save_inst_max: u64,
98 pub delete_inst_max: u64,
99}
100
101thread_local! {
102 static EVENT_STATE: RefCell<EventState> = RefCell::new(EventState::default());
103}
104
105pub(crate) fn with_state<R>(f: impl FnOnce(&EventState) -> R) -> R {
107 EVENT_STATE.with(|m| f(&m.borrow()))
108}
109
110pub(crate) fn with_state_mut<R>(f: impl FnOnce(&mut EventState) -> R) -> R {
112 EVENT_STATE.with(|m| f(&mut m.borrow_mut()))
113}
114
115pub fn reset() {
117 with_state_mut(|m| *m = EventState::default());
118}
119
120pub fn reset_all() {
122 reset();
123}
124
125#[allow(clippy::missing_const_for_fn)]
127pub fn add_instructions(total: &mut u128, max: &mut u64, delta_inst: u64) {
128 *total = total.saturating_add(u128::from(delta_inst));
129 if delta_inst > *max {
130 *max = delta_inst;
131 }
132}
133
134#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
140pub struct EventReport {
141 pub counters: Option<EventState>,
143 pub entity_counters: Vec<EntitySummary>,
145}
146
147#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
152pub struct EntitySummary {
153 pub path: String,
154 pub load_calls: u64,
155 pub exists_calls: u64,
156 pub delete_calls: u64,
157 pub rows_loaded: u64,
158 pub rows_scanned: u64,
159 pub rows_deleted: u64,
160 pub avg_rows_per_load: f64,
161 pub avg_rows_scanned_per_load: f64,
162 pub avg_rows_per_delete: f64,
163 pub index_inserts: u64,
164 pub index_removes: u64,
165 pub unique_violations: u64,
166}
167
168#[derive(CandidType, Clone, Copy, Debug, Deserialize, Serialize)]
174#[allow(clippy::struct_excessive_bools)]
175pub struct EventSelect {
176 pub data: bool,
177 pub index: bool,
178 pub counters: bool,
179 pub entities: bool,
180}
181
182impl EventSelect {
183 #[must_use]
184 pub const fn all() -> Self {
185 Self {
186 data: true,
187 index: true,
188 counters: true,
189 entities: true,
190 }
191 }
192}
193
194impl Default for EventSelect {
195 fn default() -> Self {
196 Self::all()
197 }
198}
199
200#[must_use]
202#[allow(clippy::cast_precision_loss)]
203pub fn report() -> EventReport {
204 let snap = with_state(Clone::clone);
205
206 let mut entity_counters: Vec<EntitySummary> = Vec::new();
207 for (path, ops) in &snap.entities {
208 let avg_load = if ops.load_calls > 0 {
209 ops.rows_loaded as f64 / ops.load_calls as f64
210 } else {
211 0.0
212 };
213 let avg_scanned = if ops.load_calls > 0 {
214 ops.rows_scanned as f64 / ops.load_calls as f64
215 } else {
216 0.0
217 };
218 let avg_delete = if ops.delete_calls > 0 {
219 ops.rows_deleted as f64 / ops.delete_calls as f64
220 } else {
221 0.0
222 };
223
224 entity_counters.push(EntitySummary {
225 path: path.clone(),
226 load_calls: ops.load_calls,
227 exists_calls: ops.exists_calls,
228 delete_calls: ops.delete_calls,
229 rows_loaded: ops.rows_loaded,
230 rows_scanned: ops.rows_scanned,
231 rows_deleted: ops.rows_deleted,
232 avg_rows_per_load: avg_load,
233 avg_rows_scanned_per_load: avg_scanned,
234 avg_rows_per_delete: avg_delete,
235 index_inserts: ops.index_inserts,
236 index_removes: ops.index_removes,
237 unique_violations: ops.unique_violations,
238 });
239 }
240
241 entity_counters.sort_by(|a, b| {
242 match b
243 .avg_rows_per_load
244 .partial_cmp(&a.avg_rows_per_load)
245 .unwrap_or(Ordering::Equal)
246 {
247 Ordering::Equal => match b.rows_loaded.cmp(&a.rows_loaded) {
248 Ordering::Equal => a.path.cmp(&b.path),
249 other => other,
250 },
251 other => other,
252 }
253 });
254
255 EventReport {
256 counters: Some(snap),
257 entity_counters,
258 }
259}
260
261#[cfg(test)]
266#[allow(clippy::float_cmp)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn reset_all_clears_state() {
272 with_state_mut(|m| {
273 m.ops.load_calls = 3;
274 m.ops.index_inserts = 2;
275 m.perf.save_inst_max = 9;
276 m.entities.insert(
277 "alpha".to_string(),
278 EntityCounters {
279 load_calls: 1,
280 ..Default::default()
281 },
282 );
283 });
284
285 reset_all();
286
287 with_state(|m| {
288 assert_eq!(m.ops.load_calls, 0);
289 assert_eq!(m.ops.index_inserts, 0);
290 assert_eq!(m.perf.save_inst_max, 0);
291 assert!(m.entities.is_empty());
292 });
293 }
294
295 #[test]
296 fn report_sorts_entities_by_average_rows() {
297 reset_all();
298 with_state_mut(|m| {
299 m.entities.insert(
300 "alpha".to_string(),
301 EntityCounters {
302 load_calls: 2,
303 rows_loaded: 6,
304 ..Default::default()
305 },
306 );
307 m.entities.insert(
308 "beta".to_string(),
309 EntityCounters {
310 load_calls: 1,
311 rows_loaded: 5,
312 ..Default::default()
313 },
314 );
315 m.entities.insert(
316 "gamma".to_string(),
317 EntityCounters {
318 load_calls: 2,
319 rows_loaded: 6,
320 ..Default::default()
321 },
322 );
323 });
324
325 let report = report();
326 let paths: Vec<_> = report
327 .entity_counters
328 .iter()
329 .map(|e| e.path.as_str())
330 .collect();
331
332 assert_eq!(paths, ["beta", "alpha", "gamma"]);
334 assert_eq!(report.entity_counters[0].avg_rows_per_load, 5.0);
335 assert_eq!(report.entity_counters[1].avg_rows_per_load, 3.0);
336 assert_eq!(report.entity_counters[2].avg_rows_per_load, 3.0);
337 }
338}