canic_core/
perf.rs

1//! Cross-cutting performance instrumentation.
2//!
3//! This module provides instruction-count measurement primitives used
4//! across endpoints, ops, timers, and background tasks.
5//!
6//! It is intentionally crate-level infrastructure, not part of the
7//! domain layering (endpoints → ops → model).
8
9use canic_cdk::candid::CandidType;
10use serde::{Deserialize, Serialize};
11use std::{cell::RefCell, collections::HashMap};
12
13thread_local! {
14    /// Last snapshot used by the `perf!` macro.
15    #[cfg(not(test))]
16    pub static PERF_LAST: RefCell<u64> = RefCell::new(perf_counter());
17
18    // Unit tests run outside a canister context, so `perf_counter()` would trap.
19    #[cfg(test)]
20    pub static PERF_LAST: RefCell<u64> = const { RefCell::new(0) };
21
22    /// Aggregated perf counters keyed by kind (endpoint vs timer) and label.
23    static PERF_TABLE: RefCell<HashMap<PerfKey, PerfSlot>> = RefCell::new(HashMap::new());
24}
25
26/// Returns the **call-context instruction counter** for the current execution.
27///
28/// This value is obtained from `ic0.performance_counter(1)` and represents the
29/// total number of WebAssembly instructions executed by *this canister* within
30/// the **current call context**.
31///
32/// Key properties:
33/// - Monotonically increasing for the duration of the call context
34/// - Accumulates across `await` points and resumptions
35/// - Resets only when a new call context begins
36/// - Counts *only* instructions executed by this canister (not other canisters)
37///
38/// This counter is suitable for:
39/// - Endpoint-level performance accounting
40/// - Async workflows and timers
41/// - Regression detection and coarse-grained profiling
42///
43/// It is **not** a measure of cycle cost. Expensive inter-canister operations
44/// (e.g., canister creation) may have low instruction counts here but high cycle
45/// charges elsewhere.
46///
47/// For fine-grained, single-slice profiling (e.g., hot loops), use
48/// `ic0.performance_counter(0)` instead.
49#[must_use]
50pub fn perf_counter() -> u64 {
51    crate::cdk::api::performance_counter(1)
52}
53
54///
55/// PerfKey
56/// splitting up by Timer type to avoid confusing string comparisons
57///
58
59#[derive(
60    CandidType, Clone, Debug, Deserialize, Serialize, Eq, Hash, Ord, PartialEq, PartialOrd,
61)]
62pub enum PerfKey {
63    Endpoint(String),
64    Timer(String),
65}
66
67///
68/// PerfSlot
69///
70
71#[derive(Default)]
72struct PerfSlot {
73    count: u64,
74    total_instructions: u64,
75}
76
77impl PerfSlot {
78    const fn increment(&mut self, delta: u64) {
79        self.count = self.count.saturating_add(1);
80        self.total_instructions = self.total_instructions.saturating_add(delta);
81    }
82}
83
84///
85/// PerfEntry
86/// Aggregated perf counters keyed by kind (endpoint vs timer) and label.
87///
88
89#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
90pub struct PerfEntry {
91    pub label: String,
92    pub key: PerfKey,
93    pub count: u64,
94    pub total_instructions: u64,
95}
96
97/// Record a counter under the provided key.
98pub fn record(key: PerfKey, delta: u64) {
99    PERF_TABLE.with(|table| {
100        let mut table = table.borrow_mut();
101        table.entry(key).or_default().increment(delta);
102    });
103}
104
105pub fn record_endpoint(func: &str, total_instructions: u64) {
106    record(PerfKey::Endpoint(func.to_string()), total_instructions);
107}
108
109pub fn record_timer(label: &str, delta_instructions: u64) {
110    record(PerfKey::Timer(label.to_string()), delta_instructions);
111}
112
113/// Snapshot all recorded perf counters, sorted by key.
114#[must_use]
115pub fn entries() -> Vec<PerfEntry> {
116    PERF_TABLE.with(|table| {
117        let table = table.borrow();
118
119        let mut out: Vec<PerfEntry> = table
120            .iter()
121            .map(|(key, slot)| PerfEntry {
122                label: match key {
123                    PerfKey::Endpoint(label) | PerfKey::Timer(label) => label.clone(),
124                },
125                key: key.clone(),
126                count: slot.count,
127                total_instructions: slot.total_instructions,
128            })
129            .collect();
130
131        out.sort_by(|a, b| a.key.cmp(&b.key));
132        out
133    })
134}
135
136///
137/// TESTS
138///
139
140#[cfg(test)]
141pub fn reset() {
142    PERF_TABLE.with(|t| t.borrow_mut().clear());
143    PERF_LAST.with(|last| *last.borrow_mut() = 0);
144}