Skip to main content

fret_runtime/
redraw_request_diagnostics.rs

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