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
69    frames: Vec<Instant>,
70    fps_historic: Vec<usize>,
71    max_fps: usize,
72
73    started_render: Option<Instant>,
74
75    started_layout: Option<Instant>,
76    finished_layout: Option<Duration>,
77
78    started_tree_updates: Option<Instant>,
79    finished_tree_updates: Option<Duration>,
80
81    started_accessibility_updates: Option<Instant>,
82    finished_accessibility_updates: Option<Duration>,
83
84    started_presenting: Option<Instant>,
85    finished_presenting: Option<Duration>,
86}
87
88impl PerformanceOverlayPlugin {
89    /// Set the keyboard shortcut that toggles the overlay visibility.
90    pub fn with_toggle_shortcut(mut self, key: Key, modifiers: Modifiers) -> Self {
91        self.toggle_shortcut = (key, modifiers);
92        self
93    }
94
95    /// Set whether the overlay is visible by default.
96    pub fn with_visible(mut self, visible: bool) -> Self {
97        self.enabled = visible;
98        self
99    }
100
101    fn get_metrics(&mut self, id: WindowId) -> &mut WindowMetrics {
102        self.metrics.entry(id).or_default()
103    }
104}
105
106impl FreyaPlugin for PerformanceOverlayPlugin {
107    fn plugin_id(&self) -> &'static str {
108        "freya-performance-overlay"
109    }
110
111    fn on_event(&mut self, event: &mut PluginEvent, handle: PluginHandle) {
112        match event {
113            PluginEvent::KeyboardInput {
114                window,
115                key,
116                modifiers,
117                is_pressed,
118                ..
119            } => {
120                let (shortcut_key, shortcut_modifiers) = &self.toggle_shortcut;
121                let key_matches = match (key, shortcut_key) {
122                    (Key::Character(a), Key::Character(b)) => a.eq_ignore_ascii_case(b),
123                    (a, b) => a == b,
124                };
125                if *is_pressed && *modifiers == *shortcut_modifiers && key_matches {
126                    self.enabled = !self.enabled;
127                    handle.send_event_loop_event(NativeEvent::Window(NativeWindowEvent {
128                        window_id: window.id(),
129                        action: NativeWindowEventAction::User(UserEvent::RequestRedraw),
130                    }));
131                }
132            }
133            PluginEvent::WindowCreated {
134                window,
135                graphics_driver,
136                ..
137            } => {
138                self.get_metrics(window.id()).graphics_driver = graphics_driver;
139            }
140            PluginEvent::AfterRedraw { window, .. } => {
141                let metrics = self.get_metrics(window.id());
142                let now = Instant::now();
143
144                metrics
145                    .frames
146                    .retain(|frame| now.duration_since(*frame).as_millis() < 1000);
147
148                metrics.frames.push(now);
149            }
150            PluginEvent::BeforePresenting { window, .. } => {
151                self.get_metrics(window.id()).started_presenting = Some(Instant::now())
152            }
153            PluginEvent::AfterPresenting { window, .. } => {
154                let metrics = self.get_metrics(window.id());
155                metrics.finished_presenting = Some(metrics.started_presenting.unwrap().elapsed())
156            }
157            PluginEvent::StartedMeasuringLayout { window, .. } => {
158                self.get_metrics(window.id()).started_layout = Some(Instant::now())
159            }
160            PluginEvent::FinishedMeasuringLayout { window, .. } => {
161                let metrics = self.get_metrics(window.id());
162                metrics.finished_layout = Some(metrics.started_layout.unwrap().elapsed())
163            }
164            PluginEvent::StartedUpdatingTree { window, .. } => {
165                self.get_metrics(window.id()).started_tree_updates = Some(Instant::now())
166            }
167            PluginEvent::FinishedUpdatingTree { window, .. } => {
168                let metrics = self.get_metrics(window.id());
169                metrics.finished_tree_updates =
170                    Some(metrics.started_tree_updates.unwrap().elapsed())
171            }
172            PluginEvent::BeforeAccessibility { window, .. } => {
173                self.get_metrics(window.id()).started_accessibility_updates = Some(Instant::now())
174            }
175            PluginEvent::AfterAccessibility { window, .. } => {
176                let metrics = self.get_metrics(window.id());
177                metrics.finished_accessibility_updates =
178                    Some(metrics.started_accessibility_updates.unwrap().elapsed())
179            }
180            PluginEvent::BeforeRender { window, .. } => {
181                self.get_metrics(window.id()).started_render = Some(Instant::now())
182            }
183            PluginEvent::AfterRender {
184                window,
185                canvas,
186                font_collection,
187                tree,
188                animation_clock,
189            } => {
190                if !self.enabled {
191                    return;
192                }
193                let metrics = self.get_metrics(window.id());
194                let scale_factor = window.scale_factor() as f32;
195                let started_render = metrics.started_render.take().unwrap();
196
197                canvas.save();
198                canvas.scale((scale_factor, scale_factor));
199
200                let finished_render = started_render.elapsed();
201                let finished_presenting = metrics.finished_presenting.unwrap_or_default();
202                let finished_layout = metrics.finished_layout.unwrap();
203                let finished_tree_updates = metrics.finished_tree_updates.unwrap_or_default();
204                let finished_accessibility_updates =
205                    metrics.finished_accessibility_updates.unwrap_or_default();
206
207                let mut paint = Paint::default();
208                paint.set_anti_alias(true);
209                paint.set_style(PaintStyle::Fill);
210                paint.set_color(Color::from_argb(225, 225, 225, 225));
211
212                canvas.draw_rect(Rect::new(5., 5., 220., 440.), &paint);
213
214                // Render the texts
215                let mut paragraph_builder =
216                    ParagraphBuilder::new(&ParagraphStyle::default(), *font_collection);
217                let mut text_style = TextStyle::default();
218                text_style.set_color(Color::from_rgb(63, 255, 0));
219                text_style.add_shadow(TextShadow::new(
220                    Color::from_rgb(60, 60, 60),
221                    (0.0, 1.0),
222                    1.0,
223                ));
224                paragraph_builder.push_style(&text_style);
225
226                // FPS
227                add_text(
228                    &mut paragraph_builder,
229                    format!("{} FPS\n", metrics.frames.len()),
230                    30.0,
231                );
232
233                metrics.fps_historic.push(metrics.frames.len());
234                if metrics.fps_historic.len() > 70 {
235                    metrics.fps_historic.remove(0);
236                }
237
238                // Rendering time
239                add_text(
240                    &mut paragraph_builder,
241                    format!(
242                        "Rendering: {:.3}ms \n",
243                        finished_render.as_secs_f64() * 1000.0
244                    ),
245                    18.0,
246                );
247
248                // Presenting time
249                add_text(
250                    &mut paragraph_builder,
251                    format!(
252                        "Presenting: {:.3}ms \n",
253                        finished_presenting.as_secs_f64() * 1000.0
254                    ),
255                    18.0,
256                );
257
258                // Layout time
259                add_text(
260                    &mut paragraph_builder,
261                    format!("Layout: {:.3}ms \n", finished_layout.as_secs_f64() * 1000.0),
262                    18.0,
263                );
264
265                // Tree updates time
266                add_text(
267                    &mut paragraph_builder,
268                    format!(
269                        "Tree Updates: {:.3}ms \n",
270                        finished_tree_updates.as_secs_f64() * 1000.0
271                    ),
272                    18.0,
273                );
274
275                // Tree updates time
276                add_text(
277                    &mut paragraph_builder,
278                    format!(
279                        "a11y Updates: {:.3}ms \n",
280                        finished_accessibility_updates.as_secs_f64() * 1000.0
281                    ),
282                    18.0,
283                );
284
285                // Tree size
286                add_text(
287                    &mut paragraph_builder,
288                    format!("{} Tree Nodes \n", tree.size()),
289                    14.0,
290                );
291
292                // Layout size
293                add_text(
294                    &mut paragraph_builder,
295                    format!("{} Layout Nodes \n", tree.layout.size()),
296                    14.0,
297                );
298
299                // Scale Factor
300                add_text(
301                    &mut paragraph_builder,
302                    format!("Scale Factor: {}x\n", window.scale_factor()),
303                    14.0,
304                );
305
306                // TODO: Also track events measurement
307
308                // Animation clock speed
309                add_text(
310                    &mut paragraph_builder,
311                    format!("Animation clock speed: {}x \n", animation_clock.speed()),
312                    14.0,
313                );
314
315                // Graphics driver
316                add_text(
317                    &mut paragraph_builder,
318                    format!("Graphics: {} \n", metrics.graphics_driver),
319                    14.0,
320                );
321
322                let mut paragraph = paragraph_builder.build();
323                paragraph.layout(f32::MAX);
324                paragraph.paint(canvas, (5.0, 0.0));
325
326                metrics.max_fps = metrics.max_fps.max(
327                    metrics
328                        .fps_historic
329                        .iter()
330                        .max()
331                        .copied()
332                        .unwrap_or_default(),
333                );
334                let start_x = 5.0;
335                let start_y = 290.0 + metrics.max_fps.max(60) as f32;
336
337                for (i, fps) in metrics.fps_historic.iter().enumerate() {
338                    let mut paint = Paint::default();
339                    paint.set_anti_alias(true);
340                    paint.set_style(PaintStyle::Fill);
341                    paint.set_color(Color::from_rgb(63, 255, 0));
342                    paint.set_stroke_width(3.0);
343
344                    let x = start_x + (i * 2) as f32;
345                    let y = start_y - *fps as f32 + 2.0;
346                    canvas.draw_circle((x, y), 2.0, &paint);
347                }
348
349                canvas.restore();
350            }
351            _ => {}
352        }
353    }
354}
355
356fn add_text(paragraph_builder: &mut ParagraphBuilder, text: String, font_size: f32) {
357    let mut text_style = TextStyle::default();
358    text_style.set_color(Color::from_rgb(25, 225, 35));
359    let font_style = FontStyle::new(Weight::BOLD, Width::EXPANDED, Slant::Upright);
360    text_style.set_font_style(font_style);
361    text_style.add_shadow(TextShadow::new(
362        Color::from_rgb(65, 65, 65),
363        (0.0, 1.0),
364        1.0,
365    ));
366    text_style.set_font_size(font_size);
367    paragraph_builder.push_style(&text_style);
368    paragraph_builder.add_text(text);
369}