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