proof_engine/debug/
profiler.rs1use std::collections::HashMap;
8use std::time::Instant;
9
10#[derive(Clone, Debug)]
14pub struct TimingSpan {
15 pub name: String,
16 pub duration_us: u64, }
18
19struct SpanAccumulator {
22 start: Instant,
23 samples: Vec<u64>,
24 capacity: usize,
25 head: usize,
26 filled: bool,
27}
28
29impl SpanAccumulator {
30 fn new(capacity: usize) -> Self {
31 Self {
32 start: Instant::now(),
33 samples: vec![0; capacity],
34 capacity,
35 head: 0,
36 filled: false,
37 }
38 }
39
40 fn begin(&mut self) { self.start = Instant::now(); }
41
42 fn end(&mut self) {
43 let dur = self.start.elapsed().as_micros() as u64;
44 self.samples[self.head] = dur;
45 self.head = (self.head + 1) % self.capacity;
46 if self.head == 0 { self.filled = true; }
47 }
48
49 fn avg_us(&self) -> u64 {
50 let count = if self.filled { self.capacity } else { self.head.max(1) };
51 let sum: u64 = self.samples[..count].iter().sum();
52 sum / count as u64
53 }
54
55 fn max_us(&self) -> u64 {
56 let count = if self.filled { self.capacity } else { self.head.max(1) };
57 self.samples[..count].iter().cloned().max().unwrap_or(0)
58 }
59
60 fn min_us(&self) -> u64 {
61 let count = if self.filled { self.capacity } else { self.head.max(1) };
62 self.samples[..count].iter().cloned().min().unwrap_or(0)
63 }
64
65 fn last_us(&self) -> u64 {
66 let idx = if self.head == 0 { self.capacity - 1 } else { self.head - 1 };
67 self.samples[idx]
68 }
69}
70
71pub struct FrameProfiler {
78 spans: HashMap<String, SpanAccumulator>,
79 window_frames: usize,
80 frame_start: Instant,
82 frame_count: u64,
83}
84
85impl FrameProfiler {
86 pub fn new(window_frames: usize) -> Self {
88 Self {
89 spans: HashMap::new(),
90 window_frames: window_frames.max(1),
91 frame_start: Instant::now(),
92 frame_count: 0,
93 }
94 }
95
96 pub fn begin(&mut self, name: &str) {
98 let cap = self.window_frames;
99 self.spans.entry(name.to_string())
100 .or_insert_with(|| SpanAccumulator::new(cap))
101 .begin();
102 }
103
104 pub fn end(&mut self, name: &str) {
106 if let Some(acc) = self.spans.get_mut(name) {
107 acc.end();
108 }
109 }
110
111 pub fn begin_frame(&mut self) {
113 self.frame_start = Instant::now();
114 self.begin("frame");
115 self.frame_count += 1;
116 }
117
118 pub fn end_frame(&mut self) {
120 self.end("frame");
121 }
122
123 pub fn avg_us(&self, name: &str) -> u64 {
125 self.spans.get(name).map(|s| s.avg_us()).unwrap_or(0)
126 }
127
128 pub fn max_us(&self, name: &str) -> u64 {
130 self.spans.get(name).map(|s| s.max_us()).unwrap_or(0)
131 }
132
133 pub fn min_us(&self, name: &str) -> u64 {
135 self.spans.get(name).map(|s| s.min_us()).unwrap_or(0)
136 }
137
138 pub fn last_us(&self, name: &str) -> u64 {
140 self.spans.get(name).map(|s| s.last_us()).unwrap_or(0)
141 }
142
143 pub fn fps(&self) -> f32 {
145 let avg_us = self.avg_us("frame");
146 if avg_us == 0 { return 0.0; }
147 1_000_000.0 / avg_us as f32
148 }
149
150 pub fn frame_count(&self) -> u64 { self.frame_count }
152
153 pub fn report(&self) -> Vec<(&str, u64)> {
155 let mut out: Vec<(&str, u64)> = self.spans.iter()
156 .map(|(name, acc)| (name.as_str(), acc.avg_us()))
157 .collect();
158 out.sort_by(|a, b| b.1.cmp(&a.1));
159 out
160 }
161
162 pub fn format_report(&self) -> String {
164 let mut lines = Vec::new();
165 lines.push(format!("=== Frame Profiler ({}f window) ===", self.window_frames));
166 lines.push(format!("Frame #{} — FPS: {:.1}", self.frame_count, self.fps()));
167 for (name, avg_us) in self.report() {
168 let max_us = self.max_us(name);
169 let last = self.last_us(name);
170 lines.push(format!(" {:20} avg={:>6}µs max={:>6}µs last={:>6}µs",
171 name, avg_us, max_us, last));
172 }
173 lines.join("\n")
174 }
175
176 pub fn reset(&mut self) {
178 self.spans.clear();
179 self.frame_count = 0;
180 }
181
182 pub fn over_budget(&self, budget_ms: f32) -> bool {
184 self.avg_us("frame") > (budget_ms * 1000.0) as u64
185 }
186}
187
188pub struct ScopedSpan<'a> {
201 profiler: &'a mut FrameProfiler,
202 name: String,
203}
204
205impl<'a> ScopedSpan<'a> {
206 pub fn new(profiler: &'a mut FrameProfiler, name: &str) -> Self {
207 profiler.begin(name);
208 Self { profiler, name: name.to_string() }
209 }
210}
211
212impl<'a> Drop for ScopedSpan<'a> {
213 fn drop(&mut self) {
214 self.profiler.end(&self.name);
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use std::thread;
222 use std::time::Duration;
223
224 #[test]
225 fn profiler_measures_sleep() {
226 let mut p = FrameProfiler::new(10);
227 p.begin("sleep");
228 thread::sleep(Duration::from_millis(5));
229 p.end("sleep");
230 let avg = p.avg_us("sleep");
231 assert!(avg >= 3000, "expected >= 3ms sleep, got {}µs", avg);
232 }
233
234 #[test]
235 fn profiler_fps() {
236 let mut p = FrameProfiler::new(5);
237 for _ in 0..5 {
238 p.begin("frame");
239 thread::sleep(Duration::from_millis(16));
240 p.end("frame");
241 }
242 let fps = p.fps();
243 assert!(fps > 40.0 && fps < 100.0, "FPS should be ~60 ±40, got {}", fps);
244 }
245
246 #[test]
247 fn report_sorted_by_duration() {
248 let mut p = FrameProfiler::new(4);
249 p.begin("slow"); thread::sleep(Duration::from_millis(10)); p.end("slow");
250 p.begin("fast"); thread::sleep(Duration::from_millis(1)); p.end("fast");
251 let report = p.report();
252 assert_eq!(report[0].0, "slow");
253 }
254
255 #[test]
256 fn scoped_span_ends_on_drop() {
257 let mut p = FrameProfiler::new(4);
258 {
259 let _s = ScopedSpan::new(&mut p, "scoped");
260 thread::sleep(Duration::from_millis(2));
261 }
262 assert!(p.last_us("scoped") >= 1000, "scoped span should have measured time");
263 }
264}