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