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
43pub 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 gpu_name: Option<String>,
69
70 frames: Vec<Instant>,
71 fps_historic: Vec<usize>,
72 max_fps: usize,
73
74 started_render: Option<Instant>,
75
76 started_layout: Option<Instant>,
77 finished_layout: Option<Duration>,
78
79 started_tree_updates: Option<Instant>,
80 finished_tree_updates: Option<Duration>,
81
82 started_tasks_poll: Option<Instant>,
83 tasks_poll_time: Duration,
84
85 started_accessibility_updates: Option<Instant>,
86 finished_accessibility_updates: Option<Duration>,
87
88 started_presenting: Option<Instant>,
89 finished_presenting: Option<Duration>,
90}
91
92impl PerformanceOverlayPlugin {
93 pub fn with_toggle_shortcut(mut self, key: Key, modifiers: Modifiers) -> Self {
95 self.toggle_shortcut = (key, modifiers);
96 self
97 }
98
99 pub fn with_visible(mut self, visible: bool) -> Self {
101 self.enabled = visible;
102 self
103 }
104
105 fn get_metrics(&mut self, id: WindowId) -> &mut WindowMetrics {
106 self.metrics.entry(id).or_default()
107 }
108}
109
110impl FreyaPlugin for PerformanceOverlayPlugin {
111 fn plugin_id(&self) -> &'static str {
112 "freya-performance-overlay"
113 }
114
115 fn on_event(&mut self, event: &mut PluginEvent, handle: PluginHandle) {
116 match event {
117 PluginEvent::KeyboardInput {
118 window,
119 key,
120 modifiers,
121 is_pressed,
122 ..
123 } => {
124 let (shortcut_key, shortcut_modifiers) = &self.toggle_shortcut;
125 let key_matches = match (key, shortcut_key) {
126 (Key::Character(a), Key::Character(b)) => a.eq_ignore_ascii_case(b),
127 (a, b) => a == b,
128 };
129 if *is_pressed && *modifiers == *shortcut_modifiers && key_matches {
130 self.enabled = !self.enabled;
131 handle.send_event_loop_event(NativeEvent::Window(NativeWindowEvent {
132 window_id: window.id(),
133 action: NativeWindowEventAction::User(UserEvent::RequestRedraw),
134 }));
135 }
136 }
137 PluginEvent::WindowCreated {
138 window,
139 graphics_driver,
140 gpu_name,
141 ..
142 }
143 | PluginEvent::GraphicsDriverChanged {
144 window,
145 graphics_driver,
146 gpu_name,
147 } => {
148 let metrics = self.get_metrics(window.id());
149 metrics.graphics_driver = graphics_driver;
150 metrics.gpu_name = gpu_name.map(str::to_string);
151 }
152 PluginEvent::AfterRedraw { window, .. } => {
153 let metrics = self.get_metrics(window.id());
154 let now = Instant::now();
155
156 metrics
157 .frames
158 .retain(|frame| now.duration_since(*frame).as_millis() < 1000);
159
160 metrics.frames.push(now);
161
162 metrics.tasks_poll_time = Duration::ZERO;
164 }
165 PluginEvent::BeforePresenting { window, .. } => {
166 self.get_metrics(window.id()).started_presenting = Some(Instant::now())
167 }
168 PluginEvent::AfterPresenting { window, .. } => {
169 let metrics = self.get_metrics(window.id());
170 metrics.finished_presenting = Some(metrics.started_presenting.unwrap().elapsed())
171 }
172 PluginEvent::StartedMeasuringLayout { window, .. } => {
173 self.get_metrics(window.id()).started_layout = Some(Instant::now())
174 }
175 PluginEvent::FinishedMeasuringLayout { window, .. } => {
176 let metrics = self.get_metrics(window.id());
177 metrics.finished_layout = Some(metrics.started_layout.unwrap().elapsed())
178 }
179 PluginEvent::StartedUpdatingTree { window, .. } => {
180 self.get_metrics(window.id()).started_tree_updates = Some(Instant::now())
181 }
182 PluginEvent::FinishedUpdatingTree { window, .. } => {
183 let metrics = self.get_metrics(window.id());
184 metrics.finished_tree_updates =
185 Some(metrics.started_tree_updates.unwrap().elapsed())
186 }
187 PluginEvent::StartedPollingTasks { window, .. } => {
188 self.get_metrics(window.id()).started_tasks_poll = Some(Instant::now())
189 }
190 PluginEvent::FinishedPollingTasks { window, .. } => {
191 let metrics = self.get_metrics(window.id());
192 if let Some(started) = metrics.started_tasks_poll.take() {
193 metrics.tasks_poll_time += started.elapsed();
194 }
195 if self.enabled {
196 handle.send_event_loop_event(NativeEvent::Window(NativeWindowEvent {
197 window_id: window.id(),
198 action: NativeWindowEventAction::User(UserEvent::RequestRedraw),
199 }));
200 }
201 }
202 PluginEvent::BeforeAccessibility { window, .. } => {
203 self.get_metrics(window.id()).started_accessibility_updates = Some(Instant::now())
204 }
205 PluginEvent::AfterAccessibility { window, .. } => {
206 let metrics = self.get_metrics(window.id());
207 metrics.finished_accessibility_updates =
208 Some(metrics.started_accessibility_updates.unwrap().elapsed())
209 }
210 PluginEvent::BeforeRender { window, .. } => {
211 self.get_metrics(window.id()).started_render = Some(Instant::now())
212 }
213 PluginEvent::AfterRender {
214 window,
215 canvas,
216 font_collection,
217 tree,
218 animation_clock,
219 } => {
220 if !self.enabled {
221 return;
222 }
223 let metrics = self.get_metrics(window.id());
224 let scale_factor = window.scale_factor() as f32;
225 let started_render = metrics.started_render.take().unwrap();
226
227 canvas.save();
228 canvas.scale((scale_factor, scale_factor));
229
230 let finished_render = started_render.elapsed();
231 let finished_presenting = metrics.finished_presenting.unwrap_or_default();
232 let finished_layout = metrics.finished_layout.unwrap();
233 let finished_tree_updates = metrics.finished_tree_updates.unwrap_or_default();
234 let tasks_poll_time = metrics.tasks_poll_time;
235 let finished_accessibility_updates =
236 metrics.finished_accessibility_updates.unwrap_or_default();
237
238 let mut paragraph_builder =
240 ParagraphBuilder::new(&ParagraphStyle::default(), *font_collection);
241 let mut text_style = TextStyle::default();
242 text_style.set_color(Color::from_rgb(63, 255, 0));
243 text_style.add_shadow(TextShadow::new(
244 Color::from_rgb(60, 60, 60),
245 (0.0, 1.0),
246 1.0,
247 ));
248 paragraph_builder.push_style(&text_style);
249
250 add_text(
252 &mut paragraph_builder,
253 format!("{} FPS\n", metrics.frames.len()),
254 30.0,
255 );
256
257 metrics.fps_historic.push(metrics.frames.len());
258 if metrics.fps_historic.len() > 70 {
259 metrics.fps_historic.remove(0);
260 }
261
262 add_text(
264 &mut paragraph_builder,
265 format!(
266 "Rendering: {:.3}ms \n",
267 finished_render.as_secs_f64() * 1000.0
268 ),
269 18.0,
270 );
271
272 add_text(
274 &mut paragraph_builder,
275 format!(
276 "Presenting: {:.3}ms \n",
277 finished_presenting.as_secs_f64() * 1000.0
278 ),
279 18.0,
280 );
281
282 add_text(
284 &mut paragraph_builder,
285 format!("Layout: {:.3}ms \n", finished_layout.as_secs_f64() * 1000.0),
286 18.0,
287 );
288
289 add_text(
291 &mut paragraph_builder,
292 format!(
293 "Tree Updates: {:.3}ms \n",
294 finished_tree_updates.as_secs_f64() * 1000.0
295 ),
296 18.0,
297 );
298
299 add_text(
301 &mut paragraph_builder,
302 format!(
303 "a11y Updates: {:.3}ms \n",
304 finished_accessibility_updates.as_secs_f64() * 1000.0
305 ),
306 18.0,
307 );
308
309 add_text(
311 &mut paragraph_builder,
312 format!("Tasks: {:.3}ms \n", tasks_poll_time.as_secs_f64() * 1000.0),
313 18.0,
314 );
315
316 add_text(
318 &mut paragraph_builder,
319 format!("{} Tree Nodes \n", tree.size()),
320 14.0,
321 );
322
323 add_text(
325 &mut paragraph_builder,
326 format!("{} Layout Nodes \n", tree.layout.size()),
327 14.0,
328 );
329
330 add_text(
332 &mut paragraph_builder,
333 format!("Scale Factor: {}x\n", window.scale_factor()),
334 14.0,
335 );
336
337 add_text(
341 &mut paragraph_builder,
342 format!("Animation clock speed: {}x \n", animation_clock.speed()),
343 14.0,
344 );
345
346 add_text(
348 &mut paragraph_builder,
349 format!("Graphics: {} \n", metrics.graphics_driver),
350 14.0,
351 );
352
353 if let Some(gpu_name) = &metrics.gpu_name {
355 add_text(&mut paragraph_builder, format!("GPU: {gpu_name} \n"), 14.0);
356 }
357
358 let mut paragraph = paragraph_builder.build();
359 paragraph.layout(235.0);
360
361 metrics.max_fps = metrics.max_fps.max(
362 metrics
363 .fps_historic
364 .iter()
365 .max()
366 .copied()
367 .unwrap_or_default(),
368 );
369
370 let start_x = 5.0;
371 let start_y = paragraph.height() + 20.0 + metrics.max_fps.max(60) as f32;
372
373 let mut paint = Paint::default();
374 paint.set_anti_alias(true);
375 paint.set_style(PaintStyle::Fill);
376 paint.set_color(Color::from_argb(225, 225, 225, 225));
377 canvas.draw_rect(Rect::new(5., 5., 245.0, start_y + 15.0), &paint);
378
379 paragraph.paint(canvas, (5.0, 0.0));
380
381 for (i, fps) in metrics.fps_historic.iter().enumerate() {
382 let mut paint = Paint::default();
383 paint.set_anti_alias(true);
384 paint.set_style(PaintStyle::Fill);
385 paint.set_color(Color::from_rgb(63, 255, 0));
386 paint.set_stroke_width(3.0);
387
388 let x = start_x + (i * 2) as f32;
389 let y = start_y - *fps as f32 + 2.0;
390 canvas.draw_circle((x, y), 2.0, &paint);
391 }
392
393 canvas.restore();
394 }
395 _ => {}
396 }
397 }
398}
399
400fn add_text(paragraph_builder: &mut ParagraphBuilder, text: String, font_size: f32) {
401 let mut text_style = TextStyle::default();
402 text_style.set_color(Color::from_rgb(25, 225, 35));
403 let font_style = FontStyle::new(Weight::BOLD, Width::EXPANDED, Slant::Upright);
404 text_style.set_font_style(font_style);
405 text_style.add_shadow(TextShadow::new(
406 Color::from_rgb(65, 65, 65),
407 (0.0, 1.0),
408 1.0,
409 ));
410 text_style.set_font_size(font_size);
411 paragraph_builder.push_style(&text_style);
412 paragraph_builder.add_text(text);
413}