use std::sync::{Mutex, MutexGuard};
use web_time::Instant;
const WINDOW: usize = 120;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Phase {
Ui,
Publish,
Sync,
Fit,
Overlays,
Draw,
}
const PHASES: usize = 6;
const NAMES: [&str; PHASES] = ["ui", "publish", "sync", "fit", "overlays", "draw"];
fn slot(phase: Phase) -> usize {
match phase {
Phase::Ui => 0,
Phase::Publish => 1,
Phase::Sync => 2,
Phase::Fit => 3,
Phase::Overlays => 4,
Phase::Draw => 5,
}
}
struct Ring {
samples: [f32; WINDOW],
len: usize,
next: usize,
}
impl Ring {
const fn new() -> Self {
Self { samples: [0.0; WINDOW], len: 0, next: 0 }
}
fn push(&mut self, ms: f32) {
self.samples[self.next] = ms;
self.next = (self.next + 1) % WINDOW;
self.len = (self.len + 1).min(WINDOW);
}
fn stats(&self) -> Stats {
if self.len == 0 {
return Stats::default();
}
let live = &self.samples[..self.len];
let sum: f32 = live.iter().sum();
let mut sorted: Vec<f32> = live.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let rank = ((self.len as f32 * 0.95).ceil() as usize).clamp(1, self.len) - 1;
Stats {
avg: sum / self.len as f32,
p95: sorted[rank],
max: sorted[self.len - 1],
}
}
}
#[derive(Default, Clone, Copy)]
struct Stats {
avg: f32,
p95: f32,
max: f32,
}
struct Perf {
rings: [Ring; PHASES],
dt: Ring,
pending: [f32; PHASES],
last_tick: Option<Instant>,
frames: u64,
}
static PERF: Mutex<Perf> = Mutex::new(Perf {
rings: [Ring::new(), Ring::new(), Ring::new(), Ring::new(), Ring::new(), Ring::new()],
dt: Ring::new(),
pending: [0.0; PHASES],
last_tick: None,
frames: 0,
});
fn lock() -> MutexGuard<'static, Perf> {
PERF.lock().unwrap_or_else(|p| p.into_inner())
}
pub fn tick() {
let now = Instant::now();
let mut perf = lock();
if let Some(last) = perf.last_tick {
let dt = now.duration_since(last).as_secs_f32() * 1000.0;
perf.dt.push(dt);
let pending = perf.pending;
for (index, ms) in pending.iter().enumerate() {
perf.rings[index].push(*ms);
}
perf.frames += 1;
}
perf.pending = [0.0; PHASES];
perf.last_tick = Some(now);
}
#[must_use = "the span ends when the guard drops; bind it to a name"]
pub fn span(phase: Phase) -> Span {
Span { phase, start: Instant::now() }
}
pub struct Span {
phase: Phase,
start: Instant,
}
impl Drop for Span {
fn drop(&mut self) {
let ms = self.start.elapsed().as_secs_f32() * 1000.0;
let mut perf = lock();
perf.pending[slot(self.phase)] += ms;
}
}
pub fn json() -> String {
let perf = lock();
let mut out = String::with_capacity(320);
out.push_str("{\"frames\":");
out.push_str(&perf.frames.to_string());
out.push_str(",\"window\":");
out.push_str(&perf.dt.len.to_string());
let push = |out: &mut String, name: &str, stats: Stats| {
out.push_str(",\"");
out.push_str(name);
out.push_str("\":{\"avg\":");
out.push_str(&format!("{:.3}", stats.avg));
out.push_str(",\"p95\":");
out.push_str(&format!("{:.3}", stats.p95));
out.push_str(",\"max\":");
out.push_str(&format!("{:.3}", stats.max));
out.push('}');
};
out.push_str(",\"staleFrames\":");
out.push_str(&STALE_FRAMES.load(std::sync::atomic::Ordering::Relaxed).to_string());
push(&mut out, "dt", perf.dt.stats());
for (index, name) in NAMES.iter().enumerate() {
push(&mut out, name, perf.rings[index].stats());
}
out.push('}');
out
}
static STALE_FRAMES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
pub fn note_stale_frame() {
STALE_FRAMES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
pub fn reset() {
let mut perf = lock();
for ring in perf.rings.iter_mut() {
*ring = Ring::new();
}
perf.dt = Ring::new();
perf.pending = [0.0; PHASES];
perf.frames = 0;
perf.last_tick = None;
}