1use std::collections::VecDeque;
10use std::sync::Mutex;
11
12use web_time::Instant;
13
14const KEPT: usize = 32;
16
17#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
19pub struct FrameMark {
20 pub time_ticks: i64,
22 pub frame_index: Option<u64>,
24}
25
26#[derive(Clone, Debug, PartialEq)]
28pub struct SubmissionReport {
29 pub gpu_ms: f64,
31 pub frame: Option<FrameMark>,
33 pub effects: Vec<String>,
35 pub predicted_ms: Option<f64>,
37}
38
39#[derive(Debug)]
40struct Entry {
41 submitted_at: Instant,
42 done_at: Option<Instant>,
43 frame: Option<FrameMark>,
44 effects: Vec<String>,
45 predicted_ms: Option<f64>,
46}
47
48#[derive(Debug, Default)]
49struct ClockState {
50 first: u64,
52 entries: VecDeque<Entry>,
53 frame: Option<FrameMark>,
54 effects: Vec<String>,
55}
56
57#[derive(Debug, Default)]
59pub(crate) struct SubmissionClock {
60 state: Mutex<ClockState>,
61}
62
63impl SubmissionClock {
64 fn state(&self) -> std::sync::MutexGuard<'_, ClockState> {
65 self.state
66 .lock()
67 .unwrap_or_else(std::sync::PoisonError::into_inner)
68 }
69
70 pub(crate) fn mark_frame(&self, frame: Option<FrameMark>) {
72 self.state().frame = frame;
73 }
74
75 pub(crate) fn note_effect(&self, effect: &crate::EffectId) {
77 let name = format!("{}@{}", effect.as_str(), effect.version());
78 let mut state = self.state();
79 if !state.effects.contains(&name) {
80 state.effects.push(name);
81 }
82 }
83
84 pub(crate) fn record(&self, at: Instant, predicted_ms: Option<f64>) -> u64 {
86 let mut state = self.state();
87 let effects = std::mem::take(&mut state.effects);
88 let frame = state.frame;
89 state.entries.push_back(Entry {
90 submitted_at: at,
91 done_at: None,
92 frame,
93 effects,
94 predicted_ms,
95 });
96 if state.entries.len() > KEPT {
97 state.entries.pop_front();
98 state.first += 1;
99 }
100 state.first + u64::try_from(state.entries.len()).unwrap_or(u64::MAX) - 1
101 }
102
103 pub(crate) fn complete(&self, sequence: u64, at: Instant) {
106 let mut state = self.state();
107 let Some(position) = sequence.checked_sub(state.first) else {
108 return;
109 };
110 let Ok(position) = usize::try_from(position) else {
111 return;
112 };
113 for entry in state.entries.iter_mut().take(position + 1) {
114 entry.done_at.get_or_insert(at);
115 }
116 }
117
118 fn spans(state: &ClockState, now: Instant) -> Vec<(f64, &Entry)> {
121 let mut spans = Vec::new();
122 let mut previous_done: Option<Instant> = None;
123 for entry in &state.entries {
124 let start =
125 previous_done.map_or(entry.submitted_at, |done| done.max(entry.submitted_at));
126 let Some(done) = entry.done_at else {
127 spans.push((milliseconds(now.saturating_duration_since(start)), entry));
128 break;
129 };
130 spans.push((milliseconds(done.saturating_duration_since(start)), entry));
131 previous_done = Some(done);
132 }
133 spans
134 }
135
136 pub(crate) fn longest_at(&self, now: Instant) -> Option<SubmissionReport> {
138 let state = self.state();
139 Self::spans(&state, now)
140 .into_iter()
141 .max_by(|(a, _), (b, _)| a.total_cmp(b))
142 .map(|(gpu_ms, entry)| SubmissionReport {
143 gpu_ms,
144 frame: entry.frame,
145 effects: entry.effects.clone(),
146 predicted_ms: entry.predicted_ms,
147 })
148 }
149
150 pub(crate) fn gpu_ms(&self, sequences: std::ops::Range<u64>) -> Option<f64> {
153 let state = self.state();
154 let first = state.first;
155 let spans = Self::spans(&state, Instant::now());
156 let mut total = 0.0;
157 let mut counted = 0;
158 for (position, (ms, entry)) in spans.into_iter().enumerate() {
159 let sequence = first + u64::try_from(position).unwrap_or(u64::MAX);
160 if sequences.contains(&sequence) {
161 entry.done_at?;
162 total += ms;
163 counted += 1;
164 }
165 }
166 (counted == sequences.end.saturating_sub(sequences.start)).then_some(total)
167 }
168
169 pub(crate) fn longest(&self) -> Option<SubmissionReport> {
171 self.longest_at(Instant::now())
172 }
173
174 pub(crate) fn hung(&self) -> Option<SubmissionReport> {
177 self.longest()
178 .filter(|report| report.gpu_ms >= f64::from(alpha2_gpu::hung_threshold_ms()))
179 }
180}
181
182fn milliseconds(duration: std::time::Duration) -> f64 {
183 duration.as_secs_f64() * 1_000.0
184}
185
186#[cfg(test)]
187mod tests {
188 use std::time::Duration;
189
190 use super::*;
191
192 #[test]
194 fn gpu_time_excludes_queueing_behind_earlier_submissions() {
195 let clock = SubmissionClock::default();
196 let start = Instant::now();
197 let at = |ms: u64| start + Duration::from_millis(ms);
198 let first = clock.record(at(0), None);
199 let second = clock.record(at(0), None);
200 let third = clock.record(at(0), None);
201 clock.complete(first, at(10));
202 clock.complete(second, at(400));
203 clock.complete(third, at(405));
204 let longest = clock.longest_at(at(1_000)).expect("submissions");
205 assert!((longest.gpu_ms - 390.0).abs() < 0.5, "{longest:?}");
206 let fourth = clock.record(at(2_000), None);
208 clock.complete(fourth, at(2_005));
209 assert!((clock.longest_at(at(3_000)).expect("kept").gpu_ms - 390.0).abs() < 0.5);
210 let _ = clock.record(at(3_000), None);
211 assert!((clock.longest_at(at(3_700)).expect("running").gpu_ms - 700.0).abs() < 0.5);
212 }
213
214 #[test]
215 fn a_completion_finishes_every_earlier_submission() {
216 let clock = SubmissionClock::default();
217 let start = Instant::now();
218 let first = clock.record(start, None);
219 let second = clock.record(start, None);
220 clock.complete(second, start + Duration::from_millis(8));
221 let _ = first;
222 assert_eq!(
223 clock
224 .longest_at(start + Duration::from_secs(5))
225 .map(|r| r.gpu_ms.round()),
226 Some(8.0)
227 );
228 }
229
230 #[test]
231 fn a_frames_gpu_time_survives_completions_seen_together() {
232 let clock = SubmissionClock::default();
233 let start = Instant::now();
234 let at = |ms: u64| start + Duration::from_millis(ms);
235 let earlier = clock.record(at(0), None);
236 clock.complete(earlier, at(5));
237 let first = clock.record(at(10), Some(100.0));
238 let second = clock.record(at(10), Some(100.0));
239 assert_eq!(clock.gpu_ms(first..second + 1), None, "not finished");
240 clock.complete(second, at(210));
242 let total = clock.gpu_ms(first..second + 1).expect("finished");
243 assert!((total - 200.0).abs() < 0.5, "{total}");
244 assert_eq!(
245 clock
246 .longest_at(at(300))
247 .and_then(|report| report.predicted_ms),
248 Some(100.0)
249 );
250 }
251
252 #[test]
253 fn the_clock_keeps_the_frame_and_effects_of_each_submission() {
254 let clock = SubmissionClock::default();
255 let start = Instant::now();
256 clock.mark_frame(Some(FrameMark {
257 time_ticks: 4_000,
258 frame_index: Some(1),
259 }));
260 clock.note_effect(&crate::EffectId::from_static("blur", 2));
261 let heavy = clock.record(start, None);
262 clock.complete(heavy, start + Duration::from_millis(30));
263 let light = clock.record(start + Duration::from_millis(40), None);
264 clock.complete(light, start + Duration::from_millis(41));
265 let report = clock
266 .longest_at(start + Duration::from_millis(50))
267 .expect("kept");
268 assert_eq!(report.effects, ["blur@2"]);
269 assert_eq!(report.frame.and_then(|frame| frame.frame_index), Some(1));
270 }
271}