libsurfer/
benchmark.rs
1use std::{
3 cmp::Ordering,
4 collections::{BTreeSet, HashMap, VecDeque},
5 time::{Duration, Instant},
6};
7
8use egui_plot::{Legend, Line, Plot, PlotPoints, PlotUi};
9use itertools::Itertools;
10use log::warn;
11
12use crate::{message::Message, SystemState};
13
14pub const NUM_PERF_SAMPLES: usize = 1000;
15
16struct TimingRegion {
17 durations: VecDeque<Duration>,
18 start: Option<Instant>,
19 end: Option<Instant>,
20 subregions: BTreeSet<String>,
21}
22
23impl TimingRegion {
24 pub fn start(&mut self) {
25 #[cfg(not(target_arch = "wasm32"))]
26 {
27 self.start = Some(Instant::now());
28 }
29 }
30 pub fn stop(&mut self) {
31 #[cfg(not(target_arch = "wasm32"))]
32 {
33 self.end = Some(Instant::now());
34 }
35 }
36}
37
38pub struct Timing {
39 active_region: Vec<String>,
40 regions: HashMap<Vec<String>, TimingRegion>,
41}
42
43impl Timing {
44 pub fn new() -> Self {
45 let initial = vec![(
46 vec![],
47 TimingRegion {
48 durations: VecDeque::new(),
49 start: None,
50 end: None,
51 subregions: BTreeSet::new(),
52 },
53 )]
54 .into_iter()
55 .collect();
56
57 Self {
58 active_region: vec![],
59 regions: initial,
60 }
61 }
62
63 pub fn start_frame(&mut self) {
64 if !self.active_region.is_empty() {
65 warn!(
66 "Starting frame with active region {}",
67 self.active_region.join(".")
68 );
69 }
70 for reg in self.regions.values_mut() {
71 reg.start = None;
72 reg.end = None;
73 }
74 if let Some(r) = self.regions.get_mut(&vec![]) {
75 r.start();
76 }
77 }
78
79 pub fn end_frame(&mut self) {
80 if let Some(r) = self.regions.get_mut(&vec![]) {
81 r.stop();
82 }
83
84 if !self.active_region.is_empty() {
85 warn!(
86 "Ended frame with active region {}",
87 self.active_region.join(".")
88 );
89 }
90
91 for (path, reg) in &mut self.regions {
92 match (reg.start, reg.end) {
93 (Some(start), Some(end)) => reg.durations.push_back(end - start),
94 (None, Some(_)) => {
95 warn!(
96 "Timing region [{}] was stopped but not started",
97 path.join(".")
98 );
99 reg.durations.push_back(Duration::ZERO);
100 }
101 (Some(_), None) => {
102 warn!(
103 "Timing region [{}] was satrted but not stopped",
104 path.join(".")
105 );
106 reg.durations.push_back(Duration::ZERO);
107 }
108 (None, None) => reg.durations.push_back(Duration::ZERO),
109 }
110 if reg.durations.len() > NUM_PERF_SAMPLES {
111 reg.durations.pop_front();
112 }
113 reg.start = None;
114 reg.end = None;
115 }
116 }
117
118 pub fn start(&mut self, name: impl Into<String>) {
119 let name = name.into();
120 if let Some(reg) = self.regions.get_mut(&self.active_region) {
121 if !reg.subregions.contains(&name) {
122 reg.subregions.insert(name.clone());
123 }
124 }
125
126 self.active_region.push(name);
127
128 let region = self
129 .regions
130 .entry(self.active_region.clone())
131 .or_insert_with(|| TimingRegion {
132 durations: VecDeque::new(),
133 start: None,
134 end: None,
135 subregions: BTreeSet::new(),
136 });
137 region.start();
138 }
139
140 pub fn end(&mut self, name: impl Into<String>) {
141 let name = name.into();
142 if let Some(reg) = self.regions.get_mut(&self.active_region) {
143 reg.stop();
144 } else {
145 warn!(
146 "did not find a timing region {}",
147 self.active_region.join(".")
148 );
149 }
150 if let Some(reg_name) = self.active_region.pop() {
151 if reg_name != name {
152 warn!("Ended timing region {reg_name} but used {name}. Timing reports will be unreliable");
153 }
154 } else {
155 warn!("Ended timing region {name} with no timing region active");
156 }
157 }
158}
159
160impl Default for Timing {
161 fn default() -> Self {
162 Self::new()
163 }
164}
165
166impl SystemState {
167 pub fn draw_performance_graph(&self, ctx: &egui::Context, msgs: &mut Vec<Message>) {
168 let mut open = true;
169 egui::Window::new("Frame times")
170 .open(&mut open)
171 .collapsible(true)
172 .resizable(true)
173 .default_width(700.)
174 .show(ctx, |ui| {
175 let timing = self.timing.borrow_mut();
176
177 let frame_times_f32 = timing.regions[&vec![]]
178 .durations
179 .iter()
180 .map(|t| t.as_nanos() as f32 / 1_000_000_000.)
181 .collect::<Vec<_>>();
182
183 ui.horizontal(|ui| {
184 let mut redraw_state = self.continuous_redraw;
185 ui.checkbox(&mut redraw_state, "Continuous redraw");
186 if redraw_state != self.continuous_redraw {
187 msgs.push(Message::SetContinuousRedraw(redraw_state));
188 }
189
190 let f32_cmp = |a: &f32, b: &f32| a.partial_cmp(b).unwrap_or(Ordering::Equal);
191 ui.horizontal(|ui| {
192 ui.monospace(format!(
193 "99%: {:.3}",
194 frame_times_f32.iter().cloned().sum::<f32>()
195 / frame_times_f32.len() as f32
196 ));
197 ui.monospace(format!(
198 "Average: {:.3}",
199 frame_times_f32
200 .iter()
201 .cloned()
202 .sorted_by(f32_cmp)
203 .skip((frame_times_f32.len() as f32 * 0.99) as usize)
204 .sum::<f32>()
205 / (frame_times_f32.len() as f32 * 0.99)
206 ));
207
208 ui.monospace(format!(
209 "min: {:.3}",
210 frame_times_f32
211 .iter()
212 .cloned()
213 .min_by(f32_cmp)
214 .unwrap_or(0.)
215 ));
216
217 ui.monospace(format!(
218 "max: {:.3}",
219 frame_times_f32
220 .iter()
221 .cloned()
222 .max_by(f32_cmp)
223 .unwrap_or(0.)
224 ));
225 })
226 });
227
228 let plot = Plot::new("frame time")
229 .legend(Legend::default())
230 .show_axes([true, true])
231 .show_grid([true, true])
232 .include_x(0)
233 .include_x(NUM_PERF_SAMPLES as f64);
234
235 plot.show(ui, |plot_ui| {
236 plot_ui.line(Line::new(
237 "egui CPU draw time",
238 PlotPoints::from_ys_f32(
239 &self.rendering_cpu_times.iter().cloned().collect::<Vec<_>>(),
240 ),
241 ));
242
243 draw_timing_region(plot_ui, &vec![], &timing);
244
245 plot_ui.line(Line::new(
246 "60 fps",
247 PlotPoints::new(vec![[0., 1. / 60.], [NUM_PERF_SAMPLES as f64, 1. / 60.]]),
248 ));
249 plot_ui.line(Line::new(
250 "30 fps",
251 PlotPoints::new(vec![[0., 1. / 30.], [NUM_PERF_SAMPLES as f64, 1. / 30.]]),
252 ));
253 });
254 });
255 if !open {
256 msgs.push(Message::SetPerformanceVisible(false));
257 }
258 }
259}
260
261pub fn draw_timing_region(plot_ui: &mut PlotUi, region: &Vec<String>, timing: &Timing) {
262 let reg = &timing.regions[region];
263
264 for sub in ®.subregions {
265 let mut new_region = region.clone();
266 new_region.push(sub.clone());
267 draw_timing_region(plot_ui, &new_region, timing);
268 }
269 let times_f32 = timing.regions[region]
270 .durations
271 .iter()
272 .map(|t| t.as_nanos() as f32 / 1_000_000_000.)
273 .collect::<Vec<_>>();
274
275 plot_ui.line(Line::new(
276 if region.is_empty() {
277 "total".to_string()
278 } else {
279 region.join(".")
280 },
281 PlotPoints::from_ys_f32(×_f32),
282 ));
283}