use std::{
collections::HashMap,
time::{
Duration,
Instant,
},
};
use freya_core::prelude::{
ModifiersExt,
UserEvent,
};
use freya_engine::prelude::{
Color,
FontStyle,
Paint,
PaintStyle,
ParagraphBuilder,
ParagraphStyle,
Rect,
Slant,
TextShadow,
TextStyle,
Weight,
Width,
};
use freya_winit::{
plugins::{
FreyaPlugin,
Key,
Modifiers,
PluginEvent,
PluginHandle,
},
reexports::winit::window::WindowId,
renderer::{
NativeEvent,
NativeWindowEvent,
NativeWindowEventAction,
},
};
pub struct PerformanceOverlayPlugin {
enabled: bool,
toggle_shortcut: (Key, Modifiers),
metrics: HashMap<WindowId, WindowMetrics>,
}
impl Default for PerformanceOverlayPlugin {
fn default() -> Self {
Self {
enabled: false,
toggle_shortcut: (
Key::Character("p".into()),
Modifiers::ctrl_or_meta() | Modifiers::SHIFT,
),
metrics: HashMap::new(),
}
}
}
#[derive(Default)]
struct WindowMetrics {
graphics_driver: &'static str,
gpu_name: Option<String>,
frames: Vec<Instant>,
fps_historic: Vec<usize>,
max_fps: usize,
started_render: Option<Instant>,
started_layout: Option<Instant>,
finished_layout: Option<Duration>,
started_tree_updates: Option<Instant>,
finished_tree_updates: Option<Duration>,
started_tasks_poll: Option<Instant>,
tasks_poll_time: Duration,
started_accessibility_updates: Option<Instant>,
finished_accessibility_updates: Option<Duration>,
started_presenting: Option<Instant>,
finished_presenting: Option<Duration>,
}
impl PerformanceOverlayPlugin {
pub fn with_toggle_shortcut(mut self, key: Key, modifiers: Modifiers) -> Self {
self.toggle_shortcut = (key, modifiers);
self
}
pub fn with_visible(mut self, visible: bool) -> Self {
self.enabled = visible;
self
}
fn get_metrics(&mut self, id: WindowId) -> &mut WindowMetrics {
self.metrics.entry(id).or_default()
}
}
impl FreyaPlugin for PerformanceOverlayPlugin {
fn plugin_id(&self) -> &'static str {
"freya-performance-overlay"
}
fn on_event(&mut self, event: &mut PluginEvent, handle: PluginHandle) {
match event {
PluginEvent::KeyboardInput {
window,
key,
modifiers,
is_pressed,
..
} => {
let (shortcut_key, shortcut_modifiers) = &self.toggle_shortcut;
let key_matches = match (key, shortcut_key) {
(Key::Character(a), Key::Character(b)) => a.eq_ignore_ascii_case(b),
(a, b) => a == b,
};
if *is_pressed && *modifiers == *shortcut_modifiers && key_matches {
self.enabled = !self.enabled;
handle.send_event_loop_event(NativeEvent::Window(NativeWindowEvent {
window_id: window.id(),
action: NativeWindowEventAction::User(UserEvent::RequestRedraw),
}));
}
}
PluginEvent::WindowCreated {
window,
graphics_driver,
gpu_name,
..
}
| PluginEvent::GraphicsDriverChanged {
window,
graphics_driver,
gpu_name,
} => {
let metrics = self.get_metrics(window.id());
metrics.graphics_driver = graphics_driver;
metrics.gpu_name = gpu_name.map(str::to_string);
}
PluginEvent::AfterRedraw { window, .. } => {
let metrics = self.get_metrics(window.id());
let now = Instant::now();
metrics
.frames
.retain(|frame| now.duration_since(*frame).as_millis() < 1000);
metrics.frames.push(now);
metrics.tasks_poll_time = Duration::ZERO;
}
PluginEvent::BeforePresenting { window, .. } => {
self.get_metrics(window.id()).started_presenting = Some(Instant::now())
}
PluginEvent::AfterPresenting { window, .. } => {
let metrics = self.get_metrics(window.id());
metrics.finished_presenting = Some(metrics.started_presenting.unwrap().elapsed())
}
PluginEvent::StartedMeasuringLayout { window, .. } => {
self.get_metrics(window.id()).started_layout = Some(Instant::now())
}
PluginEvent::FinishedMeasuringLayout { window, .. } => {
let metrics = self.get_metrics(window.id());
metrics.finished_layout = Some(metrics.started_layout.unwrap().elapsed())
}
PluginEvent::StartedUpdatingTree { window, .. } => {
self.get_metrics(window.id()).started_tree_updates = Some(Instant::now())
}
PluginEvent::FinishedUpdatingTree { window, .. } => {
let metrics = self.get_metrics(window.id());
metrics.finished_tree_updates =
Some(metrics.started_tree_updates.unwrap().elapsed())
}
PluginEvent::StartedPollingTasks { window, .. } => {
self.get_metrics(window.id()).started_tasks_poll = Some(Instant::now())
}
PluginEvent::FinishedPollingTasks { window, .. } => {
let metrics = self.get_metrics(window.id());
if let Some(started) = metrics.started_tasks_poll.take() {
metrics.tasks_poll_time += started.elapsed();
}
if self.enabled {
handle.send_event_loop_event(NativeEvent::Window(NativeWindowEvent {
window_id: window.id(),
action: NativeWindowEventAction::User(UserEvent::RequestRedraw),
}));
}
}
PluginEvent::BeforeAccessibility { window, .. } => {
self.get_metrics(window.id()).started_accessibility_updates = Some(Instant::now())
}
PluginEvent::AfterAccessibility { window, .. } => {
let metrics = self.get_metrics(window.id());
metrics.finished_accessibility_updates =
Some(metrics.started_accessibility_updates.unwrap().elapsed())
}
PluginEvent::BeforeRender { window, .. } => {
self.get_metrics(window.id()).started_render = Some(Instant::now())
}
PluginEvent::AfterRender {
window,
canvas,
font_collection,
tree,
animation_clock,
} => {
if !self.enabled {
return;
}
let metrics = self.get_metrics(window.id());
let scale_factor = window.scale_factor() as f32;
let started_render = metrics.started_render.take().unwrap();
canvas.save();
canvas.scale((scale_factor, scale_factor));
let finished_render = started_render.elapsed();
let finished_presenting = metrics.finished_presenting.unwrap_or_default();
let finished_layout = metrics.finished_layout.unwrap();
let finished_tree_updates = metrics.finished_tree_updates.unwrap_or_default();
let tasks_poll_time = metrics.tasks_poll_time;
let finished_accessibility_updates =
metrics.finished_accessibility_updates.unwrap_or_default();
let mut paragraph_builder =
ParagraphBuilder::new(&ParagraphStyle::default(), *font_collection);
let mut text_style = TextStyle::default();
text_style.set_color(Color::from_rgb(63, 255, 0));
text_style.add_shadow(TextShadow::new(
Color::from_rgb(60, 60, 60),
(0.0, 1.0),
1.0,
));
paragraph_builder.push_style(&text_style);
add_text(
&mut paragraph_builder,
format!("{} FPS\n", metrics.frames.len()),
30.0,
);
metrics.fps_historic.push(metrics.frames.len());
if metrics.fps_historic.len() > 70 {
metrics.fps_historic.remove(0);
}
add_text(
&mut paragraph_builder,
format!(
"Rendering: {:.3}ms \n",
finished_render.as_secs_f64() * 1000.0
),
18.0,
);
add_text(
&mut paragraph_builder,
format!(
"Presenting: {:.3}ms \n",
finished_presenting.as_secs_f64() * 1000.0
),
18.0,
);
add_text(
&mut paragraph_builder,
format!("Layout: {:.3}ms \n", finished_layout.as_secs_f64() * 1000.0),
18.0,
);
add_text(
&mut paragraph_builder,
format!(
"Tree Updates: {:.3}ms \n",
finished_tree_updates.as_secs_f64() * 1000.0
),
18.0,
);
add_text(
&mut paragraph_builder,
format!(
"a11y Updates: {:.3}ms \n",
finished_accessibility_updates.as_secs_f64() * 1000.0
),
18.0,
);
add_text(
&mut paragraph_builder,
format!("Tasks: {:.3}ms \n", tasks_poll_time.as_secs_f64() * 1000.0),
18.0,
);
add_text(
&mut paragraph_builder,
format!("{} Tree Nodes \n", tree.size()),
14.0,
);
add_text(
&mut paragraph_builder,
format!("{} Layout Nodes \n", tree.layout.size()),
14.0,
);
add_text(
&mut paragraph_builder,
format!("Scale Factor: {}x\n", window.scale_factor()),
14.0,
);
add_text(
&mut paragraph_builder,
format!("Animation clock speed: {}x \n", animation_clock.speed()),
14.0,
);
add_text(
&mut paragraph_builder,
format!("Graphics: {} \n", metrics.graphics_driver),
14.0,
);
if let Some(gpu_name) = &metrics.gpu_name {
add_text(&mut paragraph_builder, format!("GPU: {gpu_name} \n"), 14.0);
}
let mut paragraph = paragraph_builder.build();
paragraph.layout(235.0);
metrics.max_fps = metrics.max_fps.max(
metrics
.fps_historic
.iter()
.max()
.copied()
.unwrap_or_default(),
);
let start_x = 5.0;
let start_y = paragraph.height() + 20.0 + metrics.max_fps.max(60) as f32;
let mut paint = Paint::default();
paint.set_anti_alias(true);
paint.set_style(PaintStyle::Fill);
paint.set_color(Color::from_argb(225, 225, 225, 225));
canvas.draw_rect(Rect::new(5., 5., 245.0, start_y + 15.0), &paint);
paragraph.paint(canvas, (5.0, 0.0));
for (i, fps) in metrics.fps_historic.iter().enumerate() {
let mut paint = Paint::default();
paint.set_anti_alias(true);
paint.set_style(PaintStyle::Fill);
paint.set_color(Color::from_rgb(63, 255, 0));
paint.set_stroke_width(3.0);
let x = start_x + (i * 2) as f32;
let y = start_y - *fps as f32 + 2.0;
canvas.draw_circle((x, y), 2.0, &paint);
}
canvas.restore();
}
_ => {}
}
}
}
fn add_text(paragraph_builder: &mut ParagraphBuilder, text: String, font_size: f32) {
let mut text_style = TextStyle::default();
text_style.set_color(Color::from_rgb(25, 225, 35));
let font_style = FontStyle::new(Weight::BOLD, Width::EXPANDED, Slant::Upright);
text_style.set_font_style(font_style);
text_style.add_shadow(TextShadow::new(
Color::from_rgb(65, 65, 65),
(0.0, 1.0),
1.0,
));
text_style.set_font_size(font_size);
paragraph_builder.push_style(&text_style);
paragraph_builder.add_text(text);
}