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 | PluginEvent::GraphicsDriverChanged {
139 window,
140 graphics_driver,
141 } => {
142 self.get_metrics(window.id()).graphics_driver = graphics_driver;
143 }
144 PluginEvent::AfterRedraw { window, .. } => {
145 let metrics = self.get_metrics(window.id());
146 let now = Instant::now();
147
148 metrics
149 .frames
150 .retain(|frame| now.duration_since(*frame).as_millis() < 1000);
151
152 metrics.frames.push(now);
153 }
154 PluginEvent::BeforePresenting { window, .. } => {
155 self.get_metrics(window.id()).started_presenting = Some(Instant::now())
156 }
157 PluginEvent::AfterPresenting { window, .. } => {
158 let metrics = self.get_metrics(window.id());
159 metrics.finished_presenting = Some(metrics.started_presenting.unwrap().elapsed())
160 }
161 PluginEvent::StartedMeasuringLayout { window, .. } => {
162 self.get_metrics(window.id()).started_layout = Some(Instant::now())
163 }
164 PluginEvent::FinishedMeasuringLayout { window, .. } => {
165 let metrics = self.get_metrics(window.id());
166 metrics.finished_layout = Some(metrics.started_layout.unwrap().elapsed())
167 }
168 PluginEvent::StartedUpdatingTree { window, .. } => {
169 self.get_metrics(window.id()).started_tree_updates = Some(Instant::now())
170 }
171 PluginEvent::FinishedUpdatingTree { window, .. } => {
172 let metrics = self.get_metrics(window.id());
173 metrics.finished_tree_updates =
174 Some(metrics.started_tree_updates.unwrap().elapsed())
175 }
176 PluginEvent::BeforeAccessibility { window, .. } => {
177 self.get_metrics(window.id()).started_accessibility_updates = Some(Instant::now())
178 }
179 PluginEvent::AfterAccessibility { window, .. } => {
180 let metrics = self.get_metrics(window.id());
181 metrics.finished_accessibility_updates =
182 Some(metrics.started_accessibility_updates.unwrap().elapsed())
183 }
184 PluginEvent::BeforeRender { window, .. } => {
185 self.get_metrics(window.id()).started_render = Some(Instant::now())
186 }
187 PluginEvent::AfterRender {
188 window,
189 canvas,
190 font_collection,
191 tree,
192 animation_clock,
193 } => {
194 if !self.enabled {
195 return;
196 }
197 let metrics = self.get_metrics(window.id());
198 let scale_factor = window.scale_factor() as f32;
199 let started_render = metrics.started_render.take().unwrap();
200
201 canvas.save();
202 canvas.scale((scale_factor, scale_factor));
203
204 let finished_render = started_render.elapsed();
205 let finished_presenting = metrics.finished_presenting.unwrap_or_default();
206 let finished_layout = metrics.finished_layout.unwrap();
207 let finished_tree_updates = metrics.finished_tree_updates.unwrap_or_default();
208 let finished_accessibility_updates =
209 metrics.finished_accessibility_updates.unwrap_or_default();
210
211 let mut paint = Paint::default();
212 paint.set_anti_alias(true);
213 paint.set_style(PaintStyle::Fill);
214 paint.set_color(Color::from_argb(225, 225, 225, 225));
215
216 canvas.draw_rect(Rect::new(5., 5., 220., 440.), &paint);
217
218 let mut paragraph_builder =
220 ParagraphBuilder::new(&ParagraphStyle::default(), *font_collection);
221 let mut text_style = TextStyle::default();
222 text_style.set_color(Color::from_rgb(63, 255, 0));
223 text_style.add_shadow(TextShadow::new(
224 Color::from_rgb(60, 60, 60),
225 (0.0, 1.0),
226 1.0,
227 ));
228 paragraph_builder.push_style(&text_style);
229
230 add_text(
232 &mut paragraph_builder,
233 format!("{} FPS\n", metrics.frames.len()),
234 30.0,
235 );
236
237 metrics.fps_historic.push(metrics.frames.len());
238 if metrics.fps_historic.len() > 70 {
239 metrics.fps_historic.remove(0);
240 }
241
242 add_text(
244 &mut paragraph_builder,
245 format!(
246 "Rendering: {:.3}ms \n",
247 finished_render.as_secs_f64() * 1000.0
248 ),
249 18.0,
250 );
251
252 add_text(
254 &mut paragraph_builder,
255 format!(
256 "Presenting: {:.3}ms \n",
257 finished_presenting.as_secs_f64() * 1000.0
258 ),
259 18.0,
260 );
261
262 add_text(
264 &mut paragraph_builder,
265 format!("Layout: {:.3}ms \n", finished_layout.as_secs_f64() * 1000.0),
266 18.0,
267 );
268
269 add_text(
271 &mut paragraph_builder,
272 format!(
273 "Tree Updates: {:.3}ms \n",
274 finished_tree_updates.as_secs_f64() * 1000.0
275 ),
276 18.0,
277 );
278
279 add_text(
281 &mut paragraph_builder,
282 format!(
283 "a11y Updates: {:.3}ms \n",
284 finished_accessibility_updates.as_secs_f64() * 1000.0
285 ),
286 18.0,
287 );
288
289 add_text(
291 &mut paragraph_builder,
292 format!("{} Tree Nodes \n", tree.size()),
293 14.0,
294 );
295
296 add_text(
298 &mut paragraph_builder,
299 format!("{} Layout Nodes \n", tree.layout.size()),
300 14.0,
301 );
302
303 add_text(
305 &mut paragraph_builder,
306 format!("Scale Factor: {}x\n", window.scale_factor()),
307 14.0,
308 );
309
310 add_text(
314 &mut paragraph_builder,
315 format!("Animation clock speed: {}x \n", animation_clock.speed()),
316 14.0,
317 );
318
319 add_text(
321 &mut paragraph_builder,
322 format!("Graphics: {} \n", metrics.graphics_driver),
323 14.0,
324 );
325
326 let mut paragraph = paragraph_builder.build();
327 paragraph.layout(f32::MAX);
328 paragraph.paint(canvas, (5.0, 0.0));
329
330 metrics.max_fps = metrics.max_fps.max(
331 metrics
332 .fps_historic
333 .iter()
334 .max()
335 .copied()
336 .unwrap_or_default(),
337 );
338 let start_x = 5.0;
339 let start_y = 290.0 + metrics.max_fps.max(60) as f32;
340
341 for (i, fps) in metrics.fps_historic.iter().enumerate() {
342 let mut paint = Paint::default();
343 paint.set_anti_alias(true);
344 paint.set_style(PaintStyle::Fill);
345 paint.set_color(Color::from_rgb(63, 255, 0));
346 paint.set_stroke_width(3.0);
347
348 let x = start_x + (i * 2) as f32;
349 let y = start_y - *fps as f32 + 2.0;
350 canvas.draw_circle((x, y), 2.0, &paint);
351 }
352
353 canvas.restore();
354 }
355 _ => {}
356 }
357 }
358}
359
360fn add_text(paragraph_builder: &mut ParagraphBuilder, text: String, font_size: f32) {
361 let mut text_style = TextStyle::default();
362 text_style.set_color(Color::from_rgb(25, 225, 35));
363 let font_style = FontStyle::new(Weight::BOLD, Width::EXPANDED, Slant::Upright);
364 text_style.set_font_style(font_style);
365 text_style.add_shadow(TextShadow::new(
366 Color::from_rgb(65, 65, 65),
367 (0.0, 1.0),
368 1.0,
369 ));
370 text_style.set_font_size(font_size);
371 paragraph_builder.push_style(&text_style);
372 paragraph_builder.add_text(text);
373}