From 3987b20dafb121b1dd7b60ac0744ec5b4c356450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20K=C3=A5re=20Alsaker?= Date: Fri, 10 Nov 2023 15:27:51 +0100 Subject: [PATCH] Add a basic frame profiler --- src/app.rs | 8 +- src/app_handle.rs | 72 ++++++++++ src/inspector.rs | 90 ++++++++++--- src/lib.rs | 1 + src/profiler.rs | 307 +++++++++++++++++++++++++++++++++++++++++++ src/widgets/mod.rs | 2 +- src/window_handle.rs | 7 +- 7 files changed, 461 insertions(+), 26 deletions(-) create mode 100644 src/profiler.rs diff --git a/src/app.rs b/src/app.rs index e121b0d8..6b386965 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,8 +10,8 @@ use winit::{ }; use crate::{ - action::Timer, app_handle::ApplicationHandle, inspector::Capture, view::View, - window::WindowConfig, + action::Timer, app_handle::ApplicationHandle, inspector::Capture, profiler::Profile, + view::View, window::WindowConfig, }; type AppEventCallback = dyn Fn(AppEvent); @@ -50,6 +50,10 @@ pub(crate) enum AppUpdateEvent { window_id: WindowId, capture: WriteSignal>>, }, + ProfileWindow { + window_id: WindowId, + end_profile: Option>>>, + }, RequestTimer { timer: Timer, }, diff --git a/src/app_handle.rs b/src/app_handle.rs index e75b614d..279222a8 100644 --- a/src/app_handle.rs +++ b/src/app_handle.rs @@ -13,6 +13,7 @@ use crate::{ app::{AppUpdateEvent, UserEvent, APP_UPDATE_EVENTS}, ext_event::EXT_EVENT_HANDLER, inspector::Capture, + profiler::{Profile, ProfileEvent}, view::View, window::WindowConfig, window_handle::WindowHandle, @@ -68,6 +69,22 @@ impl ApplicationHandle { AppUpdateEvent::CaptureWindow { window_id, capture } => { capture.set(self.capture_window(window_id).map(Rc::new)); } + AppUpdateEvent::ProfileWindow { + window_id, + end_profile, + } => { + let handle = self.window_handles.get_mut(&window_id); + if let Some(handle) = handle { + if let Some(profile) = end_profile { + profile.set(handle.profile.take().map(|mut profile| { + profile.next_frame(); + Rc::new(profile) + })); + } else { + handle.profile = Some(Profile::default()); + } + } + } #[cfg(target_os = "linux")] AppUpdateEvent::MenuAction { window_id, @@ -94,6 +111,44 @@ impl ApplicationHandle { None => return, }; + let start = window_handle.profile.is_some().then(|| { + let name = match event { + WindowEvent::ActivationTokenDone { .. } => "ActivationTokenDone", + WindowEvent::Resized(..) => "Resized", + WindowEvent::Moved(..) => "Moved", + WindowEvent::CloseRequested => "CloseRequested", + WindowEvent::Destroyed => "Destroyed", + WindowEvent::DroppedFile(_) => "DroppedFile", + WindowEvent::HoveredFile(_) => "HoveredFile", + WindowEvent::HoveredFileCancelled => "HoveredFileCancelled", + WindowEvent::Focused(..) => "Focused", + WindowEvent::KeyboardInput { .. } => "KeyboardInput", + WindowEvent::ModifiersChanged(..) => "ModifiersChanged", + WindowEvent::Ime(..) => "Ime", + WindowEvent::CursorMoved { .. } => "CursorMoved", + WindowEvent::CursorEntered { .. } => "CursorEntered", + WindowEvent::CursorLeft { .. } => "CursorLeft", + WindowEvent::MouseWheel { .. } => "MouseWheel", + WindowEvent::MouseInput { .. } => "MouseInput", + WindowEvent::TouchpadMagnify { .. } => "TouchpadMagnify", + WindowEvent::SmartMagnify { .. } => "SmartMagnify", + WindowEvent::TouchpadRotate { .. } => "TouchpadRotate", + WindowEvent::TouchpadPressure { .. } => "TouchpadPressure", + WindowEvent::AxisMotion { .. } => "AxisMotion", + WindowEvent::Touch(_) => "Touch", + WindowEvent::ScaleFactorChanged { .. } => "ScaleFactorChanged", + WindowEvent::ThemeChanged(..) => "ThemeChanged", + WindowEvent::Occluded(..) => "Occluded", + WindowEvent::MenuAction(..) => "MenuAction", + WindowEvent::RedrawRequested => "RedrawRequested", + }; + ( + name, + Instant::now(), + matches!(event, WindowEvent::RedrawRequested), + ) + }); + match event { WindowEvent::ActivationTokenDone { .. } => {} WindowEvent::Resized(size) => { @@ -162,6 +217,23 @@ impl ApplicationHandle { window_handle.render_frame(); } } + + if let Some((name, start, new_frame)) = start { + let end = Instant::now(); + + if let Some(window_handle) = self.window_handles.get_mut(&window_id) { + let profile = window_handle.profile.as_mut().unwrap(); + + profile + .current + .events + .push(ProfileEvent { start, end, name }); + + if new_frame { + profile.next_frame(); + } + } + } } pub(crate) fn new_window( diff --git a/src/inspector.rs b/src/inspector.rs index 07928db4..e222f923 100644 --- a/src/inspector.rs +++ b/src/inspector.rs @@ -2,16 +2,17 @@ use crate::app::{add_app_update_event, AppUpdateEvent}; use crate::context::{AppState, ChangeFlags, StyleCx}; use crate::event::{Event, EventListener}; use crate::id::Id; +use crate::profiler::profiler; use crate::style::{Style, StyleMapValue}; use crate::view::{view_children, View}; use crate::views::{ container, dyn_container, empty, h_stack, img_dynamic, scroll, stack, static_label, - static_list, text, v_stack, Decorators, Label, + static_list, tab, text, v_stack, Decorators, Label, }; use crate::widgets::button; use crate::window::WindowConfig; use crate::{new_window, style}; -use floem_reactive::{create_effect, create_rw_signal, RwSignal, Scope}; +use floem_reactive::{create_effect, create_rw_signal, create_signal, RwSignal, Scope}; use image::DynamicImage; use kurbo::{Point, Rect, Size}; use peniko::Color; @@ -361,7 +362,7 @@ fn captured_view( } } -fn header(label: impl Display) -> Label { +pub(crate) fn header(label: impl Display) -> Label { text(label).style(|s| { s.padding(5.0) .background(Color::WHITE_SMOKE) @@ -806,8 +807,7 @@ fn capture_view(capture: &Rc) -> impl View { .background(Color::BLACK.with_alpha_factor(0.2)) }); - stack((left, seperator, tree)) - .style(|s| s.flex_row().height_full().width_full().max_width_full()) + h_stack((left, seperator, tree)).style(|s| s.height_full().width_full().max_width_full()) } fn inspector_view(capture: &Option>) -> impl View { @@ -819,13 +819,8 @@ fn inspector_view(capture: &Option>) -> impl View { stack((view,)) .window_title(|| "Floem Inspector".to_owned()) - .on_event(EventListener::WindowClosed, |_| { - RUNNING.set(false); - false - }) .style(|s| { - s.font_size(12.0) - .width_full() + s.width_full() .height_full() .background(Color::WHITE) .class(scroll::Handle, |s| { @@ -856,14 +851,64 @@ pub fn capture(window_id: WindowId) { RUNNING.set(true); new_window( move |_| { - let view = dyn_container( - move || capture.get(), - |capture| Box::new(inspector_view(&capture)), - ); - let id = view.id(); - view.style(|s| s.width_full().height_full()).on_event( - EventListener::KeyUp, - move |e| { + let (selected, set_selected) = create_signal(0); + + let tab_item = |name, index| { + text(name) + .on_click(move |_| { + set_selected.set(index); + true + }) + .style(move |s| { + s.padding(5.0) + .border_right(1) + .border_color(Color::BLACK.with_alpha_factor(0.2)) + .hover(move |s| { + s.background(Color::rgba8(228, 237, 216, 160)) + .apply_if(selected.get() == index, |s| { + s.background(Color::rgb8(186, 180, 216)) + }) + }) + .apply_if(selected.get() == index, |s| { + s.background(Color::rgb8(213, 208, 216)) + }) + }) + }; + + let tabs = h_stack((tab_item("Views", 0), tab_item("Profiler", 1))) + .style(|s| s.background(Color::WHITE)); + + let tab = tab( + move || selected.get(), + move || [0, 1].into_iter(), + |it| *it, + move |it| -> Box { + match it { + 0 => Box::new( + dyn_container( + move || capture.get(), + |capture| Box::new(inspector_view(&capture)), + ) + .style(|s| s.width_full().height_full()), + ), + 1 => Box::new(profiler(window_id)), + _ => panic!(), + } + }, + ) + .style(|s| s.flex_basis(0.0).min_height(0.0).flex_grow(1.0)); + + let seperator = empty().style(move |s| { + s.width_full() + .min_height(1.0) + .background(Color::BLACK.with_alpha_factor(0.2)) + }); + + let stack = v_stack((tabs, seperator, tab)); + let id = stack.id(); + stack + .style(|s| s.width_full().height_full()) + .on_event(EventListener::KeyUp, move |e| { if let Event::KeyUp(e) = e { if e.key.logical_key == Key::Named(NamedKey::F11) && e.modifiers.shift_key() @@ -873,8 +918,11 @@ pub fn capture(window_id: WindowId) { } } false - }, - ) + }) + .on_event(EventListener::WindowClosed, |_| { + RUNNING.set(false); + false + }) }, Some(WindowConfig { size: Some(Size { diff --git a/src/lib.rs b/src/lib.rs index af755fc6..9621fe87 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -103,6 +103,7 @@ pub mod keyboard; pub mod menu; mod nav; pub mod pointer; +mod profiler; pub mod renderer; pub mod responsive; pub mod style; diff --git a/src/profiler.rs b/src/profiler.rs new file mode 100644 index 00000000..5ae14ab5 --- /dev/null +++ b/src/profiler.rs @@ -0,0 +1,307 @@ +use crate::app::{add_app_update_event, AppUpdateEvent}; +use crate::event::{Event, EventListener}; +use crate::inspector::header; +use crate::view::View; +use crate::views::{ + clip, container, dyn_container, empty, h_stack, label, scroll, stack, static_label, + static_list, text, v_stack, Decorators, +}; +use crate::widgets::button; +use floem_reactive::{create_rw_signal, RwSignal, Scope}; +use peniko::Color; +use std::fmt::Display; +use std::mem; +use std::rc::Rc; +use std::time::{Duration, Instant}; +use taffy::style::FlexDirection; +use winit::window::WindowId; + +#[derive(Clone)] +pub struct ProfileEvent { + pub start: Instant, + pub end: Instant, + pub name: &'static str, +} + +#[derive(Default)] +pub struct ProfileFrame { + pub events: Vec, +} + +#[derive(Default)] +pub struct Profile { + pub current: ProfileFrame, + frames: Vec, +} + +impl Profile { + pub fn next_frame(&mut self) { + self.frames.push(mem::take(&mut self.current)); + } +} + +struct ProfileFrameData { + start: Option, + duration: Duration, + sum: Duration, + events: Vec, +} + +fn info(name: impl Display, value: String) -> impl View { + info_row(name.to_string(), static_label(value)) +} + +fn info_row(name: String, view: impl View + 'static) -> impl View { + stack(( + stack((static_label(name).style(|s| { + s.margin_right(5.0) + .color(Color::BLACK.with_alpha_factor(0.6)) + }),)) + .style(|s| s.min_width(80.0).flex_direction(FlexDirection::RowReverse)), + view, + )) + .style(|s| { + s.padding(5.0) + .hover(|s| s.background(Color::rgba8(228, 237, 216, 160))) + }) +} + +fn profile_view(profile: &Rc) -> impl View { + let mut frames: Vec<_> = profile + .frames + .iter() + .map(|frame| { + let start = frame.events.first().map(|event| event.start); + let end = frame.events.last().map(|event| event.end); + let sum = frame + .events + .iter() + .map(|event| event.end.saturating_duration_since(event.start)) + .sum(); + let duration = start + .and_then(|start| end.map(|end| end.saturating_duration_since(start))) + .unwrap_or_default(); + Rc::new(ProfileFrameData { + start, + duration, + sum, + events: frame.events.clone(), + }) + }) + .collect(); + frames.sort_by(|a, b| b.sum.cmp(&a.sum)); + + let selected_frame = create_rw_signal(None); + + let zoom = create_rw_signal(1.0); + + let frames: Vec<_> = frames + .iter() + .enumerate() + .map(|(i, frame)| { + let frame = frame.clone(); + let frame_ = frame.clone(); + h_stack(( + static_label(format!("Frame #{i}")).style(|s| s.flex_grow(1.0)), + static_label(format!("{:.4} ms", frame.sum.as_secs_f64() * 1000.0)) + .style(|s| s.margin_right(16)), + )) + .on_click(move |_| { + selected_frame.set(Some(frame.clone())); + zoom.set(1.0); + true + }) + .style(move |s| { + let selected = selected_frame + .get() + .map(|selected| Rc::ptr_eq(&selected, &frame_)) + .unwrap_or(false); + s.padding(5.0) + .apply_if(selected, |s| s.background(Color::rgb8(213, 208, 216))) + .hover(move |s| { + s.background(Color::rgba8(228, 237, 216, 160)) + .apply_if(selected, |s| s.background(Color::rgb8(186, 180, 216))) + }) + }) + }) + .collect(); + + let hovered_event = create_rw_signal(None); + + let event_tooltip = dyn_container( + move || hovered_event.get(), + move |event: Option| -> Box { + if let Some(event) = event { + let len = event + .end + .saturating_duration_since(event.start) + .as_secs_f64(); + Box::new(v_stack(( + info("Name", event.name.to_string()), + info("Time", format!("{:.4} ms", len * 1000.0)), + ))) + } else { + Box::new(text("No hovered event").style(|s| s.padding(5.0))) + } + }, + ) + .style(|s| s.min_height(50)); + + let frames = v_stack(( + header("Frames"), + scroll(static_list(frames).style(|s| s.width_full())).style(|s| { + s.background(Color::WHITE) + .flex_basis(0) + .min_height(0) + .flex_grow(1.0) + }), + header("Event"), + event_tooltip, + )) + .style(|s| s.max_width_pct(60.0).min_width(200.0)); + + let seperator = empty().style(move |s| { + s.height_full() + .min_width(1.0) + .background(Color::BLACK.with_alpha_factor(0.2)) + }); + + let timeline = dyn_container( + move || selected_frame.get(), + move |frame| { + if let Some(frame) = frame { + let list = frame.events.iter().map(|event| { + let len = event + .end + .saturating_duration_since(event.start) + .as_secs_f64(); + let left = event + .start + .saturating_duration_since(frame.start.unwrap()) + .as_secs_f64() + / frame.duration.as_secs_f64(); + let width = len / frame.duration.as_secs_f64(); + let event_ = event.clone(); + clip( + static_label(format!("{} ({:.4} ms)", event.name, len * 1000.0)) + .style(|s| s.padding(5.0)), + ) + .style(move |s| { + s.min_width(0) + .width_pct(width * 100.0) + .absolute() + .inset_left_pct(left * 100.0) + .border(0.3) + .border_color(Color::rgb8(129, 164, 192)) + .background(Color::rgb8(209, 222, 233).with_alpha_factor(0.6)) + .text_clip() + .hover(|s| { + s.color(Color::WHITE) + .background(Color::BLACK.with_alpha_factor(0.6)) + }) + }) + .on_event(EventListener::PointerEnter, move |_| { + hovered_event.set(Some(event_.clone())); + false + }) + }); + Box::new( + scroll( + static_list(list) + .style(move |s| s.min_width_pct(zoom.get() * 100.0).height_full()), + ) + .style(|s| s.height_full().min_width(0).flex_basis(0).flex_grow(1.0)) + .on_event(EventListener::PointerWheel, move |e| { + if let Event::PointerWheel(e) = e { + zoom.set(zoom.get() * (1.0 - e.delta.y / 400.0)); + true + } else { + false + } + }), + ) + } else { + Box::new(text("No selected frame").style(|s| s.padding(5.0))) + } + }, + ) + .style(|s| { + s.width_full() + .min_height(0) + .flex_basis(0) + .flex_grow(1.0) + .background(Color::WHITE) + }); + + let timeline = v_stack((header("Timeline"), timeline)) + .style(|s| s.min_width(0).flex_basis(0).flex_grow(1.0)); + + h_stack((frames, seperator, timeline)).style(|s| s.height_full().width_full().max_width_full()) +} + +thread_local! { + pub(crate) static PROFILE: RwSignal>> = { + Scope::new().create_rw_signal(None) + }; +} + +pub fn profiler(window_id: WindowId) -> impl View { + let profiling = create_rw_signal(false); + let profile = PROFILE.with(|c| *c); + + let button = h_stack(( + button(move || { + if profiling.get() { + "Stop Profiling" + } else { + "Start Profiling" + } + }) + .on_click(move |_| { + add_app_update_event(AppUpdateEvent::ProfileWindow { + window_id, + end_profile: if profiling.get() { + Some(profile.write_only()) + } else { + None + }, + }); + profiling.set(!profiling.get()); + true + }) + .style(|s| s.margin(5.0)), + label(move || if profiling.get() { "Profiling..." } else { "" }), + )) + .style(|s| s.items_center()); + + let seperator = empty().style(move |s| { + s.width_full() + .min_height(1.0) + .background(Color::BLACK.with_alpha_factor(0.2)) + }); + + let lower = dyn_container( + move || profile.get(), + |profile| { + if let Some(profile) = profile { + Box::new(profile_view(&profile)) + } else { + Box::new(text("No profile").style(|s| s.padding(5.0))) + } + }, + ) + .style(|s| s.width_full().min_height(0).flex_basis(0).flex_grow(1.0)); + + // FIXME: This needs an extra `container` or the `v_stack` ends up horizontal. + container(v_stack((button, seperator, lower)).style(|s| s.width_full().height_full())) + .style(|s| s.width_full().height_full()) + .on_event(EventListener::WindowClosed, move |_| { + if profiling.get() { + add_app_update_event(AppUpdateEvent::ProfileWindow { + window_id, + end_profile: Some(profile.write_only()), + }); + } + false + }) +} diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 1df4b415..e71e90a2 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -114,7 +114,7 @@ pub(crate) fn default_theme() -> Theme { }) .apply(focus_style.clone()); - const FONT_SIZE: f32 = 13.0; + const FONT_SIZE: f32 = 12.0; let input_style = Style::new() .background(Color::WHITE) diff --git a/src/window_handle.rs b/src/window_handle.rs index 1c2c305b..8d072c3f 100644 --- a/src/window_handle.rs +++ b/src/window_handle.rs @@ -32,6 +32,7 @@ use crate::{ menu::Menu, nav::view_arrow_navigation, pointer::{PointerButton, PointerInputEvent, PointerMoveEvent, PointerWheelEvent}, + profiler::Profile, style::{CursorStyle, StyleSelector}, update::{ UpdateMessage, ANIM_UPDATE_MESSAGES, CENTRAL_DEFERRED_UPDATE_MESSAGES, @@ -59,6 +60,7 @@ pub(crate) struct WindowHandle { paint_state: PaintState, size: RwSignal, theme: Option, + pub(crate) profile: Option, os_theme: RwSignal>, is_maximized: bool, transparent: bool, @@ -128,6 +130,7 @@ impl WindowHandle { os_theme: theme, is_maximized, transparent, + profile: None, scale, modifiers: ModifiersState::default(), cursor_position: Point::ZERO, @@ -270,7 +273,7 @@ impl WindowHandle { || view_state.has_style_selectors.has(StyleSelector::Hover) || view_state.has_style_selectors.has(StyleSelector::Active) { - cx.app_state.request_style(*id); + cx.app_state.request_style_recursive(*id); } if hovered.contains(id) { if let Some(action) = cx.get_event_listener(*id, &EventListener::PointerEnter) { @@ -397,7 +400,7 @@ impl WindowHandle { || view_state.has_style_selectors.has(StyleSelector::Active) || view_state.animation.is_some() { - cx.app_state.request_style(id); + cx.app_state.request_style_recursive(id); } let id_path = ID_PATHS.with(|paths| paths.borrow().get(&id).cloned()); if let Some(id_path) = id_path {