freya_performance_plugin/
lib.rs1use 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#[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 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 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 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 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 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 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 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 add_text(
232 &mut paragraph_builder,
233 format!("{} Tree Nodes \n", tree.size()),
234 14.0,
235 );
236
237 add_text(
239 &mut paragraph_builder,
240 format!("{} Layout Nodes \n", tree.layout.size()),
241 14.0,
242 );
243
244 add_text(
246 &mut paragraph_builder,
247 format!("Scale Factor: {}x\n", window.scale_factor()),
248 14.0,
249 );
250
251 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}