1use std::{
2 collections::HashMap,
3 time::{
4 Duration,
5 Instant,
6 },
7};
8
9use freya_core::prelude::{
10 ModifiersExt,
11 UserEvent,
12};
13use freya_engine::prelude::{
14 Canvas,
15 Color,
16 FontCollection,
17 FontStyle,
18 Paint,
19 PaintStyle,
20 ParagraphBuilder,
21 ParagraphStyle,
22 Rect,
23 Slant,
24 TextAlign,
25 TextStyle,
26 Weight,
27 Width,
28};
29use freya_winit::{
30 plugins::{
31 FreyaPlugin,
32 Key,
33 Modifiers,
34 PluginEvent,
35 PluginHandle,
36 },
37 reexports::winit::window::WindowId,
38 renderer::{
39 NativeEvent,
40 NativeWindowEvent,
41 NativeWindowEventAction,
42 },
43};
44
45const FRAME_TIME_SAMPLES: usize = 100;
46
47pub struct PerformanceOverlayPlugin {
51 enabled: bool,
52 toggle_shortcut: (Key, Modifiers),
53 metrics: HashMap<WindowId, WindowMetrics>,
54}
55
56impl Default for PerformanceOverlayPlugin {
57 fn default() -> Self {
58 Self {
59 enabled: false,
60 toggle_shortcut: (
61 Key::Character("p".into()),
62 Modifiers::ctrl_or_meta() | Modifiers::SHIFT,
63 ),
64 metrics: HashMap::new(),
65 }
66 }
67}
68
69#[derive(Default)]
70struct WindowMetrics {
71 graphics_driver: &'static str,
72 gpu_name: Option<String>,
73
74 frames: Vec<Instant>,
75
76 started_redraw: Option<Instant>,
77 frame_times: Vec<f32>,
78 graph_scale_max: f32,
79 graph_scale_checked_at: Option<Instant>,
80
81 started_render: Option<Instant>,
82
83 started_layout: Option<Instant>,
84 finished_layout: Option<Duration>,
85
86 started_tree_updates: Option<Instant>,
87 finished_tree_updates: Option<Duration>,
88
89 started_tasks_poll: Option<Instant>,
90 tasks_poll_time: Duration,
91
92 started_events: Option<Instant>,
93 events_time: Duration,
94
95 started_accessibility_updates: Option<Instant>,
96 finished_accessibility_updates: Option<Duration>,
97
98 started_presenting: Option<Instant>,
99 finished_presenting: Option<Duration>,
100
101 overlay_time: Duration,
102}
103
104impl WindowMetrics {
105 fn record_frame_time(&mut self) {
106 let Some(started_redraw) = self.started_redraw.take() else {
107 return;
108 };
109 let frame_time = started_redraw.elapsed().as_secs_f32() * 1000.0;
110 self.frame_times.push(frame_time);
111 if self.frame_times.len() > FRAME_TIME_SAMPLES {
112 self.frame_times.remove(0);
113 }
114 }
115
116 fn graph_scale_max(&mut self) -> f32 {
117 let max_frame_time = self.frame_times.iter().copied().fold(0.0, f32::max);
118 let required = nice_scale_max(max_frame_time);
119
120 let due_for_recheck = match self.graph_scale_checked_at {
121 Some(checked_at) => checked_at.elapsed() >= Duration::from_secs(1),
122 None => true,
123 };
124
125 if max_frame_time > self.graph_scale_max || due_for_recheck {
126 self.graph_scale_max = required;
127 self.graph_scale_checked_at = Some(Instant::now());
128 }
129
130 self.graph_scale_max
131 }
132}
133
134impl PerformanceOverlayPlugin {
135 pub fn with_toggle_shortcut(mut self, key: Key, modifiers: Modifiers) -> Self {
137 self.toggle_shortcut = (key, modifiers);
138 self
139 }
140
141 pub fn with_visible(mut self, visible: bool) -> Self {
143 self.enabled = visible;
144 self
145 }
146
147 fn get_metrics(&mut self, id: WindowId) -> &mut WindowMetrics {
148 self.metrics.entry(id).or_default()
149 }
150}
151
152impl FreyaPlugin for PerformanceOverlayPlugin {
153 fn plugin_id(&self) -> &'static str {
154 "freya-performance-overlay"
155 }
156
157 fn on_event(&mut self, event: &mut PluginEvent, handle: PluginHandle) {
158 match event {
159 PluginEvent::KeyboardInput {
160 window,
161 key,
162 modifiers,
163 is_pressed,
164 ..
165 } => {
166 let (shortcut_key, shortcut_modifiers) = &self.toggle_shortcut;
167 let key_matches = match (key, shortcut_key) {
168 (Key::Character(a), Key::Character(b)) => a.eq_ignore_ascii_case(b),
169 (a, b) => a == b,
170 };
171 if *is_pressed && *modifiers == *shortcut_modifiers && key_matches {
172 self.enabled = !self.enabled;
173 handle.send_event_loop_event(NativeEvent::Window(NativeWindowEvent {
174 window_id: window.id(),
175 action: NativeWindowEventAction::User(UserEvent::RequestRedraw),
176 }));
177 }
178 }
179 PluginEvent::WindowCreated {
180 window,
181 graphics_driver,
182 gpu_name,
183 ..
184 }
185 | PluginEvent::GraphicsDriverChanged {
186 window,
187 graphics_driver,
188 gpu_name,
189 } => {
190 let metrics = self.get_metrics(window.id());
191 metrics.graphics_driver = graphics_driver;
192 metrics.gpu_name = gpu_name.map(str::to_string);
193 }
194 PluginEvent::AfterRedraw { window, .. } => {
195 let metrics = self.get_metrics(window.id());
196 let now = Instant::now();
197
198 metrics.record_frame_time();
199
200 metrics
201 .frames
202 .retain(|frame| now.duration_since(*frame).as_millis() < 1000);
203
204 metrics.frames.push(now);
205
206 metrics.tasks_poll_time = Duration::ZERO;
208 metrics.events_time = Duration::ZERO;
209 metrics.finished_layout = None;
210 }
211 PluginEvent::BeforePresenting { window, .. } => {
212 self.get_metrics(window.id()).started_presenting = Some(Instant::now())
213 }
214 PluginEvent::AfterPresenting { window, .. } => {
215 let metrics = self.get_metrics(window.id());
216 metrics.finished_presenting = Some(metrics.started_presenting.unwrap().elapsed())
217 }
218 PluginEvent::StartedMeasuringLayout { window, .. } => {
219 let metrics = self.get_metrics(window.id());
220 metrics.started_redraw.get_or_insert(Instant::now());
221 metrics.started_layout = Some(Instant::now());
222 }
223 PluginEvent::FinishedMeasuringLayout { window, .. } => {
224 let metrics = self.get_metrics(window.id());
225 metrics.finished_layout = Some(metrics.started_layout.unwrap().elapsed())
226 }
227 PluginEvent::StartedUpdatingTree { window, .. } => {
228 self.get_metrics(window.id()).started_tree_updates = Some(Instant::now())
229 }
230 PluginEvent::FinishedUpdatingTree { window, .. } => {
231 let metrics = self.get_metrics(window.id());
232 metrics.finished_tree_updates =
233 Some(metrics.started_tree_updates.unwrap().elapsed())
234 }
235 PluginEvent::StartedPollingTasks { window, .. } => {
236 self.get_metrics(window.id()).started_tasks_poll = Some(Instant::now())
237 }
238 PluginEvent::FinishedPollingTasks { window, .. } => {
239 let metrics = self.get_metrics(window.id());
240 if let Some(started) = metrics.started_tasks_poll.take() {
241 metrics.tasks_poll_time += started.elapsed();
242 }
243 if self.enabled {
244 handle.send_event_loop_event(NativeEvent::Window(NativeWindowEvent {
245 window_id: window.id(),
246 action: NativeWindowEventAction::User(UserEvent::RequestRedraw),
247 }));
248 }
249 }
250 PluginEvent::StartedMeasuringEvents { window, .. } => {
251 self.get_metrics(window.id()).started_events = Some(Instant::now())
252 }
253 PluginEvent::FinishedMeasuringEvents { window, .. } => {
254 let metrics = self.get_metrics(window.id());
255 if let Some(started) = metrics.started_events.take() {
256 metrics.events_time += started.elapsed();
257 }
258 if self.enabled {
259 handle.send_event_loop_event(NativeEvent::Window(NativeWindowEvent {
260 window_id: window.id(),
261 action: NativeWindowEventAction::User(UserEvent::RequestRedraw),
262 }));
263 }
264 }
265 PluginEvent::BeforeAccessibility { window, .. } => {
266 self.get_metrics(window.id()).started_accessibility_updates = Some(Instant::now())
267 }
268 PluginEvent::AfterAccessibility { window, .. } => {
269 let metrics = self.get_metrics(window.id());
270 metrics.finished_accessibility_updates =
271 Some(metrics.started_accessibility_updates.unwrap().elapsed())
272 }
273 PluginEvent::BeforeRender { window, .. } => {
274 let metrics = self.get_metrics(window.id());
275 metrics.started_redraw.get_or_insert(Instant::now());
276 metrics.started_render = Some(Instant::now());
277 }
278 PluginEvent::AfterRender {
279 window,
280 canvas,
281 font_collection,
282 tree,
283 animation_clock,
284 } => {
285 if !self.enabled {
286 return;
287 }
288 let metrics = self.get_metrics(window.id());
289 let scale_factor = window.scale_factor() as f32;
290 let started_render = metrics.started_render.take().unwrap();
291
292 canvas.save();
293 canvas.scale((scale_factor, scale_factor));
294
295 let finished_render = started_render.elapsed();
296 let finished_presenting = metrics.finished_presenting.unwrap_or_default();
297 let finished_layout = metrics.finished_layout.unwrap_or_default();
298 let finished_tree_updates = metrics.finished_tree_updates.unwrap_or_default();
299 let tasks_poll_time = metrics.tasks_poll_time;
300 let events_time = metrics.events_time;
301 let finished_accessibility_updates =
302 metrics.finished_accessibility_updates.unwrap_or_default();
303 let overlay_time = metrics.overlay_time;
304 let overlay_started = Instant::now();
305
306 let mut fps_paragraph_builder =
307 ParagraphBuilder::new(&ParagraphStyle::default(), *font_collection);
308 add_text(
309 &mut fps_paragraph_builder,
310 format!("{} FPS", metrics.frames.len()),
311 24.0,
312 );
313 let mut fps_paragraph = fps_paragraph_builder.build();
314 fps_paragraph.layout(235.0);
315
316 let rows = [
317 (
318 "Rendering",
319 format!("{:.3}ms", finished_render.as_secs_f64() * 1000.0),
320 ),
321 (
322 "Presenting",
323 format!("{:.3}ms", finished_presenting.as_secs_f64() * 1000.0),
324 ),
325 (
326 "Layout",
327 format!("{:.3}ms", finished_layout.as_secs_f64() * 1000.0),
328 ),
329 (
330 "Tree Updates",
331 format!("{:.3}ms", finished_tree_updates.as_secs_f64() * 1000.0),
332 ),
333 (
334 "a11y Updates",
335 format!(
336 "{:.3}ms",
337 finished_accessibility_updates.as_secs_f64() * 1000.0
338 ),
339 ),
340 (
341 "Tasks",
342 format!("{:.3}ms", tasks_poll_time.as_secs_f64() * 1000.0),
343 ),
344 (
345 "Events",
346 format!("{:.3}ms", events_time.as_secs_f64() * 1000.0),
347 ),
348 (
349 "Overlay",
350 format!("{:.3}ms", overlay_time.as_secs_f64() * 1000.0),
351 ),
352 (
353 "Frame",
354 format!(
355 "{:.3}ms",
356 (finished_render
357 + finished_presenting
358 + finished_layout
359 + finished_tree_updates
360 + tasks_poll_time
361 + events_time
362 + finished_accessibility_updates
363 + overlay_time)
364 .as_secs_f64()
365 * 1000.0
366 ),
367 ),
368 ("Tree Nodes", tree.size().to_string()),
369 ("Layout Nodes", tree.layout.size().to_string()),
370 ("Scale Factor", format!("{}x", window.scale_factor())),
371 (
372 "Animation clock speed",
373 format!("{}x", animation_clock.speed()),
374 ),
375 ("Renderer", metrics.graphics_driver.to_string()),
376 ("Freya", env!("CARGO_PKG_VERSION").to_string()),
377 (
378 "Build",
379 (if cfg!(debug_assertions) {
380 "Debug"
381 } else {
382 "Release"
383 })
384 .to_string(),
385 ),
386 ];
387
388 let mut keys_paragraph_builder =
389 ParagraphBuilder::new(&ParagraphStyle::default(), *font_collection);
390 let mut values_style = ParagraphStyle::default();
391 values_style.set_text_align(TextAlign::Right);
392 let mut values_paragraph_builder =
393 ParagraphBuilder::new(&values_style, *font_collection);
394 for (key, value) in &rows {
395 add_text(&mut keys_paragraph_builder, format!("{key}\n"), 14.0);
396 add_text(&mut values_paragraph_builder, format!("{value}\n"), 14.0);
397 }
398 let mut keys_paragraph = keys_paragraph_builder.build();
399 keys_paragraph.layout(235.0);
400 let mut values_paragraph = values_paragraph_builder.build();
401 values_paragraph.layout(235.0);
402
403 let gpu_paragraph = metrics.gpu_name.as_ref().map(|gpu_name| {
404 let mut builder =
405 ParagraphBuilder::new(&ParagraphStyle::default(), *font_collection);
406 add_text(&mut builder, format!("GPU: {gpu_name}"), 14.0);
407 let mut paragraph = builder.build();
408 paragraph.layout(235.0);
409 paragraph
410 });
411
412 let graph_left = 40.0;
413 let graph_top = fps_paragraph.height() + 10.0;
414 let graph_bottom = graph_top + 60.0;
415
416 let rows_top = graph_bottom + 22.0;
417 let gpu_top = rows_top + keys_paragraph.height() + 4.0;
418 let content_bottom = gpu_paragraph
419 .as_ref()
420 .map(|paragraph| gpu_top + paragraph.height())
421 .unwrap_or(rows_top + keys_paragraph.height());
422
423 let mut paint = Paint::default();
424 paint.set_anti_alias(true);
425 paint.set_style(PaintStyle::Fill);
426 paint.set_color(Color::from_argb(235, 24, 24, 24));
427 canvas.draw_rect(Rect::new(5., 5., 245.0, content_bottom + 10.0), &paint);
428
429 fps_paragraph.paint(canvas, (5.0, 0.0));
430 keys_paragraph.paint(canvas, (5.0, rows_top));
431 values_paragraph.paint(canvas, (5.0, rows_top));
432 if let Some(paragraph) = &gpu_paragraph {
433 paragraph.paint(canvas, (5.0, gpu_top));
434 }
435
436 let scale_max = metrics.graph_scale_max();
437
438 let mut axis_paint = Paint::default();
439 axis_paint.set_anti_alias(true);
440 axis_paint.set_style(PaintStyle::Stroke);
441 axis_paint.set_stroke_width(1.0);
442 axis_paint.set_color(Color::from_rgb(130, 130, 130));
443
444 canvas.draw_line(
445 (graph_left, graph_top),
446 (graph_left, graph_bottom),
447 &axis_paint,
448 );
449 canvas.draw_line(
450 (graph_left, graph_bottom),
451 (graph_left + 195.0, graph_bottom),
452 &axis_paint,
453 );
454
455 let decimals = if scale_max < 10.0 { 1 } else { 0 };
456 for (value, y) in [
457 (scale_max, graph_top),
458 (scale_max / 2.0, graph_top + 30.0),
459 (0.0, graph_bottom),
460 ] {
461 canvas.draw_line((graph_left - 3.0, y), (graph_left, y), &axis_paint);
462 draw_axis_label(
463 canvas,
464 font_collection,
465 &format!("{value:.decimals$}ms"),
466 7.0,
467 y - 6.0,
468 );
469 }
470
471 draw_axis_label(
472 canvas,
473 font_collection,
474 &format!("last {} frames", metrics.frame_times.len()),
475 graph_left + 62.5,
476 graph_bottom + 4.0,
477 );
478
479 let mut line_paint = Paint::default();
480 line_paint.set_anti_alias(true);
481 line_paint.set_style(PaintStyle::Stroke);
482 line_paint.set_stroke_width(1.5);
483 line_paint.set_color(Color::from_rgb(255, 204, 92));
484
485 let step = 195.0 / (FRAME_TIME_SAMPLES - 1) as f32;
486 let point = |index: usize, frame_time: f32| {
487 let x = graph_left + index as f32 * step;
488 let y = graph_bottom - (frame_time / scale_max).min(1.0) * 60.0;
489 (x, y)
490 };
491 for (index, window) in metrics.frame_times.windows(2).enumerate() {
492 canvas.draw_line(
493 point(index, window[0]),
494 point(index + 1, window[1]),
495 &line_paint,
496 );
497 }
498
499 metrics.overlay_time = overlay_started.elapsed();
500
501 canvas.restore();
502 }
503 _ => {}
504 }
505 }
506}
507
508fn nice_scale_max(value: f32) -> f32 {
510 if value <= 0.0 {
511 return 1.0;
512 }
513 let magnitude = 10f32.powf(value.log10().floor());
514 let fraction = value / magnitude;
515 let nice_fraction = if fraction <= 1.0 {
516 1.0
517 } else if fraction <= 2.0 {
518 2.0
519 } else if fraction <= 5.0 {
520 5.0
521 } else {
522 10.0
523 };
524 nice_fraction * magnitude
525}
526
527fn draw_axis_label(canvas: &Canvas, font_collection: &FontCollection, text: &str, x: f32, y: f32) {
528 let mut paragraph_builder = ParagraphBuilder::new(&ParagraphStyle::default(), font_collection);
529 let mut text_style = TextStyle::default();
530 text_style.set_color(Color::from_rgb(170, 170, 170));
531 text_style.set_font_size(10.0);
532 paragraph_builder.push_style(&text_style);
533 paragraph_builder.add_text(text);
534 let mut paragraph = paragraph_builder.build();
535 paragraph.layout(90.0);
536 paragraph.paint(canvas, (x, y));
537}
538
539fn add_text(paragraph_builder: &mut ParagraphBuilder, text: String, font_size: f32) {
540 let mut text_style = TextStyle::default();
541 text_style.set_color(Color::from_rgb(255, 204, 92));
542 let font_style = FontStyle::new(Weight::BOLD, Width::NORMAL, Slant::Upright);
543 text_style.set_font_style(font_style);
544 text_style.set_font_size(font_size);
545 paragraph_builder.push_style(&text_style);
546 paragraph_builder.add_text(text);
547}