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}