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