libsurfer/
benchmark.rs

1//! Drawing and handling of the performance plot.
2use 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 &reg.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(&times_f32),
282    ));
283}