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}