use std::time::Instant;
#[derive(Debug, Clone, Default)]
pub struct FrameMetrics {
pub frame_count: usize,
pub avg_interval_ms: f64,
pub p95_interval_ms: f64,
pub worst_interval_ms: f64,
pub jank_count: usize,
}
pub struct MetricsRecorder {
jank_threshold_ms: f64,
timestamps: Vec<Instant>,
}
impl MetricsRecorder {
pub fn new(jank_threshold_ms: f64) -> Self {
Self {
jank_threshold_ms,
timestamps: Vec::new(),
}
}
pub fn record_frame(&mut self) {
self.timestamps.push(Instant::now());
}
pub fn compute(&self) -> FrameMetrics {
let n = self.timestamps.len();
if n < 2 {
return FrameMetrics::default();
}
let mut intervals: Vec<f64> = self
.timestamps
.windows(2)
.map(|pair| {
let delta = pair[1].duration_since(pair[0]);
delta.as_secs_f64() * 1_000.0
})
.collect();
let count = intervals.len();
let sum: f64 = intervals.iter().sum();
let avg_interval_ms = sum / count as f64;
let worst_interval_ms = intervals.iter().copied().fold(f64::NEG_INFINITY, f64::max);
intervals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let p95_index = (0.95 * count as f64).floor() as usize;
let p95_interval_ms = intervals[p95_index.min(count - 1)];
let jank_threshold_ms = self.jank_threshold_ms;
let jank_count = intervals
.iter()
.filter(|&&iv| iv > jank_threshold_ms)
.count();
FrameMetrics {
frame_count: n,
avg_interval_ms,
p95_interval_ms,
worst_interval_ms,
jank_count,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn zero_frames_returns_default() {
let recorder = MetricsRecorder::new(25.0);
let m = recorder.compute();
assert_eq!(m.frame_count, 0);
assert_eq!(m.avg_interval_ms, 0.0);
assert_eq!(m.jank_count, 0);
}
#[test]
fn one_frame_returns_default() {
let mut recorder = MetricsRecorder::new(25.0);
recorder.record_frame();
let m = recorder.compute();
assert_eq!(m.frame_count, 0); assert_eq!(m.avg_interval_ms, 0.0);
}
#[test]
fn multiple_frames_returns_valid_stats() {
let mut recorder = MetricsRecorder::new(25.0);
for _ in 0..5 {
recorder.record_frame();
std::thread::sleep(Duration::from_millis(5));
}
let m = recorder.compute();
assert_eq!(m.frame_count, 5);
assert!(m.avg_interval_ms > 0.0, "avg should be > 0");
assert!(m.worst_interval_ms >= m.avg_interval_ms - 1.0);
assert!(m.p95_interval_ms > 0.0);
}
#[test]
fn jank_count_correctly_counts_above_threshold() {
let mut recorder = MetricsRecorder::new(8.0);
let base = Instant::now();
recorder.timestamps.push(base);
recorder.timestamps.push(base + Duration::from_millis(5));
recorder.timestamps.push(base + Duration::from_millis(10));
recorder.timestamps.push(base + Duration::from_millis(20)); recorder.timestamps.push(base + Duration::from_millis(25));
let m = recorder.compute();
assert_eq!(m.frame_count, 5);
assert_eq!(m.jank_count, 1, "expected exactly 1 jank interval");
assert!(
(m.worst_interval_ms - 10.0).abs() < 1.0,
"worst={}",
m.worst_interval_ms
);
assert!(
(m.avg_interval_ms - 6.25).abs() < 0.5,
"avg={}",
m.avg_interval_ms
);
}
#[test]
fn jank_count_zero_when_all_intervals_below_threshold() {
let mut recorder = MetricsRecorder::new(100.0);
let base = Instant::now();
recorder.timestamps.push(base);
recorder.timestamps.push(base + Duration::from_millis(10));
recorder.timestamps.push(base + Duration::from_millis(20));
let m = recorder.compute();
assert_eq!(m.jank_count, 0);
}
}