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 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 ®.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(×_f32),
285 ));
286}