use crate::ids::{EndpointCall, EndpointId};
use std::{cell::RefCell, collections::HashMap};
thread_local! {
#[cfg(not(test))]
#[allow(clippy::missing_const_for_thread_local)]
pub static PERF_LAST: RefCell<u64> = RefCell::new(perf_counter());
#[cfg(test)]
pub static PERF_LAST: RefCell<u64> = const { RefCell::new(0) };
static PERF_TABLE: RefCell<HashMap<PerfKey, PerfSlot>> = RefCell::new(HashMap::new());
static PERF_STACK: RefCell<Vec<PerfFrame>> = const { RefCell::new(Vec::new()) };
}
#[must_use]
#[allow(clippy::missing_const_for_fn)]
pub fn perf_counter() -> u64 {
#[cfg(target_arch = "wasm32")]
{
crate::cdk::api::performance_counter(1)
}
#[cfg(not(target_arch = "wasm32"))]
{
0
}
}
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PerfKey {
Endpoint(String),
Timer(String),
Checkpoint { scope: String, label: String },
}
struct PerfFrame {
start: u64,
child_instructions: u64,
}
#[derive(Default)]
struct PerfSlot {
count: u64,
total_instructions: u64,
}
impl PerfSlot {
const fn increment(&mut self, delta: u64) {
self.count = self.count.saturating_add(1);
self.total_instructions = self.total_instructions.saturating_add(delta);
}
}
#[derive(Clone)]
pub struct PerfEntry {
pub key: PerfKey,
pub count: u64,
pub total_instructions: u64,
}
pub fn record(key: PerfKey, delta: u64) {
PERF_TABLE.with(|table| {
let mut table = table.borrow_mut();
table.entry(key).or_default().increment(delta);
});
}
pub fn record_endpoint(endpoint: EndpointId, delta_instructions: u64) {
record(
PerfKey::Endpoint(endpoint.name.to_string()),
delta_instructions,
);
}
pub fn record_timer(label: &str, delta_instructions: u64) {
record(PerfKey::Timer(label.to_string()), delta_instructions);
}
pub fn record_checkpoint(scope: &str, label: &str, delta_instructions: u64) {
record(
PerfKey::Checkpoint {
scope: scope.to_string(),
label: label.to_string(),
},
delta_instructions,
);
}
pub(crate) fn enter_endpoint() {
enter_endpoint_at(perf_counter());
}
pub(crate) fn exit_endpoint(call: EndpointCall) {
exit_endpoint_at(call.endpoint, perf_counter());
}
fn enter_endpoint_at(start: u64) {
PERF_STACK.with(|stack| {
let mut stack = stack.borrow_mut();
if let Some(last) = stack.last()
&& start < last.start
{
stack.clear();
}
stack.push(PerfFrame {
start,
child_instructions: 0,
});
});
}
fn exit_endpoint_at(endpoint: EndpointId, end: u64) {
PERF_STACK.with(|stack| {
let mut stack = stack.borrow_mut();
let Some(frame) = stack.pop() else {
record_endpoint(endpoint, end);
return;
};
let total = end.saturating_sub(frame.start);
let exclusive = total.saturating_sub(frame.child_instructions);
if let Some(parent) = stack.last_mut() {
parent.child_instructions = parent.child_instructions.saturating_add(total);
}
record_endpoint(endpoint, exclusive);
});
}
#[must_use]
pub fn entries() -> Vec<PerfEntry> {
PERF_TABLE.with(|table| {
let table = table.borrow();
let mut out: Vec<PerfEntry> = table
.iter()
.map(|(key, slot)| PerfEntry {
key: key.clone(),
count: slot.count,
total_instructions: slot.total_instructions,
})
.collect();
out.sort_by(|a, b| a.key.cmp(&b.key));
out
})
}
#[cfg(test)]
pub fn reset() {
PERF_TABLE.with(|t| t.borrow_mut().clear());
PERF_LAST.with(|last| *last.borrow_mut() = 0);
PERF_STACK.with(|stack| stack.borrow_mut().clear());
}
#[cfg(test)]
mod tests {
use super::*;
fn checkpoint_at(now: u64) {
PERF_LAST.with(|last| *last.borrow_mut() = now);
}
fn entry_for(label: &str) -> PerfEntry {
entries()
.into_iter()
.find(|entry| matches!(&entry.key, PerfKey::Endpoint(l) if l == label))
.expect("expected perf entry to exist")
}
fn checkpoint_entry_for(scope: &str, label: &str) -> PerfEntry {
entries()
.into_iter()
.find(|entry| {
matches!(
&entry.key,
PerfKey::Checkpoint {
scope: entry_scope,
label: entry_label,
} if entry_scope == scope && entry_label == label
)
})
.expect("expected checkpoint perf entry to exist")
}
#[test]
fn nested_endpoints_record_exclusive_totals() {
reset();
enter_endpoint_at(100);
checkpoint_at(140);
enter_endpoint_at(200);
checkpoint_at(230);
exit_endpoint_at(EndpointId::new("child"), 260);
exit_endpoint_at(EndpointId::new("parent"), 300);
let parent = entry_for("parent");
let child = entry_for("child");
assert_eq!(child.count, 1);
assert_eq!(child.total_instructions, 60);
assert_eq!(parent.count, 1);
assert_eq!(parent.total_instructions, 140);
}
#[test]
fn checkpoints_record_scope_and_label() {
reset();
record_checkpoint("workflow::bootstrap", "load_cfg", 120);
record_checkpoint("workflow::bootstrap", "load_cfg", 80);
let checkpoint = checkpoint_entry_for("workflow::bootstrap", "load_cfg");
assert_eq!(checkpoint.count, 2);
assert_eq!(checkpoint.total_instructions, 200);
}
}