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_engine::prelude::{
10    Color,
11    FontStyle,
12    Paint,
13    PaintStyle,
14    ParagraphBuilder,
15    ParagraphStyle,
16    Rect,
17    Slant,
18    TextShadow,
19    TextStyle,
20    Weight,
21    Width,
22};
23use freya_winit::{
24    plugins::{
25        FreyaPlugin,
26        Key,
27        Modifiers,
28        PluginEvent,
29        PluginHandle,
30    },
31    reexports::winit::window::WindowId,
32};
33
34/// Performance overlay plugin that displays FPS, timing metrics, and other
35/// diagnostics on top of the rendered frame. Hidden by default, toggle with
36/// Ctrl+Shift+P (Cmd+Shift+P on macOS).
37#[derive(Default)]
38pub struct PerformanceOverlayPlugin {
39    enabled: bool,
40    metrics: HashMap<WindowId, WindowMetrics>,
41}
42
43#[derive(Default)]
44struct WindowMetrics {
45    frames: Vec<Instant>,
46    fps_historic: Vec<usize>,
47    max_fps: usize,
48
49    started_render: Option<Instant>,
50
51    started_layout: Option<Instant>,
52    finished_layout: Option<Duration>,
53
54    started_tree_updates: Option<Instant>,
55    finished_tree_updates: Option<Duration>,
56
57    started_accessibility_updates: Option<Instant>,
58    finished_accessibility_updates: Option<Duration>,
59
60    started_presenting: Option<Instant>,
61    finished_presenting: Option<Duration>,
62}
63
64impl PerformanceOverlayPlugin {
65    fn get_metrics(&mut self, id: WindowId) -> &mut WindowMetrics {
66        self.metrics.entry(id).or_default()
67    }
68}
69
70impl FreyaPlugin for PerformanceOverlayPlugin {
71    fn on_event(&mut self, event: &mut PluginEvent, _handle: PluginHandle) {
72        match event {
73            PluginEvent::KeyboardInput {
74                key,
75                modifiers,
76                is_pressed,
77                ..
78            } => {
79                let toggle_modifier = if cfg!(target_os = "macos") {
80                    Modifiers::META | Modifiers::SHIFT
81                } else {
82                    Modifiers::CONTROL | Modifiers::SHIFT
83                };
84                let is_p = matches!(key, Key::Character(c) if c.eq_ignore_ascii_case("p"));
85                if *is_pressed && is_p && *modifiers == toggle_modifier {
86                    self.enabled = !self.enabled;
87                }
88            }
89            PluginEvent::AfterRedraw { window, .. } => {
90                let metrics = self.get_metrics(window.id());
91                let now = Instant::now();
92
93                metrics
94                    .frames
95                    .retain(|frame| now.duration_since(*frame).as_millis() < 1000);
96
97                metrics.frames.push(now);
98            }
99            PluginEvent::BeforePresenting { window, .. } => {
100                self.get_metrics(window.id()).started_presenting = Some(Instant::now())
101            }
102            PluginEvent::AfterPresenting { window, .. } => {
103                let metrics = self.get_metrics(window.id());
104                metrics.finished_presenting = Some(metrics.started_presenting.unwrap().elapsed())
105            }
106            PluginEvent::StartedMeasuringLayout { window, .. } => {
107                self.get_metrics(window.id()).started_layout = Some(Instant::now())
108            }
109            PluginEvent::FinishedMeasuringLayout { window, .. } => {
110                let metrics = self.get_metrics(window.id());
111                metrics.finished_layout = Some(metrics.started_layout.unwrap().elapsed())
112            }
113            PluginEvent::StartedUpdatingTree { window, .. } => {
114                self.get_metrics(window.id()).started_tree_updates = Some(Instant::now())
115            }
116            PluginEvent::FinishedUpdatingTree { window, .. } => {
117                let metrics = self.get_metrics(window.id());
118                metrics.finished_tree_updates =
119                    Some(metrics.started_tree_updates.unwrap().elapsed())
120            }
121            PluginEvent::BeforeAccessibility { window, .. } => {
122                self.get_metrics(window.id()).started_accessibility_updates = Some(Instant::now())
123            }
124            PluginEvent::AfterAccessibility { window, .. } => {
125                let metrics = self.get_metrics(window.id());
126                metrics.finished_accessibility_updates =
127                    Some(metrics.started_accessibility_updates.unwrap().elapsed())
128            }
129            PluginEvent::BeforeRender { window, .. } => {
130                self.get_metrics(window.id()).started_render = Some(Instant::now())
131            }
132            PluginEvent::AfterRender {
133                window,
134                canvas,
135                font_collection,
136                tree,
137                animation_clock,
138            } => {
139                if !self.enabled {
140                    return;
141                }
142                let metrics = self.get_metrics(window.id());
143                let started_render = metrics.started_render.take().unwrap();
144
145                let finished_render = started_render.elapsed();
146                let finished_presenting = metrics.finished_presenting.unwrap_or_default();
147                let finished_layout = metrics.finished_layout.unwrap();
148                let finished_tree_updates = metrics.finished_tree_updates.unwrap_or_default();
149                let finished_accessibility_updates =
150                    metrics.finished_accessibility_updates.unwrap_or_default();
151
152                let mut paint = Paint::default();
153                paint.set_anti_alias(true);
154                paint.set_style(PaintStyle::Fill);
155                paint.set_color(Color::from_argb(225, 225, 225, 225));
156
157                canvas.draw_rect(Rect::new(5., 5., 220., 440.), &paint);
158
159                // Render the texts
160                let mut paragraph_builder =
161                    ParagraphBuilder::new(&ParagraphStyle::default(), *font_collection);
162                let mut text_style = TextStyle::default();
163                text_style.set_color(Color::from_rgb(63, 255, 0));
164                text_style.add_shadow(TextShadow::new(
165                    Color::from_rgb(60, 60, 60),
166                    (0.0, 1.0),
167                    1.0,
168                ));
169                paragraph_builder.push_style(&text_style);
170
171                // FPS
172                add_text(
173                    &mut paragraph_builder,
174                    format!("{} FPS\n", metrics.frames.len()),
175                    30.0,
176                );
177
178                metrics.fps_historic.push(metrics.frames.len());
179                if metrics.fps_historic.len() > 70 {
180                    metrics.fps_historic.remove(0);
181                }
182
183                // Rendering time
184                add_text(
185                    &mut paragraph_builder,
186                    format!(
187                        "Rendering: {:.3}ms \n",
188                        finished_render.as_secs_f64() * 1000.0
189                    ),
190                    18.0,
191                );
192
193                // Presenting time
194                add_text(
195                    &mut paragraph_builder,
196                    format!(
197                        "Presenting: {:.3}ms \n",
198                        finished_presenting.as_secs_f64() * 1000.0
199                    ),
200                    18.0,
201                );
202
203                // Layout time
204                add_text(
205                    &mut paragraph_builder,
206                    format!("Layout: {:.3}ms \n", finished_layout.as_secs_f64() * 1000.0),
207                    18.0,
208                );
209
210                // Tree updates time
211                add_text(
212                    &mut paragraph_builder,
213                    format!(
214                        "Tree Updates: {:.3}ms \n",
215                        finished_tree_updates.as_secs_f64() * 1000.0
216                    ),
217                    18.0,
218                );
219
220                // Tree updates time
221                add_text(
222                    &mut paragraph_builder,
223                    format!(
224                        "a11y Updates: {:.3}ms \n",
225                        finished_accessibility_updates.as_secs_f64() * 1000.0
226                    ),
227                    18.0,
228                );
229
230                // Tree size
231                add_text(
232                    &mut paragraph_builder,
233                    format!("{} Tree Nodes \n", tree.size()),
234                    14.0,
235                );
236
237                // Layout size
238                add_text(
239                    &mut paragraph_builder,
240                    format!("{} Layout Nodes \n", tree.layout.size()),
241                    14.0,
242                );
243
244                // Scale Factor
245                add_text(
246                    &mut paragraph_builder,
247                    format!("Scale Factor: {}x\n", window.scale_factor()),
248                    14.0,
249                );
250
251                // TODO: Also track events measurement
252
253                // Animation clock speed
254                add_text(
255                    &mut paragraph_builder,
256                    format!("Animation clock speed: {}x \n", animation_clock.speed()),
257                    14.0,
258                );
259
260                let mut paragraph = paragraph_builder.build();
261                paragraph.layout(f32::MAX);
262                paragraph.paint(canvas, (5.0, 0.0));
263
264                metrics.max_fps = metrics.max_fps.max(
265                    metrics
266                        .fps_historic
267                        .iter()
268                        .max()
269                        .copied()
270                        .unwrap_or_default(),
271                );
272                let start_x = 5.0;
273                let start_y = 290.0 + metrics.max_fps.max(60) as f32;
274
275                for (i, fps) in metrics.fps_historic.iter().enumerate() {
276                    let mut paint = Paint::default();
277                    paint.set_anti_alias(true);
278                    paint.set_style(PaintStyle::Fill);
279                    paint.set_color(Color::from_rgb(63, 255, 0));
280                    paint.set_stroke_width(3.0);
281
282                    let x = start_x + (i * 2) as f32;
283                    let y = start_y - *fps as f32 + 2.0;
284                    canvas.draw_circle((x, y), 2.0, &paint);
285                }
286            }
287            _ => {}
288        }
289    }
290}
291
292fn add_text(paragraph_builder: &mut ParagraphBuilder, text: String, font_size: f32) {
293    let mut text_style = TextStyle::default();
294    text_style.set_color(Color::from_rgb(25, 225, 35));
295    let font_style = FontStyle::new(Weight::BOLD, Width::EXPANDED, Slant::Upright);
296    text_style.set_font_style(font_style);
297    text_style.add_shadow(TextShadow::new(
298        Color::from_rgb(65, 65, 65),
299        (0.0, 1.0),
300        1.0,
301    ));
302    text_style.set_font_size(font_size);
303    paragraph_builder.push_style(&text_style);
304    paragraph_builder.add_text(text);
305}