Skip to main content

fret_runtime/
runner_present_diagnostics.rs

1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3
4use fret_core::{
5    AppWindowId, FrameId,
6    time::{SystemTime, UNIX_EPOCH},
7};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub struct RunnerPresentWindowSnapshot {
11    pub present_count: u64,
12    pub last_present_frame_id: u64,
13    pub last_present_unix_ms: Option<u64>,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub struct RunnerPresentAggregateSnapshot {
18    pub window_count: u32,
19    pub total_present_count: u64,
20    pub max_present_count: u64,
21    pub last_present_frame_id: u64,
22    pub last_present_unix_ms: Option<u64>,
23}
24
25#[derive(Debug, Default)]
26struct RunnerPresentDiagnosticsState {
27    windows: HashMap<AppWindowId, RunnerPresentWindowSnapshot>,
28}
29
30#[derive(Debug, Clone, Default)]
31pub struct RunnerPresentDiagnosticsStore {
32    inner: Arc<Mutex<RunnerPresentDiagnosticsState>>,
33}
34
35impl RunnerPresentDiagnosticsStore {
36    pub fn window_snapshot(&self, window: AppWindowId) -> Option<RunnerPresentWindowSnapshot> {
37        self.inner
38            .lock()
39            .unwrap_or_else(|err| err.into_inner())
40            .windows
41            .get(&window)
42            .copied()
43    }
44
45    pub fn aggregate_snapshot(&self) -> RunnerPresentAggregateSnapshot {
46        let state = self.inner.lock().unwrap_or_else(|err| err.into_inner());
47        let mut snapshot = RunnerPresentAggregateSnapshot {
48            window_count: state.windows.len().min(u32::MAX as usize) as u32,
49            ..RunnerPresentAggregateSnapshot::default()
50        };
51
52        for window_snapshot in state.windows.values() {
53            snapshot.total_present_count = snapshot
54                .total_present_count
55                .saturating_add(window_snapshot.present_count);
56            snapshot.max_present_count = snapshot
57                .max_present_count
58                .max(window_snapshot.present_count);
59            let next_unix_ms = window_snapshot.last_present_unix_ms.unwrap_or(0);
60            let current_unix_ms = snapshot.last_present_unix_ms.unwrap_or(0);
61            if next_unix_ms > current_unix_ms
62                || (next_unix_ms == current_unix_ms
63                    && window_snapshot.last_present_frame_id >= snapshot.last_present_frame_id)
64            {
65                snapshot.last_present_unix_ms = window_snapshot.last_present_unix_ms;
66                snapshot.last_present_frame_id = window_snapshot.last_present_frame_id;
67            }
68        }
69
70        snapshot
71    }
72
73    pub fn record_present(&self, window: AppWindowId, frame_id: FrameId) {
74        let mut state = self.inner.lock().unwrap_or_else(|err| err.into_inner());
75        let entry = state.windows.entry(window).or_default();
76        entry.present_count = entry.present_count.saturating_add(1);
77        entry.last_present_frame_id = frame_id.0;
78        entry.last_present_unix_ms = Some(unix_ms_now());
79    }
80
81    pub fn clear_window(&self, window: AppWindowId) -> Option<RunnerPresentWindowSnapshot> {
82        self.inner
83            .lock()
84            .unwrap_or_else(|err| err.into_inner())
85            .windows
86            .remove(&window)
87    }
88}
89
90fn unix_ms_now() -> u64 {
91    SystemTime::now()
92        .duration_since(UNIX_EPOCH)
93        .map(|duration| duration.as_millis() as u64)
94        .unwrap_or(0)
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use slotmap::KeyData;
101
102    #[test]
103    fn record_present_updates_window_and_aggregate_snapshots() {
104        let store = RunnerPresentDiagnosticsStore::default();
105        let w1 = AppWindowId::from(KeyData::from_ffi(1));
106        let w2 = AppWindowId::from(KeyData::from_ffi(2));
107
108        store.record_present(w1, FrameId(3));
109        store.record_present(w1, FrameId(4));
110        store.record_present(w2, FrameId(8));
111
112        let w1_snapshot = store.window_snapshot(w1).expect("window snapshot");
113        assert_eq!(w1_snapshot.present_count, 2);
114        assert_eq!(w1_snapshot.last_present_frame_id, 4);
115        assert!(w1_snapshot.last_present_unix_ms.is_some());
116
117        let aggregate = store.aggregate_snapshot();
118        assert_eq!(aggregate.window_count, 2);
119        assert_eq!(aggregate.total_present_count, 3);
120        assert_eq!(aggregate.max_present_count, 2);
121        assert_eq!(aggregate.last_present_frame_id, 8);
122        assert!(aggregate.last_present_unix_ms.is_some());
123    }
124
125    #[test]
126    fn clear_window_removes_it_from_aggregate_snapshot() {
127        let store = RunnerPresentDiagnosticsStore::default();
128        let window = AppWindowId::from(KeyData::from_ffi(7));
129        store.record_present(window, FrameId(11));
130        assert!(store.window_snapshot(window).is_some());
131
132        let removed = store.clear_window(window).expect("removed snapshot");
133        assert_eq!(removed.present_count, 1);
134        assert!(store.window_snapshot(window).is_none());
135
136        let aggregate = store.aggregate_snapshot();
137        assert_eq!(aggregate.window_count, 0);
138        assert_eq!(aggregate.total_present_count, 0);
139        assert_eq!(aggregate.max_present_count, 0);
140        assert_eq!(aggregate.last_present_unix_ms, None);
141    }
142}