Skip to main content

ranim_render/
cpu_probe.rs

1//! Lightweight CPU timing spans, mirroring [`crate::upload_probe`] and the
2//! GPU timer scopes: leaf-level named spans recorded into a thread-local
3//! buffer, drained once per frame by the consumer (the preview profiler
4//! panel).
5//!
6//! Spans are always recorded (a span costs two `Instant::now` reads, ~50ns)
7//! and are **leaf-level only** — never nest them, so the frame total is the
8//! sum of all spans without double counting. If nobody drains the buffer
9//! (e.g. the CLI render worker thread), it self-clears at a span cap so
10//! memory stays bounded.
11
12use std::{cell::RefCell, time::Instant};
13
14/// Upper bound on buffered spans; exceeding it clears the buffer (the
15/// consumer is expected to drain every frame).
16const MAX_SPANS: usize = 256;
17
18thread_local! {
19    static SPANS: RefCell<Vec<(&'static str, f64)>> = const { RefCell::new(Vec::new()) };
20}
21
22/// A scoped CPU timing span. Records `(label, ms)` into the thread-local
23/// frame buffer when dropped.
24#[must_use]
25pub struct Span {
26    label: &'static str,
27    start: Instant,
28}
29
30impl Drop for Span {
31    fn drop(&mut self) {
32        let ms = self.start.elapsed().as_secs_f64() * 1e3;
33        SPANS.with(|spans| {
34            let mut spans = spans.borrow_mut();
35            if spans.len() >= MAX_SPANS {
36                spans.clear();
37            }
38            spans.push((self.label, ms));
39        });
40    }
41}
42
43/// Start a named leaf-level CPU span (records on scope exit):
44///
45/// ```ignore
46/// let _span = cpu_probe::span("my_stage");
47/// do_work();
48/// ```
49pub fn span(label: &'static str) -> Span {
50    Span {
51        label,
52        start: Instant::now(),
53    }
54}
55
56/// Drain the spans recorded on this thread since the last call.
57pub fn take_frame() -> Vec<(&'static str, f64)> {
58    SPANS.with(|spans| std::mem::take(&mut *spans.borrow_mut()))
59}