Skip to main content

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