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