1use std::collections::VecDeque;
18use std::time::Instant;
19
20use gpui::{IntoElement, ParentElement, Styled, div, rgb};
21
22const WINDOW: usize = 120;
25
26#[derive(Debug, Clone, Default)]
28pub struct FrameStats {
29 frames: VecDeque<Instant>,
30}
31
32impl FrameStats {
33 pub fn new() -> Self {
35 Self::default()
36 }
37
38 pub fn record(&mut self) {
40 self.record_at(Instant::now());
41 }
42
43 pub fn record_at(&mut self, now: Instant) {
48 if self.frames.back().is_some_and(|&last| now < last) {
51 return;
52 }
53 if self.frames.len() >= WINDOW {
54 self.frames.pop_front();
55 }
56 self.frames.push_back(now);
57 }
58
59 pub fn samples(&self) -> usize {
61 self.frames.len()
62 }
63
64 pub fn fps(&self) -> Option<f32> {
66 let n = self.frames.len();
67 if n < 2 {
68 return None;
69 }
70 let span = self.span_secs()?;
71 if span <= 0.0 {
72 return None;
73 }
74 Some((n - 1) as f32 / span)
75 }
76
77 pub fn avg_ms(&self) -> Option<f32> {
79 let n = self.frames.len();
80 if n < 2 {
81 return None;
82 }
83 let span = self.span_secs()?;
84 if span <= 0.0 {
85 return None;
86 }
87 Some(span * 1000.0 / (n - 1) as f32)
88 }
89
90 pub fn worst_ms(&self) -> Option<f32> {
95 if self.frames.len() < 2 {
96 return None;
97 }
98 self.frames
99 .iter()
100 .zip(self.frames.iter().skip(1))
101 .map(|(a, b)| (*b - *a).as_secs_f32() * 1000.0)
102 .reduce(f32::max)
103 }
104
105 pub fn clear(&mut self) {
107 self.frames.clear();
108 }
109
110 pub fn text(&self) -> String {
113 match (self.fps(), self.avg_ms(), self.worst_ms()) {
114 (Some(fps), Some(avg), Some(worst)) => {
115 format!("{fps:.0} fps · {avg:.1} ms avg · {worst:.1} ms worst")
116 }
117 _ => "fps: …".to_string(),
118 }
119 }
120
121 fn span_secs(&self) -> Option<f32> {
122 let (first, last) = (self.frames.front()?, self.frames.back()?);
123 Some((*last - *first).as_secs_f32())
124 }
125}
126
127pub fn fps_badge(stats: &FrameStats) -> impl IntoElement {
133 let color = match stats.fps() {
134 Some(fps) if fps >= 50.0 => rgb(0xa6e3a1),
135 Some(fps) if fps >= 25.0 => rgb(0xf9e2af),
136 Some(_) => rgb(0xf38ba8),
137 None => rgb(0x6c7086),
138 };
139 div()
140 .text_xs()
141 .px_2()
142 .py_0p5()
143 .rounded_md()
144 .bg(rgb(0x313244))
145 .text_color(color)
146 .child(stats.text())
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use std::time::Duration;
153
154 fn stats_at(ms: &[u64]) -> FrameStats {
155 let mut s = FrameStats::new();
156 let t0 = Instant::now();
157 for &m in ms {
158 s.record_at(t0 + Duration::from_millis(m));
159 }
160 s
161 }
162
163 #[test]
164 fn empty_and_singleton_yield_no_reading() {
165 let empty = FrameStats::new();
166 assert_eq!(empty.samples(), 0);
167 assert_eq!(empty.fps(), None);
168 assert_eq!(empty.avg_ms(), None);
169 assert_eq!(empty.worst_ms(), None);
170 assert_eq!(empty.text(), "fps: …");
171
172 let mut one = FrameStats::new();
173 one.record();
174 assert_eq!(one.samples(), 1);
175 assert_eq!(one.fps(), None);
176 }
177
178 #[test]
179 fn steady_60fps_reports_60fps() {
180 let s = stats_at(&[0, 16, 32, 48]);
182 assert!((s.fps().unwrap() - 62.5).abs() < 0.01);
183 assert!((s.avg_ms().unwrap() - 16.0).abs() < 0.01);
184 assert!((s.worst_ms().unwrap() - 16.0).abs() < 0.01);
185 assert_eq!(s.text(), "62 fps · 16.0 ms avg · 16.0 ms worst");
186 }
187
188 #[test]
189 fn worst_frame_tracks_the_spike() {
190 let s = stats_at(&[0, 16, 32, 132]);
192 assert!((s.worst_ms().unwrap() - 100.0).abs() < 0.01);
193 assert!((s.avg_ms().unwrap() - 44.0).abs() < 0.01);
195 }
196
197 #[test]
198 fn window_is_bounded() {
199 let t0 = Instant::now();
200 let mut s = FrameStats::new();
201 for i in 0..(WINDOW + 50) {
202 s.record_at(t0 + Duration::from_millis(i as u64));
203 }
204 assert_eq!(s.samples(), WINDOW);
205 assert!(s.fps().unwrap() > 900.0);
206 }
207
208 #[test]
209 fn clock_regression_is_dropped() {
210 let t0 = Instant::now();
211 let mut s = FrameStats::new();
212 s.record_at(t0);
213 s.record_at(t0 + Duration::from_millis(16));
214 s.record_at(t0); assert_eq!(s.samples(), 2);
216 }
217
218 #[test]
219 fn clear_resets_to_no_reading() {
220 let mut s = stats_at(&[0, 16, 32]);
221 assert!(s.fps().is_some());
222 s.clear();
223 assert_eq!(s.samples(), 0);
224 assert_eq!(s.fps(), None);
225 }
226}