telar_renderer_core/
perf.rs1use std::sync::OnceLock;
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::time::{Duration, Instant};
10
11#[derive(Clone, Copy)]
13pub enum Phase {
14 Build = 0,
16 Clone = 1,
18 Interpret = 2,
20 Gpu = 3,
22 Frame = 4,
24 Present = 5,
27}
28
29const N: usize = 6;
30const NAMES: [&str; N] = ["build", "clone", "interpret", "gpu", "frame", "present"];
31const DUMP_EVERY: u64 = 60;
33
34static ENABLED: OnceLock<bool> = OnceLock::new();
35static SUMS: [AtomicU64; N] = [const { AtomicU64::new(0) }; N];
36static COUNTS: [AtomicU64; N] = [const { AtomicU64::new(0) }; N];
37static MAXES: [AtomicU64; N] = [const { AtomicU64::new(0) }; N];
38static FRAMES: AtomicU64 = AtomicU64::new(0);
39static DAMAGE_FRAMES: AtomicU64 = AtomicU64::new(0);
42
43#[inline]
45pub fn note_damage(active: bool) {
46 if active && enabled() {
47 DAMAGE_FRAMES.fetch_add(1, Ordering::Relaxed);
48 }
49}
50
51#[inline]
52fn enabled() -> bool {
53 *ENABLED.get_or_init(
54 || matches!(std::env::var("TELAR_PERF").as_deref(), Ok(v) if !v.is_empty() && v != "0"),
55 )
56}
57
58#[inline]
60pub fn now_if_enabled() -> Option<Instant> {
61 if enabled() {
62 Some(Instant::now())
63 } else {
64 None
65 }
66}
67
68#[inline]
69fn record(phase: Phase, dur: Duration) {
70 let i = phase as usize;
71 let ns = dur.as_nanos() as u64;
72 SUMS[i].fetch_add(ns, Ordering::Relaxed);
73 COUNTS[i].fetch_add(1, Ordering::Relaxed);
74 MAXES[i].fetch_max(ns, Ordering::Relaxed);
75}
76
77#[inline]
79pub fn record_since(phase: Phase, start: Option<Instant>) {
80 if let Some(t) = start {
81 record(phase, t.elapsed());
82 }
83}
84
85pub struct Span {
87 phase: Phase,
88 start: Instant,
89}
90
91#[inline]
92pub fn span(phase: Phase) -> Option<Span> {
93 if enabled() {
94 Some(Span {
95 phase,
96 start: Instant::now(),
97 })
98 } else {
99 None
100 }
101}
102
103impl Drop for Span {
104 fn drop(&mut self) {
105 record(self.phase, self.start.elapsed());
106 }
107}
108
109pub fn tick() {
112 if !enabled() {
113 return;
114 }
115 let f = FRAMES.fetch_add(1, Ordering::Relaxed) + 1;
116 if f % DUMP_EVERY != 0 {
117 return;
118 }
119 let mut parts = String::new();
120 for i in 0..N {
121 let sum = SUMS[i].swap(0, Ordering::Relaxed);
122 let cnt = COUNTS[i].swap(0, Ordering::Relaxed);
123 let mx = MAXES[i].swap(0, Ordering::Relaxed);
124 if cnt == 0 {
125 continue;
126 }
127 let avg_us = (sum as f64 / cnt as f64) / 1000.0;
128 let max_us = mx as f64 / 1000.0;
129 parts.push_str(&format!("{}={avg_us:.0}/{max_us:.0}us(n{cnt}) ", NAMES[i]));
130 }
131 let damage = DAMAGE_FRAMES.swap(0, Ordering::Relaxed);
132 tracing::info!(target: "telar_perf", "perf[{DUMP_EVERY}f] {}damage={damage}", parts);
133}