1use std::collections::VecDeque;
5use std::time::Duration;
6
7use serde::Serialize;
8
9pub const WINDOW: usize = 1024;
11
12#[derive(Debug, Default)]
14pub struct FrameStats {
15 recent: VecDeque<Duration>,
16 frames: u64,
17 first_frame: Option<Duration>,
18 max_ever: Duration,
19}
20
21#[derive(Clone, Debug, Default, PartialEq, Serialize)]
23pub struct StatsSummary {
24 pub frames: u64,
26 pub window: usize,
28 pub p50_ms: f64,
29 pub p95_ms: f64,
30 pub max_ms: f64,
31 pub max_ever_ms: f64,
33 pub first_frame_ms: Option<f64>,
35}
36
37fn ms(d: Duration) -> f64 {
38 d.as_secs_f64() * 1000.0
39}
40
41impl FrameStats {
42 pub fn record(&mut self, d: Duration) {
43 if self.recent.len() >= WINDOW {
44 self.recent.pop_front();
45 }
46 self.recent.push_back(d);
47 self.frames = self.frames.saturating_add(1);
48 self.max_ever = self.max_ever.max(d);
49 }
50
51 pub fn set_first_frame(&mut self, d: Duration) {
52 self.first_frame.get_or_insert(d);
53 }
54
55 pub fn frames(&self) -> u64 {
56 self.frames
57 }
58
59 pub fn reset_window(&mut self) {
61 self.recent.clear();
62 }
63
64 pub fn summary(&self) -> StatsSummary {
65 let mut v: Vec<Duration> = self.recent.iter().copied().collect();
66 v.sort_unstable();
67 let pick = |q: f64| -> f64 {
68 if v.is_empty() {
69 return 0.0;
70 }
71 let i = ((v.len() as f64 - 1.0) * q).round() as usize;
72 v.get(i.min(v.len() - 1)).copied().map_or(0.0, ms)
73 };
74 StatsSummary {
75 frames: self.frames,
76 window: v.len(),
77 p50_ms: pick(0.5),
78 p95_ms: pick(0.95),
79 max_ms: v.last().copied().map_or(0.0, ms),
80 max_ever_ms: ms(self.max_ever),
81 first_frame_ms: self.first_frame.map(ms),
82 }
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn percentiles() {
92 let mut s = FrameStats::default();
93 for i in 1..=100u64 {
94 s.record(Duration::from_millis(i));
95 }
96 let sum = s.summary();
97 assert_eq!(sum.frames, 100);
98 assert!((sum.p50_ms - 50.0).abs() <= 1.0);
99 assert!((sum.p95_ms - 95.0).abs() <= 1.0);
100 assert!((sum.max_ms - 100.0).abs() < 1e-9);
101 assert_eq!(FrameStats::default().summary().p95_ms, 0.0);
102 }
103
104 #[test]
105 fn the_window_is_bounded() {
106 let mut s = FrameStats::default();
107 for _ in 0..(WINDOW * 3) {
108 s.record(Duration::from_micros(10));
109 }
110 assert_eq!(s.summary().window, WINDOW);
111 assert_eq!(s.frames() as usize, WINDOW * 3);
112 }
113}