1#[allow(dead_code)]
7pub struct ProfileSpan {
8 pub name: String,
9 pub start_ns: u64,
10 pub end_ns: u64,
11 pub depth: u32,
12}
13
14#[allow(dead_code)]
15pub struct ProfileFrame {
16 pub spans: Vec<ProfileSpan>,
17 pub frame_number: u64,
18 pub total_ns: u64,
19}
20
21#[allow(dead_code)]
22pub struct Profiler {
23 pub frames: Vec<ProfileFrame>,
24 pub current_frame: ProfileFrame,
25 pub stack: Vec<String>,
26 pub max_frames: usize,
27 pub enabled: bool,
28 pub frame_counter: u64,
29 pub simulated_ns: u64,
30}
31
32#[allow(dead_code)]
33pub fn new_profiler(max_frames: usize) -> Profiler {
34 Profiler {
35 frames: Vec::new(),
36 current_frame: ProfileFrame {
37 spans: Vec::new(),
38 frame_number: 0,
39 total_ns: 0,
40 },
41 stack: Vec::new(),
42 max_frames,
43 enabled: true,
44 frame_counter: 0,
45 simulated_ns: 0,
46 }
47}
48
49#[allow(dead_code)]
50pub fn begin_span(profiler: &mut Profiler, name: &str) {
51 if !profiler.enabled {
52 return;
53 }
54 profiler.stack.push(name.to_string());
55 profiler.simulated_ns += 1;
57 profiler.current_frame.spans.push(ProfileSpan {
60 name: name.to_string(),
61 start_ns: profiler.simulated_ns,
62 end_ns: 0,
63 depth: (profiler.stack.len() as u32).saturating_sub(1),
64 });
65}
66
67#[allow(dead_code)]
68pub fn end_span(profiler: &mut Profiler) {
69 if !profiler.enabled {
70 return;
71 }
72 if profiler.stack.is_empty() {
73 return;
74 }
75 let Some(name) = profiler.stack.pop() else {
76 return;
77 };
78 profiler.simulated_ns += 1;
79 let end_ns = profiler.simulated_ns;
80 if let Some(span) = profiler
82 .current_frame
83 .spans
84 .iter_mut()
85 .rev()
86 .find(|s| s.name == name && s.end_ns == 0)
87 {
88 span.end_ns = end_ns;
89 }
90}
91
92#[allow(dead_code)]
93pub fn end_frame(profiler: &mut Profiler) {
94 if !profiler.enabled {
95 return;
96 }
97 let frame_number = profiler.frame_counter;
98 let total_ns = profiler
99 .current_frame
100 .spans
101 .iter()
102 .filter(|s| s.depth == 0)
103 .map(span_duration_ns)
104 .sum();
105 let frame = ProfileFrame {
106 spans: std::mem::take(&mut profiler.current_frame.spans),
107 frame_number,
108 total_ns,
109 };
110 profiler.frames.push(frame);
111 if profiler.frames.len() > profiler.max_frames {
112 profiler.frames.remove(0);
113 }
114 profiler.frame_counter += 1;
115 profiler.current_frame = ProfileFrame {
116 spans: Vec::new(),
117 frame_number: profiler.frame_counter,
118 total_ns: 0,
119 };
120 profiler.stack.clear();
121}
122
123#[allow(dead_code)]
124pub fn frame_count_profiler(profiler: &Profiler) -> usize {
125 profiler.frames.len()
126}
127
128#[allow(dead_code)]
129pub fn last_frame(profiler: &Profiler) -> Option<&ProfileFrame> {
130 profiler.frames.last()
131}
132
133#[allow(dead_code)]
134pub fn span_by_name<'a>(frame: &'a ProfileFrame, name: &str) -> Option<&'a ProfileSpan> {
135 frame.spans.iter().find(|s| s.name == name)
136}
137
138#[allow(dead_code)]
139pub fn total_frame_ns(frame: &ProfileFrame) -> u64 {
140 frame.total_ns
141}
142
143#[allow(dead_code)]
144pub fn average_frame_ns(profiler: &Profiler) -> u64 {
145 if profiler.frames.is_empty() {
146 return 0;
147 }
148 let total: u64 = profiler.frames.iter().map(|f| f.total_ns).sum();
149 total / profiler.frames.len() as u64
150}
151
152#[allow(dead_code)]
153pub fn span_duration_ns(span: &ProfileSpan) -> u64 {
154 span.end_ns.saturating_sub(span.start_ns)
155}
156
157#[allow(dead_code)]
158pub fn hottest_span(frame: &ProfileFrame) -> Option<&ProfileSpan> {
159 frame.spans.iter().max_by_key(|s| span_duration_ns(s))
160}
161
162#[allow(dead_code)]
163pub fn profiler_to_json(profiler: &Profiler) -> String {
164 let mut out = String::from("{\"frames\":[");
165 for (fi, frame) in profiler.frames.iter().enumerate() {
166 if fi > 0 {
167 out.push(',');
168 }
169 out.push_str(&format!(
170 "{{\"frame_number\":{},\"total_ns\":{},\"spans\":[",
171 frame.frame_number, frame.total_ns
172 ));
173 for (si, span) in frame.spans.iter().enumerate() {
174 if si > 0 {
175 out.push(',');
176 }
177 out.push_str(&format!(
178 "{{\"name\":\"{}\",\"start_ns\":{},\"end_ns\":{},\"depth\":{}}}",
179 span.name, span.start_ns, span.end_ns, span.depth
180 ));
181 }
182 out.push_str("]}");
183 }
184 out.push_str("]}");
185 out
186}
187
188#[allow(dead_code)]
189pub fn enable_profiler(profiler: &mut Profiler) {
190 profiler.enabled = true;
191}
192
193#[allow(dead_code)]
194pub fn disable_profiler(profiler: &mut Profiler) {
195 profiler.enabled = false;
196}
197
198#[allow(dead_code)]
199pub fn clear_profiler(profiler: &mut Profiler) {
200 profiler.frames.clear();
201 profiler.current_frame.spans.clear();
202 profiler.stack.clear();
203 profiler.frame_counter = 0;
204 profiler.simulated_ns = 0;
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 fn make_frame_with_span(name: &str, start: u64, end: u64) -> ProfileFrame {
212 ProfileFrame {
213 spans: vec![ProfileSpan {
214 name: name.to_string(),
215 start_ns: start,
216 end_ns: end,
217 depth: 0,
218 }],
219 frame_number: 0,
220 total_ns: end - start,
221 }
222 }
223
224 #[test]
225 fn test_new_profiler() {
226 let p = new_profiler(10);
227 assert_eq!(p.max_frames, 10);
228 assert!(p.enabled);
229 assert!(p.frames.is_empty());
230 assert_eq!(p.frame_counter, 0);
231 }
232
233 #[test]
234 fn test_begin_end_span() {
235 let mut p = new_profiler(10);
236 begin_span(&mut p, "render");
237 end_span(&mut p);
238 assert!(!p.current_frame.spans.is_empty());
239 let span = &p.current_frame.spans[0];
240 assert_eq!(span.name, "render");
241 assert!(span.end_ns > span.start_ns);
242 }
243
244 #[test]
245 fn test_end_frame() {
246 let mut p = new_profiler(10);
247 begin_span(&mut p, "update");
248 end_span(&mut p);
249 end_frame(&mut p);
250 assert_eq!(frame_count_profiler(&p), 1);
251 assert_eq!(p.frame_counter, 1);
252 }
253
254 #[test]
255 fn test_frame_count() {
256 let mut p = new_profiler(10);
257 for _ in 0..3 {
258 begin_span(&mut p, "x");
259 end_span(&mut p);
260 end_frame(&mut p);
261 }
262 assert_eq!(frame_count_profiler(&p), 3);
263 }
264
265 #[test]
266 fn test_max_frames_limit() {
267 let mut p = new_profiler(2);
268 for _ in 0..5 {
269 begin_span(&mut p, "x");
270 end_span(&mut p);
271 end_frame(&mut p);
272 }
273 assert!(frame_count_profiler(&p) <= 2);
274 }
275
276 #[test]
277 fn test_last_frame() {
278 let mut p = new_profiler(10);
279 assert!(last_frame(&p).is_none());
280 begin_span(&mut p, "a");
281 end_span(&mut p);
282 end_frame(&mut p);
283 assert!(last_frame(&p).is_some());
284 }
285
286 #[test]
287 fn test_span_by_name() {
288 let frame = make_frame_with_span("physics", 100, 200);
289 let span = span_by_name(&frame, "physics");
290 assert!(span.is_some());
291 assert_eq!(span.expect("should succeed").name, "physics");
292 assert!(span_by_name(&frame, "missing").is_none());
293 }
294
295 #[test]
296 fn test_span_duration_ns() {
297 let span = ProfileSpan {
298 name: "x".to_string(),
299 start_ns: 100,
300 end_ns: 250,
301 depth: 0,
302 };
303 assert_eq!(span_duration_ns(&span), 150);
304 }
305
306 #[test]
307 fn test_hottest_span() {
308 let frame = ProfileFrame {
309 spans: vec![
310 ProfileSpan {
311 name: "a".to_string(),
312 start_ns: 0,
313 end_ns: 50,
314 depth: 0,
315 },
316 ProfileSpan {
317 name: "b".to_string(),
318 start_ns: 50,
319 end_ns: 200,
320 depth: 0,
321 },
322 ProfileSpan {
323 name: "c".to_string(),
324 start_ns: 200,
325 end_ns: 210,
326 depth: 0,
327 },
328 ],
329 frame_number: 0,
330 total_ns: 210,
331 };
332 let hot = hottest_span(&frame).expect("should succeed");
333 assert_eq!(hot.name, "b");
334 }
335
336 #[test]
337 fn test_average_frame_ns() {
338 let mut p = new_profiler(10);
339 assert_eq!(average_frame_ns(&p), 0);
340 p.frames.push(ProfileFrame {
342 spans: vec![],
343 frame_number: 0,
344 total_ns: 100,
345 });
346 p.frames.push(ProfileFrame {
347 spans: vec![],
348 frame_number: 1,
349 total_ns: 200,
350 });
351 assert_eq!(average_frame_ns(&p), 150);
352 }
353
354 #[test]
355 fn test_enable_disable() {
356 let mut p = new_profiler(10);
357 disable_profiler(&mut p);
358 assert!(!p.enabled);
359 begin_span(&mut p, "x");
360 end_span(&mut p);
361 end_frame(&mut p);
362 assert_eq!(frame_count_profiler(&p), 0);
363 enable_profiler(&mut p);
364 assert!(p.enabled);
365 }
366
367 #[test]
368 fn test_clear_profiler() {
369 let mut p = new_profiler(10);
370 begin_span(&mut p, "a");
371 end_span(&mut p);
372 end_frame(&mut p);
373 clear_profiler(&mut p);
374 assert_eq!(frame_count_profiler(&p), 0);
375 assert_eq!(p.frame_counter, 0);
376 }
377
378 #[test]
379 fn test_profiler_to_json() {
380 let mut p = new_profiler(10);
381 begin_span(&mut p, "render");
382 end_span(&mut p);
383 end_frame(&mut p);
384 let json = profiler_to_json(&p);
385 assert!(json.contains("render"));
386 assert!(json.contains("frames"));
387 }
388
389 #[test]
390 fn test_end_span_without_begin() {
391 let mut p = new_profiler(10);
392 end_span(&mut p);
394 assert!(p.current_frame.spans.is_empty());
395 }
396
397 #[test]
398 fn test_nested_spans() {
399 let mut p = new_profiler(10);
400 begin_span(&mut p, "outer");
401 begin_span(&mut p, "inner");
402 end_span(&mut p);
403 end_span(&mut p);
404 end_frame(&mut p);
405 let frame = last_frame(&p).expect("should succeed");
406 let inner = span_by_name(frame, "inner").expect("should succeed");
407 assert_eq!(inner.depth, 1);
408 }
409}