fret_runtime/
runner_present_diagnostics.rs1use 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}