use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering::Relaxed};
use std::sync::{Mutex, OnceLock, PoisonError};
use web_time::Instant;
pub(crate) static ACTIVE: AtomicUsize = AtomicUsize::new(0);
#[inline]
pub(crate) fn active() -> bool {
ACTIVE.load(Relaxed) > 0
}
pub(crate) fn enable() {
ACTIVE.fetch_add(1, Relaxed);
}
pub(crate) fn disable() {
let _ = ACTIVE.try_update(Relaxed, Relaxed, |n| n.checked_sub(1));
}
fn now_ns() -> u64 {
static START: OnceLock<Instant> = OnceLock::new();
(START.get_or_init(Instant::now).elapsed().as_nanos() as u64).max(1)
}
static REGISTRY: Mutex<Vec<&'static ProfileScope>> = Mutex::new(Vec::new());
static INTERNED: Mutex<Vec<&'static ProfileScope>> = Mutex::new(Vec::new());
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
m.lock().unwrap_or_else(PoisonError::into_inner)
}
#[derive(Debug)]
pub struct ProfileScope {
name: &'static str,
ns: AtomicU64,
calls: AtomicU64,
registered: AtomicBool,
}
impl ProfileScope {
pub const fn new(name: &'static str) -> Self {
Self {
name,
ns: AtomicU64::new(0),
calls: AtomicU64::new(0),
registered: AtomicBool::new(false),
}
}
pub fn name(&self) -> &'static str {
self.name
}
#[inline]
pub fn enter(&'static self) -> ScopeGuard {
ScopeGuard {
scope: self,
token: self.begin(),
}
}
#[inline]
pub fn begin(&'static self) -> u64 {
if ACTIVE.load(Relaxed) == 0 {
return 0;
}
if !self.registered.load(Relaxed) && !self.registered.swap(true, Relaxed) {
lock(®ISTRY).push(self);
}
now_ns()
}
#[inline]
pub fn end(&self, token: u64) {
if token == 0 {
return;
}
let elapsed = now_ns().saturating_sub(token);
self.ns.fetch_add(elapsed, Relaxed);
self.calls.fetch_add(1, Relaxed);
}
fn totals(&self) -> (u64, u64) {
(self.ns.load(Relaxed), self.calls.load(Relaxed))
}
}
#[must_use = "the scope times until this is dropped; bind it to a variable"]
pub struct ScopeGuard {
scope: &'static ProfileScope,
token: u64,
}
impl Drop for ScopeGuard {
#[inline]
fn drop(&mut self) {
self.scope.end(self.token);
}
}
pub fn scope_named(name: &str) -> &'static ProfileScope {
let mut interned = lock(&INTERNED);
if let Some(found) = interned.iter().find(|s| s.name == name) {
return found;
}
let scope: &'static ProfileScope = Box::leak(Box::new(ProfileScope::new(Box::leak(name.into()))));
interned.push(scope);
scope
}
#[macro_export]
macro_rules! profile_scope {
($name:expr) => {
let _crabby_scope_guard = {
static SCOPE: $crate::ProfileScope = $crate::ProfileScope::new($name);
SCOPE.enter()
};
};
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ScopeTiming {
pub name: &'static str,
pub last_ms: f32,
pub avg_ms: f32,
pub calls: u32,
}
#[derive(Default)]
pub(crate) struct ScopeState {
seen: Vec<(u64, u64)>,
pub timings: Vec<ScopeTiming>,
}
impl ScopeState {
pub fn rebase(&mut self) {
self.seen.clear();
read_all(|_, scope| self.seen.push(scope.totals()));
}
pub fn end_frame(&mut self) {
for t in &mut self.timings {
t.last_ms = 0.0;
t.calls = 0;
}
let (seen, timings) = (&mut self.seen, &mut self.timings);
read_all(|i, scope| {
if seen.len() <= i {
seen.push((0, 0));
}
let (ns, calls) = scope.totals();
let (was_ns, was_calls) = std::mem::replace(&mut seen[i], (ns, calls));
let (d_ns, d_calls) = (ns.saturating_sub(was_ns), calls.saturating_sub(was_calls));
if d_calls == 0 {
return;
}
let slot = match timings.iter().position(|t| t.name == scope.name) {
Some(slot) => slot,
None => {
timings.push(ScopeTiming {
name: scope.name,
last_ms: 0.0,
avg_ms: 0.0,
calls: 0,
});
timings.len() - 1
}
};
timings[slot].last_ms += d_ns as f32 / 1e6;
timings[slot].calls += d_calls as u32;
});
for t in timings {
t.avg_ms += (t.last_ms - t.avg_ms) * 0.05;
}
}
}
fn read_all(mut f: impl FnMut(usize, &'static ProfileScope)) {
for (i, scope) in lock(®ISTRY).iter().enumerate() {
f(i, scope);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scopes_add_up_their_time_and_calls() {
static SCOPE: ProfileScope = ProfileScope::new("scope_test/add_up");
enable();
let mut state = ScopeState::default();
state.rebase();
for _ in 0..3 {
let _g = SCOPE.enter();
std::thread::sleep(std::time::Duration::from_millis(2));
}
state.end_frame();
disable();
let t = state.timings.iter().find(|t| t.name == "scope_test/add_up").unwrap();
assert_eq!(t.calls, 3);
assert!(t.last_ms >= 6.0, "{t:?}");
state.end_frame();
let t = state.timings.iter().find(|t| t.name == "scope_test/add_up").unwrap();
assert_eq!((t.calls, t.last_ms), (0, 0.0), "a quiet frame resets last_ms");
assert!(t.avg_ms > 0.0);
}
#[test]
fn names_map_to_one_scope() {
let a = scope_named("scope_test/named");
let b = scope_named(&String::from("scope_test/named"));
assert!(std::ptr::eq(a, b));
assert_eq!(a.name(), "scope_test/named");
}
#[test]
fn scopes_work_across_threads() {
static SCOPE: ProfileScope = ProfileScope::new("scope_test/threads");
enable();
let mut state = ScopeState::default();
state.rebase();
std::thread::scope(|s| {
for _ in 0..4 {
s.spawn(|| {
let _g = SCOPE.enter();
});
}
});
state.end_frame();
disable();
let t = state.timings.iter().find(|t| t.name == "scope_test/threads").unwrap();
assert_eq!(t.calls, 4);
}
}