Skip to main content

fret_runtime/
window_global_change_diagnostics.rs

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}