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 scale_factor = window.scale_factor() as f32;
174                let started_render = metrics.started_render.take().unwrap();
175
176                canvas.save();
177                canvas.scale((scale_factor, scale_factor));
178
179                let finished_render = started_render.elapsed();
180                let finished_presenting = metrics.finished_presenting.unwrap_or_default();
181                let finished_layout = metrics.finished_layout.unwrap();
182                let finished_tree_updates = metrics.finished_tree_updates.unwrap_or_default();
183                let finished_accessibility_updates =
184                    metrics.finished_accessibility_updates.unwrap_or_default();
185
186                let mut paint = Paint::default();
187                paint.set_anti_alias(true);
188                paint.set_style(PaintStyle::Fill);
189                paint.set_color(Color::from_argb(225, 225, 225, 225));
190
191                canvas.draw_rect(Rect::new(5., 5., 220., 440.), &paint);
192
193                // Render the texts
194                let mut paragraph_builder =
195                    ParagraphBuilder::new(&ParagraphStyle::default(), *font_collection);
196                let mut text_style = TextStyle::default();
197                text_style.set_color(Color::from_rgb(63, 255, 0));
198                text_style.add_shadow(TextShadow::new(
199                    Color::from_rgb(60, 60, 60),
200                    (0.0, 1.0),
201                    1.0,
202                ));
203                paragraph_builder.push_style(&text_style);
204
205                // FPS
206                add_text(
207                    &mut paragraph_builder,
208                    format!("{} FPS\n", metrics.frames.len()),
209                    30.0,
210                );
211
212                metrics.fps_historic.push(metrics.frames.len());
213                if metrics.fps_historic.len() > 70 {
214                    metrics.fps_historic.remove(0);
215                }
216
217                // Rendering time
218                add_text(
219                    &mut paragraph_builder,
220                    format!(
221                        "Rendering: {:.3}ms \n",
222                        finished_render.as_secs_f64() * 1000.0
223                    ),
224                    18.0,
225                );
226
227                // Presenting time
228                add_text(
229                    &mut paragraph_builder,
230                    format!(
231                        "Presenting: {:.3}ms \n",
232                        finished_presenting.as_secs_f64() * 1000.0
233                    ),
234                    18.0,
235                );
236
237                // Layout time
238                add_text(
239                    &mut paragraph_builder,
240                    format!("Layout: {:.3}ms \n", finished_layout.as_secs_f64() * 1000.0),
241                    18.0,
242                );
243
244                // Tree updates time
245                add_text(
246                    &mut paragraph_builder,
247                    format!(
248                        "Tree Updates: {:.3}ms \n",
249                        finished_tree_updates.as_secs_f64() * 1000.0
250                    ),
251                    18.0,
252                );
253
254                // Tree updates time
255                add_text(
256                    &mut paragraph_builder,
257                    format!(
258                        "a11y Updates: {:.3}ms \n",
259                        finished_accessibility_updates.as_secs_f64() * 1000.0
260                    ),
261                    18.0,
262                );
263
264                // Tree size
265                add_text(
266                    &mut paragraph_builder,
267                    format!("{} Tree Nodes \n", tree.size()),
268                    14.0,
269                );
270
271                // Layout size
272                add_text(
273                    &mut paragraph_builder,
274                    format!("{} Layout Nodes \n", tree.layout.size()),
275                    14.0,
276                );
277
278                // Scale Factor
279                add_text(
280                    &mut paragraph_builder,
281                    format!("Scale Factor: {}x\n", window.scale_factor()),
282                    14.0,
283                );
284
285                // TODO: Also track events measurement
286
287                // Animation clock speed
288                add_text(
289                    &mut paragraph_builder,
290                    format!("Animation clock speed: {}x \n", animation_clock.speed()),
291                    14.0,
292                );
293
294                // Graphics driver
295                add_text(
296                    &mut paragraph_builder,
297                    format!("Graphics: {} \n", metrics.graphics_driver),
298                    14.0,
299                );
300
301                let mut paragraph = paragraph_builder.build();
302                paragraph.layout(f32::MAX);
303                paragraph.paint(canvas, (5.0, 0.0));
304
305                metrics.max_fps = metrics.max_fps.max(
306                    metrics
307                        .fps_historic
308                        .iter()
309                        .max()
310                        .copied()
311                        .unwrap_or_default(),
312                );
313                let start_x = 5.0;
314                let start_y = 290.0 + metrics.max_fps.max(60) as f32;
315
316                for (i, fps) in metrics.fps_historic.iter().enumerate() {
317                    let mut paint = Paint::default();
318                    paint.set_anti_alias(true);
319                    paint.set_style(PaintStyle::Fill);
320                    paint.set_color(Color::from_rgb(63, 255, 0));
321                    paint.set_stroke_width(3.0);
322
323                    let x = start_x + (i * 2) as f32;
324                    let y = start_y - *fps as f32 + 2.0;
325                    canvas.draw_circle((x, y), 2.0, &paint);
326                }
327
328                canvas.restore();
329            }
330            _ => {}
331        }
332    }
333}
334
335fn add_text(paragraph_builder: &mut ParagraphBuilder, text: String, font_size: f32) {
336    let mut text_style = TextStyle::default();
337    text_style.set_color(Color::from_rgb(25, 225, 35));
338    let font_style = FontStyle::new(Weight::BOLD, Width::EXPANDED, Slant::Upright);
339    text_style.set_font_style(font_style);
340    text_style.add_shadow(TextShadow::new(
341        Color::from_rgb(65, 65, 65),
342        (0.0, 1.0),
343        1.0,
344    ));
345    text_style.set_font_size(font_size);
346    paragraph_builder.push_style(&text_style);
347    paragraph_builder.add_text(text);
348}