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 key,
161 modifiers,
162 is_pressed,
163 ..
164 } => {
165 let (shortcut_key, shortcut_modifiers) = &self.toggle_shortcut;
166 let key_matches = match (key, shortcut_key) {
167 (Key::Character(a), Key::Character(b)) => a.eq_ignore_ascii_case(b),
168 (a, b) => a == b,
169 };
170 if *is_pressed && *modifiers == *shortcut_modifiers && key_matches {
171 self.enabled = !self.enabled;
172 for window_id in self.metrics.keys() {
173 handle.send_event_loop_event(NativeEvent::Window(NativeWindowEvent {
174 window_id: *window_id,
175 action: NativeWindowEventAction::User(UserEvent::RequestRedraw),
176 }));
177 }
178 }
179 }
180 PluginEvent::WindowCreated {
181 window,
182 graphics_driver,
183 gpu_name,
184 ..
185 }
186 | PluginEvent::GraphicsDriverChanged {
187 window,
188 graphics_driver,
189 gpu_name,
190 } => {
191 let metrics = self.get_metrics(window.id());
192 metrics.graphics_driver = graphics_driver;
193 metrics.gpu_name = gpu_name.map(str::to_string);
194 }
195 PluginEvent::WindowClosed { window, .. } => {
196 self.metrics.remove(&window.id());
197 }
198 PluginEvent::AfterRedraw { window, .. } => {
199 let metrics = self.get_metrics(window.id());
200 let now = Instant::now();
201
202 metrics.record_frame_time();
203
204 metrics
205 .frames
206 .retain(|frame| now.duration_since(*frame).as_millis() < 1000);
207
208 metrics.frames.push(now);
209
210 metrics.tasks_poll_time = Duration::ZERO;
212 metrics.events_time = Duration::ZERO;
213 metrics.finished_layout = None;
214 }
215 PluginEvent::BeforePresenting { window, .. } => {
216 self.get_metrics(window.id()).started_presenting = Some(Instant::now())
217 }
218 PluginEvent::AfterPresenting { window, .. } => {
219 let metrics = self.get_metrics(window.id());
220 metrics.finished_presenting = Some(metrics.started_presenting.unwrap().elapsed())
221 }
222 PluginEvent::StartedMeasuringLayout { window, .. } => {
223 let metrics = self.get_metrics(window.id());
224 metrics.started_redraw.get_or_insert(Instant::now());
225 metrics.started_layout = Some(Instant::now());
226 }
227 PluginEvent::FinishedMeasuringLayout { window, .. } => {
228 let metrics = self.get_metrics(window.id());
229 metrics.finished_layout = Some(metrics.started_layout.unwrap().elapsed())
230 }
231 PluginEvent::StartedUpdatingTree { window, .. } => {
232 self.get_metrics(window.id()).started_tree_updates = Some(Instant::now())
233 }
234 PluginEvent::FinishedUpdatingTree { window, .. } => {
235 let metrics = self.get_metrics(window.id());
236 metrics.finished_tree_updates =
237 Some(metrics.started_tree_updates.unwrap().elapsed())
238 }
239 PluginEvent::StartedPollingTasks { window, .. } => {
240 self.get_metrics(window.id()).started_tasks_poll = Some(Instant::now())
241 }
242 PluginEvent::FinishedPollingTasks { window, .. } => {
243 let metrics = self.get_metrics(window.id());
244 if let Some(started) = metrics.started_tasks_poll.take() {
245 metrics.tasks_poll_time += started.elapsed();
246 }
247 if self.enabled {
248 handle.send_event_loop_event(NativeEvent::Window(NativeWindowEvent {
249 window_id: window.id(),
250 action: NativeWindowEventAction::User(UserEvent::RequestRedraw),
251 }));
252 }
253 }
254 PluginEvent::StartedMeasuringEvents { window, .. } => {
255 self.get_metrics(window.id()).started_events = Some(Instant::now())
256 }
257 PluginEvent::FinishedMeasuringEvents { window, .. } => {
258 let metrics = self.get_metrics(window.id());
259 if let Some(started) = metrics.started_events.take() {
260 metrics.events_time += started.elapsed();
261 }
262 if self.enabled {
263 handle.send_event_loop_event(NativeEvent::Window(NativeWindowEvent {
264 window_id: window.id(),
265 action: NativeWindowEventAction::User(UserEvent::RequestRedraw),
266 }));
267 }
268 }
269 PluginEvent::BeforeAccessibility { window, .. } => {
270 self.get_metrics(window.id()).started_accessibility_updates = Some(Instant::now())
271 }
272 PluginEvent::AfterAccessibility { window, .. } => {
273 let metrics = self.get_metrics(window.id());
274 metrics.finished_accessibility_updates =
275 Some(metrics.started_accessibility_updates.unwrap().elapsed())
276 }
277 PluginEvent::BeforeRender { window, .. } => {
278 let metrics = self.get_metrics(window.id());
279 metrics.started_redraw.get_or_insert(Instant::now());
280 metrics.started_render = Some(Instant::now());
281 }
282 PluginEvent::AfterRender {
283 window,
284 canvas,
285 font_collection,
286 tree,
287 animation_clock,
288 } => {
289 if !self.enabled {
290 return;
291 }
292 let metrics = self.get_metrics(window.id());
293 let scale_factor = window.scale_factor() as f32;
294 let started_render = metrics.started_render.take().unwrap();
295
296 canvas.save();
297 canvas.scale((scale_factor, scale_factor));
298
299 let finished_render = started_render.elapsed();
300 let finished_presenting = metrics.finished_presenting.unwrap_or_default();
301 let finished_layout = metrics.finished_layout.unwrap_or_default();
302 let finished_tree_updates = metrics.finished_tree_updates.unwrap_or_default();
303 let tasks_poll_time = metrics.tasks_poll_time;
304 let events_time = metrics.events_time;
305 let finished_accessibility_updates =
306 metrics.finished_accessibility_updates.unwrap_or_default();
307 let overlay_time = metrics.overlay_time;
308 let overlay_started = Instant::now();
309
310 let mut fps_paragraph_builder =
311 ParagraphBuilder::new(&ParagraphStyle::default(), *font_collection);
312 add_text(
313 &mut fps_paragraph_builder,
314 format!("{} FPS", metrics.frames.len()),
315 24.0,
316 );
317 let mut fps_paragraph = fps_paragraph_builder.build();
318 fps_paragraph.layout(235.0);
319
320 let rows = [
321 (
322 "Rendering",
323 format!("{:.3}ms", finished_render.as_secs_f64() * 1000.0),
324 ),
325 (
326 "Presenting",
327 format!("{:.3}ms", finished_presenting.as_secs_f64() * 1000.0),
328 ),
329 (
330 "Layout",
331 format!("{:.3}ms", finished_layout.as_secs_f64() * 1000.0),
332 ),
333 (
334 "Tree Updates",
335 format!("{:.3}ms", finished_tree_updates.as_secs_f64() * 1000.0),
336 ),
337 (
338 "a11y Updates",
339 format!(
340 "{:.3}ms",
341 finished_accessibility_updates.as_secs_f64() * 1000.0
342 ),
343 ),
344 (
345 "Tasks",
346 format!("{:.3}ms", tasks_poll_time.as_secs_f64() * 1000.0),
347 ),
348 (
349 "Events",
350 format!("{:.3}ms", events_time.as_secs_f64() * 1000.0),
351 ),
352 (
353 "Overlay",
354 format!("{:.3}ms", overlay_time.as_secs_f64() * 1000.0),
355 ),
356 (
357 "Frame",
358 format!(
359 "{:.3}ms",
360 (finished_render
361 + finished_presenting
362 + finished_layout
363 + finished_tree_updates
364 + tasks_poll_time
365 + events_time
366 + finished_accessibility_updates
367 + overlay_time)
368 .as_secs_f64()
369 * 1000.0
370 ),
371 ),
372 ("Tree Nodes", tree.size().to_string()),
373 ("Layout Nodes", tree.layout.size().to_string()),
374 ("Scale Factor", format!("{}x", window.scale_factor())),
375 (
376 "Animation clock speed",
377 format!("{}x", animation_clock.speed()),
378 ),
379 ("Renderer", metrics.graphics_driver.to_string()),
380 ("Freya", env!("CARGO_PKG_VERSION").to_string()),
381 (
382 "Build",
383 (if cfg!(debug_assertions) {
384 "Debug"
385 } else {
386 "Release"
387 })
388 .to_string(),
389 ),
390 ];
391
392 let mut keys_paragraph_builder =
393 ParagraphBuilder::new(&ParagraphStyle::default(), *font_collection);
394 let mut values_style = ParagraphStyle::default();
395 values_style.set_text_align(TextAlign::Right);
396 let mut values_paragraph_builder =
397 ParagraphBuilder::new(&values_style, *font_collection);
398 for (key, value) in &rows {
399 add_text(&mut keys_paragraph_builder, format!("{key}\n"), 14.0);
400 add_text(&mut values_paragraph_builder, format!("{value}\n"), 14.0);
401 }
402 let mut keys_paragraph = keys_paragraph_builder.build();
403 keys_paragraph.layout(235.0);
404 let mut values_paragraph = values_paragraph_builder.build();
405 values_paragraph.layout(235.0);
406
407 let gpu_paragraph = metrics.gpu_name.as_ref().map(|gpu_name| {
408 let mut builder =
409 ParagraphBuilder::new(&ParagraphStyle::default(), *font_collection);
410 add_text(&mut builder, format!("GPU: {gpu_name}"), 14.0);
411 let mut paragraph = builder.build();
412 paragraph.layout(235.0);
413 paragraph
414 });
415
416 let graph_left = 40.0;
417 let graph_top = fps_paragraph.height() + 10.0;
418 let graph_bottom = graph_top + 60.0;
419
420 let rows_top = graph_bottom + 22.0;
421 let gpu_top = rows_top + keys_paragraph.height() + 4.0;
422 let content_bottom = gpu_paragraph
423 .as_ref()
424 .map(|paragraph| gpu_top + paragraph.height())
425 .unwrap_or(rows_top + keys_paragraph.height());
426
427 let mut paint = Paint::default();
428 paint.set_anti_alias(true);
429 paint.set_style(PaintStyle::Fill);
430 paint.set_color(Color::from_argb(235, 24, 24, 24));
431 canvas.draw_rect(Rect::new(5., 5., 245.0, content_bottom + 10.0), &paint);
432
433 fps_paragraph.paint(canvas, (5.0, 0.0));
434 keys_paragraph.paint(canvas, (5.0, rows_top));
435 values_paragraph.paint(canvas, (5.0, rows_top));
436 if let Some(paragraph) = &gpu_paragraph {
437 paragraph.paint(canvas, (5.0, gpu_top));
438 }
439
440 let scale_max = metrics.graph_scale_max();
441
442 let mut axis_paint = Paint::default();
443 axis_paint.set_anti_alias(true);
444 axis_paint.set_style(PaintStyle::Stroke);
445 axis_paint.set_stroke_width(1.0);
446 axis_paint.set_color(Color::from_rgb(130, 130, 130));
447
448 canvas.draw_line(
449 (graph_left, graph_top),
450 (graph_left, graph_bottom),
451 &axis_paint,
452 );
453 canvas.draw_line(
454 (graph_left, graph_bottom),
455 (graph_left + 195.0, graph_bottom),
456 &axis_paint,
457 );
458
459 let decimals = if scale_max < 10.0 { 1 } else { 0 };
460 for (value, y) in [
461 (scale_max, graph_top),
462 (scale_max / 2.0, graph_top + 30.0),
463 (0.0, graph_bottom),
464 ] {
465 canvas.draw_line((graph_left - 3.0, y), (graph_left, y), &axis_paint);
466 draw_axis_label(
467 canvas,
468 font_collection,
469 &format!("{value:.decimals$}ms"),
470 7.0,
471 y - 6.0,
472 );
473 }
474
475 draw_axis_label(
476 canvas,
477 font_collection,
478 &format!("last {} frames", metrics.frame_times.len()),
479 graph_left + 62.5,
480 graph_bottom + 4.0,
481 );
482
483 let mut line_paint = Paint::default();
484 line_paint.set_anti_alias(true);
485 line_paint.set_style(PaintStyle::Stroke);
486 line_paint.set_stroke_width(1.5);
487 line_paint.set_color(Color::from_rgb(255, 204, 92));
488
489 let step = 195.0 / (FRAME_TIME_SAMPLES - 1) as f32;
490 let point = |index: usize, frame_time: f32| {
491 let x = graph_left + index as f32 * step;
492 let y = graph_bottom - (frame_time / scale_max).min(1.0) * 60.0;
493 (x, y)
494 };
495 for (index, window) in metrics.frame_times.windows(2).enumerate() {
496 canvas.draw_line(
497 point(index, window[0]),
498 point(index + 1, window[1]),
499 &line_paint,
500 );
501 }
502
503 metrics.overlay_time = overlay_started.elapsed();
504
505 canvas.restore();
506 }
507 _ => {}
508 }
509 }
510}
511
512fn nice_scale_max(value: f32) -> f32 {
514 if value <= 0.0 {
515 return 1.0;
516 }
517 let magnitude = 10f32.powf(value.log10().floor());
518 let fraction = value / magnitude;
519 let nice_fraction = if fraction <= 1.0 {
520 1.0
521 } else if fraction <= 2.0 {
522 2.0
523 } else if fraction <= 5.0 {
524 5.0
525 } else {
526 10.0
527 };
528 nice_fraction * magnitude
529}
530
531fn draw_axis_label(canvas: &Canvas, font_collection: &FontCollection, text: &str, x: f32, y: f32) {
532 let mut paragraph_builder = ParagraphBuilder::new(&ParagraphStyle::default(), font_collection);
533 let mut text_style = TextStyle::default();
534 text_style.set_color(Color::from_rgb(170, 170, 170));
535 text_style.set_font_size(10.0);
536 paragraph_builder.push_style(&text_style);
537 paragraph_builder.add_text(text);
538 let mut paragraph = paragraph_builder.build();
539 paragraph.layout(90.0);
540 paragraph.paint(canvas, (x, y));
541}
542
543fn add_text(paragraph_builder: &mut ParagraphBuilder, text: String, font_size: f32) {
544 let mut text_style = TextStyle::default();
545 text_style.set_color(Color::from_rgb(255, 204, 92));
546 let font_style = FontStyle::new(Weight::BOLD, Width::NORMAL, Slant::Upright);
547 text_style.set_font_style(font_style);
548 text_style.set_font_size(font_size);
549 paragraph_builder.push_style(&text_style);
550 paragraph_builder.add_text(text);
551}