Skip to main content

freya_performance_plugin/
lib.rs

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    Color,
15    FontStyle,
16    Paint,
17    PaintStyle,
18    ParagraphBuilder,
19    ParagraphStyle,
20    Rect,
21    Slant,
22    TextShadow,
23    TextStyle,
24    Weight,
25    Width,
26};
27use freya_winit::{
28    plugins::{
29        FreyaPlugin,
30        Key,
31        Modifiers,
32        PluginEvent,
33        PluginHandle,
34    },
35    reexports::winit::window::WindowId,
36    renderer::{
37        NativeEvent,
38        NativeWindowEvent,
39        NativeWindowEventAction,
40    },
41};
42
43/// Performance overlay plugin that displays FPS, timing metrics, and other
44/// diagnostics on top of the rendered frame. Hidden by default, toggle with
45/// Ctrl+Shift+P (Cmd+Shift+P on macOS).
46pub struct PerformanceOverlayPlugin {
47    enabled: bool,
48    toggle_shortcut: (Key, Modifiers),
49    metrics: HashMap<WindowId, WindowMetrics>,
50}
51
52impl Default for PerformanceOverlayPlugin {
53    fn default() -> Self {
54        Self {
55            enabled: false,
56            toggle_shortcut: (
57                Key::Character("p".into()),
58                Modifiers::ctrl_or_meta() | Modifiers::SHIFT,
59            ),
60            metrics: HashMap::new(),
61        }
62    }
63}
64
65#[derive(Default)]
66struct WindowMetrics {
67    graphics_driver: &'static str,
68    gpu_name: Option<String>,
69
70    frames: Vec<Instant>,
71    fps_historic: Vec<usize>,
72    max_fps: usize,
73
74    started_render: Option<Instant>,
75
76    started_layout: Option<Instant>,
77    finished_layout: Option<Duration>,
78
79    started_tree_updates: Option<Instant>,
80    finished_tree_updates: Option<Duration>,
81
82    started_tasks_poll: Option<Instant>,
83    tasks_poll_time: Duration,
84
85    started_accessibility_updates: Option<Instant>,
86    finished_accessibility_updates: Option<Duration>,
87
88    started_presenting: Option<Instant>,
89    finished_presenting: Option<Duration>,
90}
91
92impl PerformanceOverlayPlugin {
93    /// Set the keyboard shortcut that toggles the overlay visibility.
94    pub fn with_toggle_shortcut(mut self, key: Key, modifiers: Modifiers) -> Self {
95        self.toggle_shortcut = (key, modifiers);
96        self
97    }
98
99    /// Set whether the overlay is visible by default.
100    pub fn with_visible(mut self, visible: bool) -> Self {
101        self.enabled = visible;
102        self
103    }
104
105    fn get_metrics(&mut self, id: WindowId) -> &mut WindowMetrics {
106        self.metrics.entry(id).or_default()
107    }
108}
109
110impl FreyaPlugin for PerformanceOverlayPlugin {
111    fn plugin_id(&self) -> &'static str {
112        "freya-performance-overlay"
113    }
114
115    fn on_event(&mut self, event: &mut PluginEvent, handle: PluginHandle) {
116        match event {
117            PluginEvent::KeyboardInput {
118                window,
119                key,
120                modifiers,
121                is_pressed,
122                ..
123            } => {
124                let (shortcut_key, shortcut_modifiers) = &self.toggle_shortcut;
125                let key_matches = match (key, shortcut_key) {
126                    (Key::Character(a), Key::Character(b)) => a.eq_ignore_ascii_case(b),
127                    (a, b) => a == b,
128                };
129                if *is_pressed && *modifiers == *shortcut_modifiers && key_matches {
130                    self.enabled = !self.enabled;
131                    handle.send_event_loop_event(NativeEvent::Window(NativeWindowEvent {
132                        window_id: window.id(),
133                        action: NativeWindowEventAction::User(UserEvent::RequestRedraw),
134                    }));
135                }
136            }
137            PluginEvent::WindowCreated {
138                window,
139                graphics_driver,
140                gpu_name,
141                ..
142            }
143            | PluginEvent::GraphicsDriverChanged {
144                window,
145                graphics_driver,
146                gpu_name,
147            } => {
148                let metrics = self.get_metrics(window.id());
149                metrics.graphics_driver = graphics_driver;
150                metrics.gpu_name = gpu_name.map(str::to_string);
151            }
152            PluginEvent::AfterRedraw { window, .. } => {
153                let metrics = self.get_metrics(window.id());
154                let now = Instant::now();
155
156                metrics
157                    .frames
158                    .retain(|frame| now.duration_since(*frame).as_millis() < 1000);
159
160                metrics.frames.push(now);
161
162                // Accumulated across the frame, so it needs a reset
163                metrics.tasks_poll_time = Duration::ZERO;
164            }
165            PluginEvent::BeforePresenting { window, .. } => {
166                self.get_metrics(window.id()).started_presenting = Some(Instant::now())
167            }
168            PluginEvent::AfterPresenting { window, .. } => {
169                let metrics = self.get_metrics(window.id());
170                metrics.finished_presenting = Some(metrics.started_presenting.unwrap().elapsed())
171            }
172            PluginEvent::StartedMeasuringLayout { window, .. } => {
173                self.get_metrics(window.id()).started_layout = Some(Instant::now())
174            }
175            PluginEvent::FinishedMeasuringLayout { window, .. } => {
176                let metrics = self.get_metrics(window.id());
177                metrics.finished_layout = Some(metrics.started_layout.unwrap().elapsed())
178            }
179            PluginEvent::StartedUpdatingTree { window, .. } => {
180                self.get_metrics(window.id()).started_tree_updates = Some(Instant::now())
181            }
182            PluginEvent::FinishedUpdatingTree { window, .. } => {
183                let metrics = self.get_metrics(window.id());
184                metrics.finished_tree_updates =
185                    Some(metrics.started_tree_updates.unwrap().elapsed())
186            }
187            PluginEvent::StartedPollingTasks { window, .. } => {
188                self.get_metrics(window.id()).started_tasks_poll = Some(Instant::now())
189            }
190            PluginEvent::FinishedPollingTasks { window, .. } => {
191                let metrics = self.get_metrics(window.id());
192                if let Some(started) = metrics.started_tasks_poll.take() {
193                    metrics.tasks_poll_time += started.elapsed();
194                }
195                if self.enabled {
196                    handle.send_event_loop_event(NativeEvent::Window(NativeWindowEvent {
197                        window_id: window.id(),
198                        action: NativeWindowEventAction::User(UserEvent::RequestRedraw),
199                    }));
200                }
201            }
202            PluginEvent::BeforeAccessibility { window, .. } => {
203                self.get_metrics(window.id()).started_accessibility_updates = Some(Instant::now())
204            }
205            PluginEvent::AfterAccessibility { window, .. } => {
206                let metrics = self.get_metrics(window.id());
207                metrics.finished_accessibility_updates =
208                    Some(metrics.started_accessibility_updates.unwrap().elapsed())
209            }
210            PluginEvent::BeforeRender { window, .. } => {
211                self.get_metrics(window.id()).started_render = Some(Instant::now())
212            }
213            PluginEvent::AfterRender {
214                window,
215                canvas,
216                font_collection,
217                tree,
218                animation_clock,
219            } => {
220                if !self.enabled {
221                    return;
222                }
223                let metrics = self.get_metrics(window.id());
224                let scale_factor = window.scale_factor() as f32;
225                let started_render = metrics.started_render.take().unwrap();
226
227                canvas.save();
228                canvas.scale((scale_factor, scale_factor));
229
230                let finished_render = started_render.elapsed();
231                let finished_presenting = metrics.finished_presenting.unwrap_or_default();
232                let finished_layout = metrics.finished_layout.unwrap();
233                let finished_tree_updates = metrics.finished_tree_updates.unwrap_or_default();
234                let tasks_poll_time = metrics.tasks_poll_time;
235                let finished_accessibility_updates =
236                    metrics.finished_accessibility_updates.unwrap_or_default();
237
238                // Render the texts
239                let mut paragraph_builder =
240                    ParagraphBuilder::new(&ParagraphStyle::default(), *font_collection);
241                let mut text_style = TextStyle::default();
242                text_style.set_color(Color::from_rgb(63, 255, 0));
243                text_style.add_shadow(TextShadow::new(
244                    Color::from_rgb(60, 60, 60),
245                    (0.0, 1.0),
246                    1.0,
247                ));
248                paragraph_builder.push_style(&text_style);
249
250                // FPS
251                add_text(
252                    &mut paragraph_builder,
253                    format!("{} FPS\n", metrics.frames.len()),
254                    30.0,
255                );
256
257                metrics.fps_historic.push(metrics.frames.len());
258                if metrics.fps_historic.len() > 70 {
259                    metrics.fps_historic.remove(0);
260                }
261
262                // Rendering time
263                add_text(
264                    &mut paragraph_builder,
265                    format!(
266                        "Rendering: {:.3}ms \n",
267                        finished_render.as_secs_f64() * 1000.0
268                    ),
269                    18.0,
270                );
271
272                // Presenting time
273                add_text(
274                    &mut paragraph_builder,
275                    format!(
276                        "Presenting: {:.3}ms \n",
277                        finished_presenting.as_secs_f64() * 1000.0
278                    ),
279                    18.0,
280                );
281
282                // Layout time
283                add_text(
284                    &mut paragraph_builder,
285                    format!("Layout: {:.3}ms \n", finished_layout.as_secs_f64() * 1000.0),
286                    18.0,
287                );
288
289                // Tree updates time
290                add_text(
291                    &mut paragraph_builder,
292                    format!(
293                        "Tree Updates: {:.3}ms \n",
294                        finished_tree_updates.as_secs_f64() * 1000.0
295                    ),
296                    18.0,
297                );
298
299                // a11y updates time
300                add_text(
301                    &mut paragraph_builder,
302                    format!(
303                        "a11y Updates: {:.3}ms \n",
304                        finished_accessibility_updates.as_secs_f64() * 1000.0
305                    ),
306                    18.0,
307                );
308
309                // Async tasks polling time
310                add_text(
311                    &mut paragraph_builder,
312                    format!("Tasks: {:.3}ms \n", tasks_poll_time.as_secs_f64() * 1000.0),
313                    18.0,
314                );
315
316                // Tree size
317                add_text(
318                    &mut paragraph_builder,
319                    format!("{} Tree Nodes \n", tree.size()),
320                    14.0,
321                );
322
323                // Layout size
324                add_text(
325                    &mut paragraph_builder,
326                    format!("{} Layout Nodes \n", tree.layout.size()),
327                    14.0,
328                );
329
330                // Scale Factor
331                add_text(
332                    &mut paragraph_builder,
333                    format!("Scale Factor: {}x\n", window.scale_factor()),
334                    14.0,
335                );
336
337                // TODO: Also track events measurement
338
339                // Animation clock speed
340                add_text(
341                    &mut paragraph_builder,
342                    format!("Animation clock speed: {}x \n", animation_clock.speed()),
343                    14.0,
344                );
345
346                // Graphics driver
347                add_text(
348                    &mut paragraph_builder,
349                    format!("Graphics: {} \n", metrics.graphics_driver),
350                    14.0,
351                );
352
353                // Picked GPU
354                if let Some(gpu_name) = &metrics.gpu_name {
355                    add_text(&mut paragraph_builder, format!("GPU: {gpu_name} \n"), 14.0);
356                }
357
358                let mut paragraph = paragraph_builder.build();
359                paragraph.layout(235.0);
360
361                metrics.max_fps = metrics.max_fps.max(
362                    metrics
363                        .fps_historic
364                        .iter()
365                        .max()
366                        .copied()
367                        .unwrap_or_default(),
368                );
369
370                let start_x = 5.0;
371                let start_y = paragraph.height() + 20.0 + metrics.max_fps.max(60) as f32;
372
373                let mut paint = Paint::default();
374                paint.set_anti_alias(true);
375                paint.set_style(PaintStyle::Fill);
376                paint.set_color(Color::from_argb(225, 225, 225, 225));
377                canvas.draw_rect(Rect::new(5., 5., 245.0, start_y + 15.0), &paint);
378
379                paragraph.paint(canvas, (5.0, 0.0));
380
381                for (i, fps) in metrics.fps_historic.iter().enumerate() {
382                    let mut paint = Paint::default();
383                    paint.set_anti_alias(true);
384                    paint.set_style(PaintStyle::Fill);
385                    paint.set_color(Color::from_rgb(63, 255, 0));
386                    paint.set_stroke_width(3.0);
387
388                    let x = start_x + (i * 2) as f32;
389                    let y = start_y - *fps as f32 + 2.0;
390                    canvas.draw_circle((x, y), 2.0, &paint);
391                }
392
393                canvas.restore();
394            }
395            _ => {}
396        }
397    }
398}
399
400fn add_text(paragraph_builder: &mut ParagraphBuilder, text: String, font_size: f32) {
401    let mut text_style = TextStyle::default();
402    text_style.set_color(Color::from_rgb(25, 225, 35));
403    let font_style = FontStyle::new(Weight::BOLD, Width::EXPANDED, Slant::Upright);
404    text_style.set_font_style(font_style);
405    text_style.add_shadow(TextShadow::new(
406        Color::from_rgb(65, 65, 65),
407        (0.0, 1.0),
408        1.0,
409    ));
410    text_style.set_font_size(font_size);
411    paragraph_builder.push_style(&text_style);
412    paragraph_builder.add_text(text);
413}