1use 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, PartialEq, Eq)]
10pub struct WindowGlobalChangeNameCount {
11 pub name: String,
12 pub count: u64,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, Default)]
16pub struct WindowGlobalChangeWindowSnapshot {
17 pub batch_count: u64,
18 pub total_global_count: u64,
19 pub last_frame_id: u64,
20 pub last_unix_ms: Option<u64>,
21 pub globals: Vec<WindowGlobalChangeNameCount>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Default)]
25pub struct WindowGlobalChangeAggregateSnapshot {
26 pub window_count: u32,
27 pub batch_count: u64,
28 pub total_global_count: u64,
29 pub max_batch_count: u64,
30 pub last_frame_id: u64,
31 pub last_unix_ms: Option<u64>,
32 pub globals: Vec<WindowGlobalChangeNameCount>,
33}
34
35#[derive(Debug, Default)]
36struct WindowGlobalChangeWindowState {
37 batch_count: u64,
38 total_global_count: u64,
39 last_frame_id: u64,
40 last_unix_ms: Option<u64>,
41 globals: HashMap<String, u64>,
42}
43
44impl WindowGlobalChangeWindowState {
45 fn snapshot(&self) -> WindowGlobalChangeWindowSnapshot {
46 let mut globals: Vec<_> = self
47 .globals
48 .iter()
49 .map(|(name, count)| WindowGlobalChangeNameCount {
50 name: name.clone(),
51 count: *count,
52 })
53 .collect();
54 globals.sort_by(|a, b| b.count.cmp(&a.count).then_with(|| a.name.cmp(&b.name)));
55 WindowGlobalChangeWindowSnapshot {
56 batch_count: self.batch_count,
57 total_global_count: self.total_global_count,
58 last_frame_id: self.last_frame_id,
59 last_unix_ms: self.last_unix_ms,
60 globals,
61 }
62 }
63}
64
65#[derive(Debug, Default)]
66struct WindowGlobalChangeDiagnosticsState {
67 windows: HashMap<AppWindowId, WindowGlobalChangeWindowState>,
68}
69
70#[derive(Debug, Clone, Default)]
71pub struct WindowGlobalChangeDiagnosticsStore {
72 inner: Arc<Mutex<WindowGlobalChangeDiagnosticsState>>,
73}
74
75impl WindowGlobalChangeDiagnosticsStore {
76 pub fn window_snapshot(&self, window: AppWindowId) -> Option<WindowGlobalChangeWindowSnapshot> {
77 self.inner
78 .lock()
79 .unwrap_or_else(|err| err.into_inner())
80 .windows
81 .get(&window)
82 .map(WindowGlobalChangeWindowState::snapshot)
83 }
84
85 pub fn aggregate_snapshot(&self) -> WindowGlobalChangeAggregateSnapshot {
86 let state = self.inner.lock().unwrap_or_else(|err| err.into_inner());
87 let mut globals = HashMap::<String, u64>::new();
88 let mut snapshot = WindowGlobalChangeAggregateSnapshot {
89 window_count: state.windows.len().min(u32::MAX as usize) as u32,
90 ..WindowGlobalChangeAggregateSnapshot::default()
91 };
92
93 for window_snapshot in state.windows.values() {
94 snapshot.batch_count = snapshot
95 .batch_count
96 .saturating_add(window_snapshot.batch_count);
97 snapshot.total_global_count = snapshot
98 .total_global_count
99 .saturating_add(window_snapshot.total_global_count);
100 snapshot.max_batch_count = snapshot.max_batch_count.max(window_snapshot.batch_count);
101 let next_unix_ms = window_snapshot.last_unix_ms.unwrap_or(0);
102 let current_unix_ms = snapshot.last_unix_ms.unwrap_or(0);
103 if next_unix_ms > current_unix_ms
104 || (next_unix_ms == current_unix_ms
105 && window_snapshot.last_frame_id >= snapshot.last_frame_id)
106 {
107 snapshot.last_unix_ms = window_snapshot.last_unix_ms;
108 snapshot.last_frame_id = window_snapshot.last_frame_id;
109 }
110 for (name, count) in &window_snapshot.globals {
111 globals
112 .entry(name.clone())
113 .and_modify(|value| *value = value.saturating_add(*count))
114 .or_insert(*count);
115 }
116 }
117
118 let mut aggregate_globals: Vec<_> = globals
119 .into_iter()
120 .map(|(name, count)| WindowGlobalChangeNameCount { name, count })
121 .collect();
122 aggregate_globals.sort_by(|a, b| b.count.cmp(&a.count).then_with(|| a.name.cmp(&b.name)));
123 snapshot.globals = aggregate_globals;
124 snapshot
125 }
126
127 pub fn record_batch<I>(&self, window: AppWindowId, frame_id: FrameId, names: I)
128 where
129 I: IntoIterator,
130 I::Item: AsRef<str>,
131 {
132 let mut state = self.inner.lock().unwrap_or_else(|err| err.into_inner());
133 let entry = state.windows.entry(window).or_default();
134 entry.batch_count = entry.batch_count.saturating_add(1);
135 entry.last_frame_id = frame_id.0;
136 entry.last_unix_ms = Some(unix_ms_now());
137 for name in names {
138 entry.total_global_count = entry.total_global_count.saturating_add(1);
139 let name = name.as_ref();
140 entry
141 .globals
142 .entry(name.to_string())
143 .and_modify(|count| *count = count.saturating_add(1))
144 .or_insert(1);
145 }
146 }
147
148 pub fn clear_window(&self, window: AppWindowId) -> Option<WindowGlobalChangeWindowSnapshot> {
149 self.inner
150 .lock()
151 .unwrap_or_else(|err| err.into_inner())
152 .windows
153 .remove(&window)
154 .map(|state| state.snapshot())
155 }
156}
157
158fn unix_ms_now() -> u64 {
159 SystemTime::now()
160 .duration_since(UNIX_EPOCH)
161 .map(|duration| duration.as_millis() as u64)
162 .unwrap_or(0)
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use slotmap::KeyData;
169
170 #[test]
171 fn record_batch_aggregates_global_names() {
172 let store = WindowGlobalChangeDiagnosticsStore::default();
173 let w1 = AppWindowId::from(KeyData::from_ffi(1));
174 let w2 = AppWindowId::from(KeyData::from_ffi(2));
175
176 store.record_batch(w1, FrameId(3), ["A", "B"]);
177 store.record_batch(w1, FrameId(4), ["A"]);
178 store.record_batch(w2, FrameId(8), ["C"]);
179
180 let w1_snapshot = store.window_snapshot(w1).expect("window snapshot");
181 assert_eq!(w1_snapshot.batch_count, 2);
182 assert_eq!(w1_snapshot.total_global_count, 3);
183 assert_eq!(w1_snapshot.globals[0].name, "A");
184 assert_eq!(w1_snapshot.globals[0].count, 2);
185
186 let aggregate = store.aggregate_snapshot();
187 assert_eq!(aggregate.window_count, 2);
188 assert_eq!(aggregate.batch_count, 3);
189 assert_eq!(aggregate.total_global_count, 4);
190 assert_eq!(aggregate.max_batch_count, 2);
191 assert_eq!(aggregate.last_frame_id, 8);
192 assert_eq!(aggregate.globals[0].name, "A");
193 assert_eq!(aggregate.globals[0].count, 2);
194 }
195
196 #[test]
197 fn clear_window_removes_global_counts() {
198 let store = WindowGlobalChangeDiagnosticsStore::default();
199 let window = AppWindowId::from(KeyData::from_ffi(7));
200 store.record_batch(window, FrameId(11), ["A"]);
201
202 let removed = store.clear_window(window).expect("removed snapshot");
203 assert_eq!(removed.batch_count, 1);
204 assert_eq!(removed.total_global_count, 1);
205 assert!(store.window_snapshot(window).is_none());
206
207 let aggregate = store.aggregate_snapshot();
208 assert_eq!(aggregate.window_count, 0);
209 assert_eq!(aggregate.batch_count, 0);
210 assert_eq!(aggregate.total_global_count, 0);
211 assert_eq!(aggregate.max_batch_count, 0);
212 assert_eq!(aggregate.last_unix_ms, None);
213 assert!(aggregate.globals.is_empty());
214 }
215}