1use crate::ids::{EndpointCall, EndpointId};
11use std::{cell::RefCell, collections::HashMap};
12
13thread_local! {
14 #[cfg(not(test))]
16 pub static PERF_LAST: RefCell<u64> = RefCell::new(perf_counter());
17
18 #[cfg(test)]
20 pub static PERF_LAST: RefCell<u64> = const { RefCell::new(0) };
21
22 static PERF_TABLE: RefCell<HashMap<PerfKey, PerfSlot>> = RefCell::new(HashMap::new());
24
25 static PERF_STACK: RefCell<Vec<PerfFrame>> = const { RefCell::new(Vec::new()) };
28}
29
30#[must_use]
54pub fn perf_counter() -> u64 {
55 crate::cdk::api::performance_counter(1)
56}
57
58#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
64pub enum PerfKey {
65 Endpoint(String),
66 Timer(String),
67 Checkpoint { scope: String, label: String },
68}
69
70struct PerfFrame {
76 start: u64,
77 child_instructions: u64,
78}
79
80#[derive(Default)]
85struct PerfSlot {
86 count: u64,
87 total_instructions: u64,
88}
89
90impl PerfSlot {
91 const fn increment(&mut self, delta: u64) {
92 self.count = self.count.saturating_add(1);
93 self.total_instructions = self.total_instructions.saturating_add(delta);
94 }
95}
96
97#[derive(Clone)]
103pub struct PerfEntry {
104 pub key: PerfKey,
105 pub count: u64,
106 pub total_instructions: u64,
107}
108
109pub fn record(key: PerfKey, delta: u64) {
111 PERF_TABLE.with(|table| {
112 let mut table = table.borrow_mut();
113 table.entry(key).or_default().increment(delta);
114 });
115}
116
117pub fn record_endpoint(endpoint: EndpointId, delta_instructions: u64) {
118 record(
119 PerfKey::Endpoint(endpoint.name.to_string()),
120 delta_instructions,
121 );
122}
123
124pub fn record_timer(label: &str, delta_instructions: u64) {
125 record(PerfKey::Timer(label.to_string()), delta_instructions);
126}
127
128pub fn record_checkpoint(scope: &str, label: &str, delta_instructions: u64) {
129 record(
130 PerfKey::Checkpoint {
131 scope: scope.to_string(),
132 label: label.to_string(),
133 },
134 delta_instructions,
135 );
136}
137
138pub(crate) fn enter_endpoint() {
140 enter_endpoint_at(perf_counter());
141}
142
143pub(crate) fn exit_endpoint(call: EndpointCall) {
145 exit_endpoint_at(call.endpoint, perf_counter());
146}
147
148fn enter_endpoint_at(start: u64) {
149 PERF_STACK.with(|stack| {
150 let mut stack = stack.borrow_mut();
151
152 if let Some(last) = stack.last()
154 && start < last.start
155 {
156 stack.clear();
157 }
158
159 stack.push(PerfFrame {
160 start,
161 child_instructions: 0,
162 });
163 });
164}
165
166fn exit_endpoint_at(endpoint: EndpointId, end: u64) {
167 PERF_STACK.with(|stack| {
168 let mut stack = stack.borrow_mut();
169 let Some(frame) = stack.pop() else {
170 record_endpoint(endpoint, end);
171 return;
172 };
173
174 let total = end.saturating_sub(frame.start);
175 let exclusive = total.saturating_sub(frame.child_instructions);
176
177 if let Some(parent) = stack.last_mut() {
178 parent.child_instructions = parent.child_instructions.saturating_add(total);
179 }
180
181 record_endpoint(endpoint, exclusive);
182 });
183}
184
185#[must_use]
188pub fn entries() -> Vec<PerfEntry> {
189 PERF_TABLE.with(|table| {
190 let table = table.borrow();
191
192 let mut out: Vec<PerfEntry> = table
193 .iter()
194 .map(|(key, slot)| PerfEntry {
195 key: key.clone(),
196 count: slot.count,
197 total_instructions: slot.total_instructions,
198 })
199 .collect();
200
201 out.sort_by(|a, b| a.key.cmp(&b.key));
202 out
203 })
204}
205
206#[cfg(test)]
211pub fn reset() {
212 PERF_TABLE.with(|t| t.borrow_mut().clear());
213 PERF_LAST.with(|last| *last.borrow_mut() = 0);
214 PERF_STACK.with(|stack| stack.borrow_mut().clear());
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 fn checkpoint_at(now: u64) {
222 PERF_LAST.with(|last| *last.borrow_mut() = now);
223 }
224
225 fn entry_for(label: &str) -> PerfEntry {
226 entries()
227 .into_iter()
228 .find(|entry| matches!(&entry.key, PerfKey::Endpoint(l) if l == label))
229 .expect("expected perf entry to exist")
230 }
231
232 fn checkpoint_entry_for(scope: &str, label: &str) -> PerfEntry {
233 entries()
234 .into_iter()
235 .find(|entry| {
236 matches!(
237 &entry.key,
238 PerfKey::Checkpoint {
239 scope: entry_scope,
240 label: entry_label,
241 } if entry_scope == scope && entry_label == label
242 )
243 })
244 .expect("expected checkpoint perf entry to exist")
245 }
246
247 #[test]
248 fn nested_endpoints_record_exclusive_totals() {
249 reset();
250
251 enter_endpoint_at(100);
252 checkpoint_at(140);
253
254 enter_endpoint_at(200);
255 checkpoint_at(230);
256 exit_endpoint_at(EndpointId::new("child"), 260);
257
258 exit_endpoint_at(EndpointId::new("parent"), 300);
259
260 let parent = entry_for("parent");
261 let child = entry_for("child");
262
263 assert_eq!(child.count, 1);
264 assert_eq!(child.total_instructions, 60);
265 assert_eq!(parent.count, 1);
266 assert_eq!(parent.total_instructions, 140);
267 }
268
269 #[test]
270 fn checkpoints_record_scope_and_label() {
271 reset();
272
273 record_checkpoint("workflow::bootstrap", "load_cfg", 120);
274 record_checkpoint("workflow::bootstrap", "load_cfg", 80);
275
276 let checkpoint = checkpoint_entry_for("workflow::bootstrap", "load_cfg");
277
278 assert_eq!(checkpoint.count, 2);
279 assert_eq!(checkpoint.total_instructions, 200);
280 }
281}