pub struct DeltaTimeMs(pub u16);
#[derive(Clone, Copy, Debug, Default)]
pub struct FrameTimings {
pub frame_nanos: u64,
pub input_nanos: u64,
pub systems_nanos: u64,
pub layout_nanos: u64,
pub render_nanos: u64,
pub flush_nanos: u64,
pub seed_prev_nanos: u64,
}
pub struct FrameStats {
samples: [u64; FRAME_STATS_CAP],
head: usize,
len: usize,
}
const FRAME_STATS_CAP: usize = 256;
impl Default for FrameStats {
fn default() -> Self {
Self {
samples: [0; FRAME_STATS_CAP],
head: 0,
len: 0,
}
}
}
impl FrameStats {
pub fn push(&mut self, frame_nanos: u64) {
self.samples[self.head] = frame_nanos;
self.head = (self.head + 1) % FRAME_STATS_CAP;
if self.len < FRAME_STATS_CAP {
self.len += 1;
}
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
fn iter(&self) -> impl Iterator<Item = u64> + '_ {
let len = self.len;
let cap = FRAME_STATS_CAP;
let start = if len < cap { 0 } else { self.head };
(0..len).map(move |i| self.samples[(start + i) % cap])
}
pub fn avg(&self) -> u64 {
if self.len == 0 {
return 0;
}
let sum: u64 = self.iter().sum();
sum / self.len as u64
}
pub fn min(&self) -> u64 {
self.iter().min().unwrap_or(0)
}
pub fn max(&self) -> u64 {
self.iter().max().unwrap_or(0)
}
pub fn p99(&self) -> u64 {
if self.len == 0 {
return 0;
}
let mut sorted: alloc::vec::Vec<u64> = self.iter().collect();
sorted.sort_unstable();
let idx = ((self.len * 99).div_ceil(100) - 1).min(self.len - 1);
sorted[idx]
}
pub fn jitter(&self) -> u64 {
self.max().saturating_sub(self.min())
}
}
pub struct MonoClock {
pub clock: fn() -> u64,
pub last_ns: u64,
}
impl MonoClock {
pub fn new(clock: fn() -> u64) -> Self {
let now = clock();
Self {
clock,
last_ns: now,
}
}
pub fn now_ns(&self) -> u64 {
(self.clock)()
}
pub fn now_ms(&self) -> u32 {
(self.now_ns() / 1_000_000) as u32
}
}
#[cfg(any(test, feature = "std"))]
pub mod mock {
extern crate std;
use std::sync::Mutex;
static MOCK_NS: Mutex<u64> = Mutex::new(0);
pub fn clock_fn() -> u64 {
*MOCK_NS.lock().expect("mock clock poisoned")
}
pub fn set_ns(ns: u64) {
*MOCK_NS.lock().expect("mock clock poisoned") = ns;
}
pub fn set_ms(ms: u64) {
set_ns(ms.saturating_mul(1_000_000));
}
pub fn advance_ms(ms: u64) {
let mut guard = MOCK_NS.lock().expect("mock clock poisoned");
*guard = guard.saturating_add(ms.saturating_mul(1_000_000));
}
pub fn lock() -> std::sync::MutexGuard<'static, ()> {
static SERIAL: Mutex<()> = Mutex::new(());
SERIAL.lock().unwrap_or_else(|p| p.into_inner())
}
}
#[cfg(test)]
mod frame_stats_tests {
use super::*;
#[test]
fn empty_window_reports_zero() {
let s = FrameStats::default();
assert!(s.is_empty());
assert_eq!(s.len(), 0);
assert_eq!(s.min(), 0);
assert_eq!(s.max(), 0);
assert_eq!(s.p99(), 0);
assert_eq!(s.jitter(), 0);
}
#[test]
fn small_window_min_max_jitter() {
let mut s = FrameStats::default();
s.push(10);
s.push(20);
s.push(15);
assert_eq!(s.len(), 3);
assert_eq!(s.min(), 10);
assert_eq!(s.max(), 20);
assert_eq!(s.jitter(), 10);
assert_eq!(s.p99(), 20);
}
#[test]
fn p99_with_two_samples_returns_max() {
let mut s = FrameStats::default();
s.push(5);
s.push(50);
assert_eq!(s.p99(), 50);
}
#[test]
fn p99_with_one_sample_returns_that_sample() {
let mut s = FrameStats::default();
s.push(42);
assert_eq!(s.p99(), 42);
}
#[test]
fn p99_full_window_picks_254th_smallest() {
let mut s = FrameStats::default();
for v in 1..=256u64 {
s.push(v);
}
assert_eq!(s.len(), 256);
assert_eq!(s.p99(), 254);
}
#[test]
fn ring_drops_oldest_after_capacity() {
let mut s = FrameStats::default();
for v in 1..=256u64 {
s.push(v);
}
s.push(999);
assert_eq!(s.len(), 256);
assert_eq!(s.min(), 2);
assert_eq!(s.max(), 999);
}
#[test]
fn avg_matches_arithmetic_mean() {
let mut s = FrameStats::default();
s.push(10);
s.push(20);
s.push(30);
assert_eq!(s.avg(), 20);
}
}