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::GraphicsDriverChanged {
141                window,
142                graphics_driver,
143            } => {
144                self.get_metrics(window.id()).graphics_driver = graphics_driver;
145            }
146            PluginEvent::AfterRedraw { window, .. } => {
147                let metrics = self.get_metrics(window.id());
148                let now = Instant::now();
149
150                metrics
151                    .frames
152                    .retain(|frame| now.duration_since(*frame).as_millis() < 1000);
153
154                metrics.frames.push(now);
155            }
156            PluginEvent::BeforePresenting { window, .. } => {
157                self.get_metrics(window.id()).started_presenting = Some(Instant::now())
158            }
159            PluginEvent::AfterPresenting { window, .. } => {
160                let metrics = self.get_metrics(window.id());
161                metrics.finished_presenting = Some(metrics.started_presenting.unwrap().elapsed())
162            }
163            PluginEvent::StartedMeasuringLayout { window, .. } => {
164                self.get_metrics(window.id()).started_layout = Some(Instant::now())
165            }
166            PluginEvent::FinishedMeasuringLayout { window, .. } => {
167                let metrics = self.get_metrics(window.id());
168                metrics.finished_layout = Some(metrics.started_layout.unwrap().elapsed())
169            }
170            PluginEvent::StartedUpdatingTree { window, .. } => {
171                self.get_metrics(window.id()).started_tree_updates = Some(Instant::now())
172            }
173            PluginEvent::FinishedUpdatingTree { window, .. } => {
174                let metrics = self.get_metrics(window.id());
175                metrics.finished_tree_updates =
176                    Some(metrics.started_tree_updates.unwrap().elapsed())
177            }
178            PluginEvent::BeforeAccessibility { window, .. } => {
179                self.get_metrics(window.id()).started_accessibility_updates = Some(Instant::now())
180            }
181            PluginEvent::AfterAccessibility { window, .. } => {
182                let metrics = self.get_metrics(window.id());
183                metrics.finished_accessibility_updates =
184                    Some(metrics.started_accessibility_updates.unwrap().elapsed())
185            }
186            PluginEvent::BeforeRender { window, .. } => {
187                self.get_metrics(window.id()).started_render = Some(Instant::now())
188            }
189            PluginEvent::AfterRender {
190                window,
191                canvas,
192                font_collection,
193                tree,
194                animation_clock,
195            } => {
196                if !self.enabled {
197                    return;
198                }
199                let metrics = self.get_metrics(window.id());
200                let scale_factor = window.scale_factor() as f32;
201                let started_render = metrics.started_render.take().unwrap();
202
203                canvas.save();
204                canvas.scale((scale_factor, scale_factor));
205
206                let finished_render = started_render.elapsed();
207                let finished_presenting = metrics.finished_presenting.unwrap_or_default();
208                let finished_layout = metrics.finished_layout.unwrap();
209                let finished_tree_updates = metrics.finished_tree_updates.unwrap_or_default();
210                let finished_accessibility_updates =
211                    metrics.finished_accessibility_updates.unwrap_or_default();
212
213                let mut paint = Paint::default();
214                paint.set_anti_alias(true);
215                paint.set_style(PaintStyle::Fill);
216                paint.set_color(Color::from_argb(225, 225, 225, 225));
217
218                canvas.draw_rect(Rect::new(5., 5., 220., 440.), &paint);
219
220                // Render the texts
221                let mut paragraph_builder =
222                    ParagraphBuilder::new(&ParagraphStyle::default(), *font_collection);
223                let mut text_style = TextStyle::default();
224                text_style.set_color(Color::from_rgb(63, 255, 0));
225                text_style.add_shadow(TextShadow::new(
226                    Color::from_rgb(60, 60, 60),
227                    (0.0, 1.0),
228                    1.0,
229                ));
230                paragraph_builder.push_style(&text_style);
231
232                // FPS
233                add_text(
234                    &mut paragraph_builder,
235                    format!("{} FPS\n", metrics.frames.len()),
236                    30.0,
237                );
238
239                metrics.fps_historic.push(metrics.frames.len());
240                if metrics.fps_historic.len() > 70 {
241                    metrics.fps_historic.remove(0);
242                }
243
244                // Rendering time
245                add_text(
246                    &mut paragraph_builder,
247                    format!(
248                        "Rendering: {:.3}ms \n",
249                        finished_render.as_secs_f64() * 1000.0
250                    ),
251                    18.0,
252                );
253
254                // Presenting time
255                add_text(
256                    &mut paragraph_builder,
257                    format!(
258                        "Presenting: {:.3}ms \n",
259                        finished_presenting.as_secs_f64() * 1000.0
260                    ),
261                    18.0,
262                );
263
264                // Layout time
265                add_text(
266                    &mut paragraph_builder,
267                    format!("Layout: {:.3}ms \n", finished_layout.as_secs_f64() * 1000.0),
268                    18.0,
269                );
270
271                // Tree updates time
272                add_text(
273                    &mut paragraph_builder,
274                    format!(
275                        "Tree Updates: {:.3}ms \n",
276                        finished_tree_updates.as_secs_f64() * 1000.0
277                    ),
278                    18.0,
279                );
280
281                // Tree updates time
282                add_text(
283                    &mut paragraph_builder,
284                    format!(
285                        "a11y Updates: {:.3}ms \n",
286                        finished_accessibility_updates.as_secs_f64() * 1000.0
287                    ),
288                    18.0,
289                );
290
291                // Tree size
292                add_text(
293                    &mut paragraph_builder,
294                    format!("{} Tree Nodes \n", tree.size()),
295                    14.0,
296                );
297
298                // Layout size
299                add_text(
300                    &mut paragraph_builder,
301                    format!("{} Layout Nodes \n", tree.layout.size()),
302                    14.0,
303                );
304
305                // Scale Factor
306                add_text(
307                    &mut paragraph_builder,
308                    format!("Scale Factor: {}x\n", window.scale_factor()),
309                    14.0,
310                );
311
312                // TODO: Also track events measurement
313
314                // Animation clock speed
315                add_text(
316                    &mut paragraph_builder,
317                    format!("Animation clock speed: {}x \n", animation_clock.speed()),
318                    14.0,
319                );
320
321                // Graphics driver
322                add_text(
323                    &mut paragraph_builder,
324                    format!("Graphics: {} \n", metrics.graphics_driver),
325                    14.0,
326                );
327
328                let mut paragraph = paragraph_builder.build();
329                paragraph.layout(f32::MAX);
330                paragraph.paint(canvas, (5.0, 0.0));
331
332                metrics.max_fps = metrics.max_fps.max(
333                    metrics
334                        .fps_historic
335                        .iter()
336                        .max()
337                        .copied()
338                        .unwrap_or_default(),
339                );
340                let start_x = 5.0;
341                let start_y = 290.0 + metrics.max_fps.max(60) as f32;
342
343                for (i, fps) in metrics.fps_historic.iter().enumerate() {
344                    let mut paint = Paint::default();
345                    paint.set_anti_alias(true);
346                    paint.set_style(PaintStyle::Fill);
347                    paint.set_color(Color::from_rgb(63, 255, 0));
348                    paint.set_stroke_width(3.0);
349
350                    let x = start_x + (i * 2) as f32;
351                    let y = start_y - *fps as f32 + 2.0;
352                    canvas.draw_circle((x, y), 2.0, &paint);
353                }
354
355                canvas.restore();
356            }
357            _ => {}
358        }
359    }
360}
361
362fn add_text(paragraph_builder: &mut ParagraphBuilder, text: String, font_size: f32) {
363    let mut text_style = TextStyle::default();
364    text_style.set_color(Color::from_rgb(25, 225, 35));
365    let font_style = FontStyle::new(Weight::BOLD, Width::EXPANDED, Slant::Upright);
366    text_style.set_font_style(font_style);
367    text_style.add_shadow(TextShadow::new(
368        Color::from_rgb(65, 65, 65),
369        (0.0, 1.0),
370        1.0,
371    ));
372    text_style.set_font_size(font_size);
373    paragraph_builder.push_style(&text_style);
374    paragraph_builder.add_text(text);
375}