Skip to main content

fret_runtime/
runner_frame_drive_diagnostics.rs

1use std::collections::{BTreeMap, 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, PartialOrd, Ord, Hash)]
10pub enum RunnerFrameDriveReason {
11    EffectRedraw,
12    EffectRequestAnimationFrame,
13    AboutToWaitRaf,
14    StreamingPendingRedrawAll,
15    StreamingPendingRedrawWindow,
16    SurfaceResize,
17    SurfaceBootstrap,
18    SurfaceRecoverLost,
19    SurfaceRecoverOutdated,
20    SurfaceRecoverTimeout,
21    WebDiagKeepaliveRedraw,
22}
23
24impl RunnerFrameDriveReason {
25    pub const fn as_str(self) -> &'static str {
26        match self {
27            Self::EffectRedraw => "effect_redraw",
28            Self::EffectRequestAnimationFrame => "effect_request_animation_frame",
29            Self::AboutToWaitRaf => "about_to_wait_raf",
30            Self::StreamingPendingRedrawAll => "streaming_pending_redraw_all",
31            Self::StreamingPendingRedrawWindow => "streaming_pending_redraw_window",
32            Self::SurfaceResize => "surface_resize",
33            Self::SurfaceBootstrap => "surface_bootstrap",
34            Self::SurfaceRecoverLost => "surface_recover_lost",
35            Self::SurfaceRecoverOutdated => "surface_recover_outdated",
36            Self::SurfaceRecoverTimeout => "surface_recover_timeout",
37            Self::WebDiagKeepaliveRedraw => "web_diag_keepalive_redraw",
38        }
39    }
40
41    pub const fn for_streaming_pending_hint(window_hint_count: usize) -> Self {
42        if window_hint_count == 0 {
43            Self::StreamingPendingRedrawAll
44        } else {
45            Self::StreamingPendingRedrawWindow
46        }
47    }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct RunnerFrameDriveReasonCount {
52    pub reason: RunnerFrameDriveReason,
53    pub count: u64,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Default)]
57pub struct RunnerFrameDriveWindowSnapshot {
58    pub total_event_count: u64,
59    pub last_event_frame_id: u64,
60    pub last_event_unix_ms: Option<u64>,
61    pub reason_counts: Vec<RunnerFrameDriveReasonCount>,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Default)]
65pub struct RunnerFrameDriveAggregateSnapshot {
66    pub window_count: u32,
67    pub total_event_count: u64,
68    pub max_event_count: u64,
69    pub last_event_frame_id: u64,
70    pub last_event_unix_ms: Option<u64>,
71    pub reason_counts: Vec<RunnerFrameDriveReasonCount>,
72}
73
74#[derive(Debug, Default)]
75struct RunnerFrameDriveWindowState {
76    total_event_count: u64,
77    last_event_frame_id: u64,
78    last_event_unix_ms: Option<u64>,
79    reason_counts: BTreeMap<RunnerFrameDriveReason, u64>,
80}
81
82impl RunnerFrameDriveWindowState {
83    fn snapshot(&self) -> RunnerFrameDriveWindowSnapshot {
84        RunnerFrameDriveWindowSnapshot {
85            total_event_count: self.total_event_count,
86            last_event_frame_id: self.last_event_frame_id,
87            last_event_unix_ms: self.last_event_unix_ms,
88            reason_counts: self
89                .reason_counts
90                .iter()
91                .map(|(reason, count)| RunnerFrameDriveReasonCount {
92                    reason: *reason,
93                    count: *count,
94                })
95                .collect(),
96        }
97    }
98}
99
100#[derive(Debug, Default)]
101struct RunnerFrameDriveDiagnosticsState {
102    windows: HashMap<AppWindowId, RunnerFrameDriveWindowState>,
103}
104
105#[derive(Debug, Clone, Default)]
106pub struct RunnerFrameDriveDiagnosticsStore {
107    inner: Arc<Mutex<RunnerFrameDriveDiagnosticsState>>,
108}
109
110impl RunnerFrameDriveDiagnosticsStore {
111    pub fn window_snapshot(&self, window: AppWindowId) -> Option<RunnerFrameDriveWindowSnapshot> {
112        self.inner
113            .lock()
114            .unwrap_or_else(|err| err.into_inner())
115            .windows
116            .get(&window)
117            .map(RunnerFrameDriveWindowState::snapshot)
118    }
119
120    pub fn aggregate_snapshot(&self) -> RunnerFrameDriveAggregateSnapshot {
121        let state = self.inner.lock().unwrap_or_else(|err| err.into_inner());
122        let mut reason_counts = BTreeMap::<RunnerFrameDriveReason, u64>::new();
123        let mut snapshot = RunnerFrameDriveAggregateSnapshot {
124            window_count: state.windows.len().min(u32::MAX as usize) as u32,
125            ..RunnerFrameDriveAggregateSnapshot::default()
126        };
127
128        for window_snapshot in state.windows.values() {
129            snapshot.total_event_count = snapshot
130                .total_event_count
131                .saturating_add(window_snapshot.total_event_count);
132            snapshot.max_event_count = snapshot
133                .max_event_count
134                .max(window_snapshot.total_event_count);
135            let next_unix_ms = window_snapshot.last_event_unix_ms.unwrap_or(0);
136            let current_unix_ms = snapshot.last_event_unix_ms.unwrap_or(0);
137            if next_unix_ms > current_unix_ms
138                || (next_unix_ms == current_unix_ms
139                    && window_snapshot.last_event_frame_id >= snapshot.last_event_frame_id)
140            {
141                snapshot.last_event_unix_ms = window_snapshot.last_event_unix_ms;
142                snapshot.last_event_frame_id = window_snapshot.last_event_frame_id;
143            }
144            for (reason, count) in &window_snapshot.reason_counts {
145                reason_counts
146                    .entry(*reason)
147                    .and_modify(|value| *value = value.saturating_add(*count))
148                    .or_insert(*count);
149            }
150        }
151
152        snapshot.reason_counts = reason_counts
153            .into_iter()
154            .map(|(reason, count)| RunnerFrameDriveReasonCount { reason, count })
155            .collect();
156        snapshot
157    }
158
159    pub fn record(&self, window: AppWindowId, frame_id: FrameId, reason: RunnerFrameDriveReason) {
160        let mut state = self.inner.lock().unwrap_or_else(|err| err.into_inner());
161        let entry = state.windows.entry(window).or_default();
162        entry.total_event_count = entry.total_event_count.saturating_add(1);
163        entry.last_event_frame_id = frame_id.0;
164        entry.last_event_unix_ms = Some(unix_ms_now());
165        entry
166            .reason_counts
167            .entry(reason)
168            .and_modify(|count| *count = count.saturating_add(1))
169            .or_insert(1);
170    }
171
172    pub fn clear_window(&self, window: AppWindowId) -> Option<RunnerFrameDriveWindowSnapshot> {
173        self.inner
174            .lock()
175            .unwrap_or_else(|err| err.into_inner())
176            .windows
177            .remove(&window)
178            .map(|state| state.snapshot())
179    }
180}
181
182fn unix_ms_now() -> u64 {
183    SystemTime::now()
184        .duration_since(UNIX_EPOCH)
185        .map(|duration| duration.as_millis() as u64)
186        .unwrap_or(0)
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use slotmap::KeyData;
193
194    #[test]
195    fn record_updates_window_and_aggregate_reason_counts() {
196        let store = RunnerFrameDriveDiagnosticsStore::default();
197        let w1 = AppWindowId::from(KeyData::from_ffi(1));
198        let w2 = AppWindowId::from(KeyData::from_ffi(2));
199
200        store.record(w1, FrameId(3), RunnerFrameDriveReason::EffectRedraw);
201        store.record(
202            w1,
203            FrameId(4),
204            RunnerFrameDriveReason::EffectRequestAnimationFrame,
205        );
206        store.record(w2, FrameId(8), RunnerFrameDriveReason::AboutToWaitRaf);
207
208        let w1_snapshot = store.window_snapshot(w1).expect("window snapshot");
209        assert_eq!(w1_snapshot.total_event_count, 2);
210        assert_eq!(w1_snapshot.last_event_frame_id, 4);
211        assert!(w1_snapshot.last_event_unix_ms.is_some());
212        assert_eq!(w1_snapshot.reason_counts.len(), 2);
213
214        let aggregate = store.aggregate_snapshot();
215        assert_eq!(aggregate.window_count, 2);
216        assert_eq!(aggregate.total_event_count, 3);
217        assert_eq!(aggregate.max_event_count, 2);
218        assert_eq!(aggregate.last_event_frame_id, 8);
219        assert!(aggregate.last_event_unix_ms.is_some());
220        assert_eq!(aggregate.reason_counts.len(), 3);
221    }
222
223    #[test]
224    fn clear_window_removes_it_from_aggregate_snapshot() {
225        let store = RunnerFrameDriveDiagnosticsStore::default();
226        let window = AppWindowId::from(KeyData::from_ffi(7));
227        store.record(window, FrameId(11), RunnerFrameDriveReason::EffectRedraw);
228        assert!(store.window_snapshot(window).is_some());
229
230        let removed = store.clear_window(window).expect("removed snapshot");
231        assert_eq!(removed.total_event_count, 1);
232        assert!(store.window_snapshot(window).is_none());
233
234        let aggregate = store.aggregate_snapshot();
235        assert_eq!(aggregate.window_count, 0);
236        assert_eq!(aggregate.total_event_count, 0);
237        assert_eq!(aggregate.max_event_count, 0);
238        assert_eq!(aggregate.last_event_unix_ms, None);
239        assert!(aggregate.reason_counts.is_empty());
240    }
241
242    #[test]
243    fn streaming_pending_hint_classifies_all_vs_window_scope() {
244        assert_eq!(
245            RunnerFrameDriveReason::for_streaming_pending_hint(0),
246            RunnerFrameDriveReason::StreamingPendingRedrawAll
247        );
248        assert_eq!(
249            RunnerFrameDriveReason::for_streaming_pending_hint(1),
250            RunnerFrameDriveReason::StreamingPendingRedrawWindow
251        );
252        assert_eq!(
253            RunnerFrameDriveReason::for_streaming_pending_hint(3),
254            RunnerFrameDriveReason::StreamingPendingRedrawWindow
255        );
256    }
257}