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}