Skip to main content

oxihuman_core/
profiler.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Simple performance profiler with named sections.
5
6#[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    // We store the start time via simulated_ns for testing; increment it slightly
56    profiler.simulated_ns += 1;
57    // We will record start_ns when we actually create the span in end_span.
58    // For tracking start, we push a marker into current_frame with the start time.
59    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    // Find last unclosed span with this name
81    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        // Manually push frames for deterministic testing
341        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        // Should not panic
393        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}