Skip to main content

ranim_render/
upload_probe.rs

1//! Lightweight upload instrumentation for the `queue.write_buffer` paths,
2//! recording bytes / calls / CPU time per buffer label. Enabled via
3//! `RANIM_PROFILE_UPLOAD=1` (or `set_mode` at runtime, e.g. from the
4//! preview profiler panel); a no-op (one atomic read per upload) unless
5//! enabled.
6//!
7//! This is the stable counting core. Experimental upload strategies
8//! (skip-identical / dirty-range uploads) live on a separate branch and
9//! extend this module.
10
11use std::{
12    collections::BTreeMap,
13    sync::{
14        Mutex, OnceLock,
15        atomic::{AtomicU8, AtomicU64, Ordering},
16    },
17    time::Duration,
18};
19
20/// Upload instrumentation mode, seeded from `RANIM_PROFILE_UPLOAD` and
21/// runtime-switchable via [`set_mode`].
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum UploadMode {
24    #[default]
25    Off,
26    /// Record stats only.
27    Count,
28}
29
30impl UploadMode {
31    fn from_env() -> Self {
32        match std::env::var("RANIM_PROFILE_UPLOAD").as_deref() {
33            Ok(v) if v == "1" || v == "count" || v == "true" => UploadMode::Count,
34            _ => UploadMode::Off,
35        }
36    }
37
38    fn as_u8(self) -> u8 {
39        match self {
40            UploadMode::Off => 0,
41            UploadMode::Count => 1,
42        }
43    }
44
45    fn from_u8(v: u8) -> Self {
46        match v {
47            1 => UploadMode::Count,
48            _ => UploadMode::Off,
49        }
50    }
51
52    pub fn enabled(self) -> bool {
53        self != UploadMode::Off
54    }
55}
56
57/// `u8::MAX` marks "not seeded from the environment yet".
58static MODE: AtomicU8 = AtomicU8::new(u8::MAX);
59static MODE_SEEDED: OnceLock<()> = OnceLock::new();
60
61/// The process-wide upload mode (seeded from `RANIM_PROFILE_UPLOAD` on
62/// first use, overridable at runtime via [`set_mode`]).
63pub fn mode() -> UploadMode {
64    if MODE_SEEDED.get().is_none() {
65        let _ = MODE_SEEDED.set(());
66        MODE.store(UploadMode::from_env().as_u8(), Ordering::Relaxed);
67    }
68    UploadMode::from_u8(MODE.load(Ordering::Relaxed))
69}
70
71/// Switch the process-wide upload mode at runtime (takes effect for
72/// uploads after this call).
73pub fn set_mode(new_mode: UploadMode) {
74    mode(); // resolve the unseeded sentinel first
75    MODE.store(new_mode.as_u8(), Ordering::Relaxed);
76}
77
78#[derive(Default)]
79struct Counters {
80    /// Number of `set` calls (upload attempts).
81    calls: AtomicU64,
82    /// Logical bytes the caller wanted uploaded (the full payload size).
83    bytes: AtomicU64,
84    /// Bytes actually handed to `queue.write_buffer`.
85    written_bytes: AtomicU64,
86    /// CPU time spent inside the write calls.
87    cpu_ns: AtomicU64,
88}
89
90fn registry() -> &'static Mutex<BTreeMap<&'static str, Counters>> {
91    static REG: OnceLock<Mutex<BTreeMap<&'static str, Counters>>> = OnceLock::new();
92    REG.get_or_init(|| Mutex::new(BTreeMap::new()))
93}
94
95fn label_or(label: Option<&'static str>) -> &'static str {
96    label.unwrap_or("<unnamed>")
97}
98
99/// Record one upload for `label`.
100pub(crate) fn record(label: Option<&'static str>, bytes: u64, written_bytes: u64, cpu_ns: u64) {
101    let mut reg = registry().lock().unwrap();
102    let c = reg.entry(label_or(label)).or_default();
103    c.calls.fetch_add(1, Ordering::Relaxed);
104    c.bytes.fetch_add(bytes, Ordering::Relaxed);
105    c.written_bytes.fetch_add(written_bytes, Ordering::Relaxed);
106    c.cpu_ns.fetch_add(cpu_ns, Ordering::Relaxed);
107}
108
109/// Snapshot of the counters accumulated since the last [`take_stats`].
110#[derive(Debug, Clone, Copy)]
111pub struct UploadStats {
112    pub calls: u64,
113    pub bytes: u64,
114    pub written_bytes: u64,
115    pub cpu_time: Duration,
116}
117
118/// Take and reset the accumulated stats, per buffer label.
119pub fn take_stats() -> BTreeMap<&'static str, UploadStats> {
120    let mut reg = registry().lock().unwrap();
121    reg.iter_mut()
122        .map(|(&label, c)| {
123            (
124                label,
125                UploadStats {
126                    calls: c.calls.swap(0, Ordering::Relaxed),
127                    bytes: c.bytes.swap(0, Ordering::Relaxed),
128                    written_bytes: c.written_bytes.swap(0, Ordering::Relaxed),
129                    cpu_time: Duration::from_nanos(c.cpu_ns.swap(0, Ordering::Relaxed)),
130                },
131            )
132        })
133        .collect()
134}