use std::collections::VecDeque;
use std::sync::Mutex;
use web_time::Instant;
const KEPT: usize = 32;
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct FrameMark {
pub time_ticks: i64,
pub frame_index: Option<u64>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SubmissionReport {
pub gpu_ms: f64,
pub frame: Option<FrameMark>,
pub effects: Vec<String>,
pub predicted_ms: Option<f64>,
}
#[derive(Debug)]
struct Entry {
submitted_at: Instant,
done_at: Option<Instant>,
frame: Option<FrameMark>,
effects: Vec<String>,
predicted_ms: Option<f64>,
}
#[derive(Debug, Default)]
struct ClockState {
first: u64,
entries: VecDeque<Entry>,
frame: Option<FrameMark>,
effects: Vec<String>,
}
#[derive(Debug, Default)]
pub(crate) struct SubmissionClock {
state: Mutex<ClockState>,
}
impl SubmissionClock {
fn state(&self) -> std::sync::MutexGuard<'_, ClockState> {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub(crate) fn mark_frame(&self, frame: Option<FrameMark>) {
self.state().frame = frame;
}
pub(crate) fn note_effect(&self, effect: &crate::EffectId) {
let name = format!("{}@{}", effect.as_str(), effect.version());
let mut state = self.state();
if !state.effects.contains(&name) {
state.effects.push(name);
}
}
pub(crate) fn record(&self, at: Instant, predicted_ms: Option<f64>) -> u64 {
let mut state = self.state();
let effects = std::mem::take(&mut state.effects);
let frame = state.frame;
state.entries.push_back(Entry {
submitted_at: at,
done_at: None,
frame,
effects,
predicted_ms,
});
if state.entries.len() > KEPT {
state.entries.pop_front();
state.first += 1;
}
state.first + u64::try_from(state.entries.len()).unwrap_or(u64::MAX) - 1
}
pub(crate) fn complete(&self, sequence: u64, at: Instant) {
let mut state = self.state();
let Some(position) = sequence.checked_sub(state.first) else {
return;
};
let Ok(position) = usize::try_from(position) else {
return;
};
for entry in state.entries.iter_mut().take(position + 1) {
entry.done_at.get_or_insert(at);
}
}
fn spans(state: &ClockState, now: Instant) -> Vec<(f64, &Entry)> {
let mut spans = Vec::new();
let mut previous_done: Option<Instant> = None;
for entry in &state.entries {
let start =
previous_done.map_or(entry.submitted_at, |done| done.max(entry.submitted_at));
let Some(done) = entry.done_at else {
spans.push((milliseconds(now.saturating_duration_since(start)), entry));
break;
};
spans.push((milliseconds(done.saturating_duration_since(start)), entry));
previous_done = Some(done);
}
spans
}
pub(crate) fn longest_at(&self, now: Instant) -> Option<SubmissionReport> {
let state = self.state();
Self::spans(&state, now)
.into_iter()
.max_by(|(a, _), (b, _)| a.total_cmp(b))
.map(|(gpu_ms, entry)| SubmissionReport {
gpu_ms,
frame: entry.frame,
effects: entry.effects.clone(),
predicted_ms: entry.predicted_ms,
})
}
pub(crate) fn gpu_ms(&self, sequences: std::ops::Range<u64>) -> Option<f64> {
let state = self.state();
let first = state.first;
let spans = Self::spans(&state, Instant::now());
let mut total = 0.0;
let mut counted = 0;
for (position, (ms, entry)) in spans.into_iter().enumerate() {
let sequence = first + u64::try_from(position).unwrap_or(u64::MAX);
if sequences.contains(&sequence) {
entry.done_at?;
total += ms;
counted += 1;
}
}
(counted == sequences.end.saturating_sub(sequences.start)).then_some(total)
}
pub(crate) fn longest(&self) -> Option<SubmissionReport> {
self.longest_at(Instant::now())
}
pub(crate) fn hung(&self) -> Option<SubmissionReport> {
self.longest()
.filter(|report| report.gpu_ms >= f64::from(alpha2_gpu::hung_threshold_ms()))
}
}
fn milliseconds(duration: std::time::Duration) -> f64 {
duration.as_secs_f64() * 1_000.0
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
#[test]
fn gpu_time_excludes_queueing_behind_earlier_submissions() {
let clock = SubmissionClock::default();
let start = Instant::now();
let at = |ms: u64| start + Duration::from_millis(ms);
let first = clock.record(at(0), None);
let second = clock.record(at(0), None);
let third = clock.record(at(0), None);
clock.complete(first, at(10));
clock.complete(second, at(400));
clock.complete(third, at(405));
let longest = clock.longest_at(at(1_000)).expect("submissions");
assert!((longest.gpu_ms - 390.0).abs() < 0.5, "{longest:?}");
let fourth = clock.record(at(2_000), None);
clock.complete(fourth, at(2_005));
assert!((clock.longest_at(at(3_000)).expect("kept").gpu_ms - 390.0).abs() < 0.5);
let _ = clock.record(at(3_000), None);
assert!((clock.longest_at(at(3_700)).expect("running").gpu_ms - 700.0).abs() < 0.5);
}
#[test]
fn a_completion_finishes_every_earlier_submission() {
let clock = SubmissionClock::default();
let start = Instant::now();
let first = clock.record(start, None);
let second = clock.record(start, None);
clock.complete(second, start + Duration::from_millis(8));
let _ = first;
assert_eq!(
clock
.longest_at(start + Duration::from_secs(5))
.map(|r| r.gpu_ms.round()),
Some(8.0)
);
}
#[test]
fn a_frames_gpu_time_survives_completions_seen_together() {
let clock = SubmissionClock::default();
let start = Instant::now();
let at = |ms: u64| start + Duration::from_millis(ms);
let earlier = clock.record(at(0), None);
clock.complete(earlier, at(5));
let first = clock.record(at(10), Some(100.0));
let second = clock.record(at(10), Some(100.0));
assert_eq!(clock.gpu_ms(first..second + 1), None, "not finished");
clock.complete(second, at(210));
let total = clock.gpu_ms(first..second + 1).expect("finished");
assert!((total - 200.0).abs() < 0.5, "{total}");
assert_eq!(
clock
.longest_at(at(300))
.and_then(|report| report.predicted_ms),
Some(100.0)
);
}
#[test]
fn the_clock_keeps_the_frame_and_effects_of_each_submission() {
let clock = SubmissionClock::default();
let start = Instant::now();
clock.mark_frame(Some(FrameMark {
time_ticks: 4_000,
frame_index: Some(1),
}));
clock.note_effect(&crate::EffectId::from_static("blur", 2));
let heavy = clock.record(start, None);
clock.complete(heavy, start + Duration::from_millis(30));
let light = clock.record(start + Duration::from_millis(40), None);
clock.complete(light, start + Duration::from_millis(41));
let report = clock
.longest_at(start + Duration::from_millis(50))
.expect("kept");
assert_eq!(report.effects, ["blur@2"]);
assert_eq!(report.frame.and_then(|frame| frame.frame_index), Some(1));
}
}