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;
13use web_time::Instant;
14
15/// Upper bound on buffered spans; exceeding it clears the buffer (the
16/// consumer is expected to drain every frame).
17const MAX_SPANS: usize = 256;
18
19thread_local! {
20 static SPANS: RefCell<Vec<(&'static str, f64)>> = const { RefCell::new(Vec::new()) };
21}
22
23/// A scoped CPU timing span. Records `(label, ms)` into the thread-local
24/// frame buffer when dropped.
25#[must_use]
26pub struct Span {
27 label: &'static str,
28 start: Instant,
29}
30
31impl Drop for Span {
32 fn drop(&mut self) {
33 let ms = self.start.elapsed().as_secs_f64() * 1e3;
34 SPANS.with(|spans| {
35 let mut spans = spans.borrow_mut();
36 if spans.len() >= MAX_SPANS {
37 spans.clear();
38 }
39 spans.push((self.label, ms));
40 });
41 }
42}
43
44/// Start a named leaf-level CPU span (records on scope exit):
45///
46/// ```ignore
47/// let _span = cpu_probe::span("my_stage");
48/// do_work();
49/// ```
50pub fn span(label: &'static str) -> Span {
51 Span {
52 label,
53 start: Instant::now(),
54 }
55}
56
57/// Drain the spans recorded on this thread since the last call.
58pub fn take_frame() -> Vec<(&'static str, f64)> {
59 SPANS.with(|spans| std::mem::take(&mut *spans.borrow_mut()))
60}