diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index ab5cafcbac55b40b2e136f28a08f2245ae27e4f1..3a07dcc3948fefc4f81178a17386fe3cd77dcf0a 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -1,4 +1,4 @@ -use crate::{App, PlatformDispatcher}; +use crate::{App, Bigus, PlatformDispatcher}; use async_task::Runnable; use futures::channel::mpsc; use smol::prelude::*; @@ -63,7 +63,7 @@ enum TaskState { Ready(Option), /// A task that is currently running. - Spawned(async_task::Task), + Spawned(async_task::Task), } impl Task { @@ -168,8 +168,13 @@ impl BackgroundExecutor { label: Option, ) -> Task { let dispatcher = self.dispatcher.clone(); - let (runnable, task) = - async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable, label)); + let location = core::panic::Location::caller(); + let (runnable, task) = async_task::Builder::new() + .metadata(Bigus { location }) + .spawn( + move |_| future, + move |runnable| dispatcher.dispatch(runnable, label), + ); runnable.schedule(); Task(TaskState::Spawned(task)) } @@ -358,10 +363,13 @@ impl BackgroundExecutor { if duration.is_zero() { return Task::ready(()); } - let (runnable, task) = async_task::spawn(async move {}, { - let dispatcher = self.dispatcher.clone(); - move |runnable| dispatcher.dispatch_after(duration, runnable) - }); + let location = core::panic::Location::caller(); + let (runnable, task) = async_task::Builder::new() + .metadata(Bigus { location }) + .spawn(move |_| async move {}, { + let dispatcher = self.dispatcher.clone(); + move |runnable| dispatcher.dispatch_after(duration, runnable) + }); runnable.schedule(); Task(TaskState::Spawned(task)) } @@ -468,24 +476,29 @@ impl ForegroundExecutor { /// Enqueues the given Task to run on the main thread at some point in the future. #[track_caller] - pub fn spawn(&self, future: impl Future + 'static) -> Task + pub fn spawn(&self, future: F) -> Task where + F: Future + 'static, R: 'static, { let dispatcher = self.dispatcher.clone(); + let location = core::panic::Location::caller(); #[track_caller] fn inner( dispatcher: Arc, future: AnyLocalFuture, + location: &'static core::panic::Location<'static>, ) -> Task { - let (runnable, task) = spawn_local_with_source_location(future, move |runnable| { - dispatcher.dispatch_on_main_thread(runnable) - }); + let (runnable, task) = spawn_local_with_source_location( + future, + move |runnable| dispatcher.dispatch_on_main_thread(runnable), + Bigus { location }, + ); runnable.schedule(); Task(TaskState::Spawned(task)) } - inner::(dispatcher, Box::pin(future)) + inner::(dispatcher, Box::pin(future), location) } } @@ -494,14 +507,16 @@ impl ForegroundExecutor { /// Copy-modified from: /// #[track_caller] -fn spawn_local_with_source_location( +fn spawn_local_with_source_location( future: Fut, schedule: S, -) -> (Runnable<()>, async_task::Task) + metadata: M, +) -> (Runnable, async_task::Task) where Fut: Future + 'static, Fut::Output: 'static, - S: async_task::Schedule<()> + Send + Sync + 'static, + S: async_task::Schedule + Send + Sync + 'static, + M: 'static, { #[inline] fn thread_id() -> ThreadId { @@ -549,7 +564,11 @@ where location: Location::caller(), }; - unsafe { async_task::spawn_unchecked(future, schedule) } + unsafe { + async_task::Builder::new() + .metadata(metadata) + .spawn_unchecked(move |_| future, schedule) + } } /// Scope manages a set of tasks that are enqueued and waited on together. See [`BackgroundExecutor::scoped`]. diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 805dbbdfe740acbac4929170953e70c923403bb7..6dcf32832b44ccceba72fa8554b8634373294bd6 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -71,6 +71,7 @@ pub use app::*; pub(crate) use arena::*; pub use asset_cache::*; pub use assets::*; +use collections::HashMap; pub use color::*; pub use ctor::ctor; pub use element::*; @@ -85,6 +86,7 @@ pub use inspector::*; pub use interactive::*; use key_dispatch::*; pub use keymap::*; +use parking_lot::Mutex; pub use path_builder::*; pub use platform::*; pub use refineable::*; @@ -107,7 +109,12 @@ pub use util::{FutureExt, Timeout, arc_cow::ArcCow}; pub use view::*; pub use window::*; -use std::{any::Any, borrow::BorrowMut, future::Future}; +use std::{ + any::Any, + borrow::BorrowMut, + future::Future, + sync::{Arc, LazyLock, atomic::AtomicUsize}, +}; use taffy::TaffyLayoutEngine; /// The context trait, allows the different contexts in GPUI to be used @@ -309,3 +316,43 @@ pub struct GpuSpecs { /// Further information about the driver, as reported by Vulkan. pub driver_info: String, } + +pub(crate) static FRAME_INDEX: AtomicUsize = AtomicUsize::new(0); + +/// A +#[derive(Debug, Clone)] +pub struct FrameTimings { + /// A + pub frame_time: f64, + /// A + pub timings: HashMap<&'static core::panic::Location<'static>, f64>, +} + +/// TESTING +pub static FRAME_RING: usize = 240; +pub(crate) static FRAME_BUF: LazyLock<[Arc>; FRAME_RING]> = + LazyLock::new(|| { + core::array::from_fn(|_| { + Arc::new(Mutex::new(FrameTimings { + frame_time: 0.0, + timings: HashMap::default(), + })) + }) + }); + +/// A +pub fn get_frame_timings() -> FrameTimings { + FRAME_BUF + [(FRAME_INDEX.load(std::sync::atomic::Ordering::Acquire) % FRAME_RING).saturating_sub(1)] + .lock() + .clone() +} + +/// A +pub fn get_all_timings() -> (Vec, usize) { + let frame_index = FRAME_INDEX.load(std::sync::atomic::Ordering::Acquire) % FRAME_RING; + ( + FRAME_BUF.iter().map(|frame| frame.lock().clone()).collect(), + frame_index, + ) +} diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index a5f4a368e377d0c43c43c101532feb3f34ae9fd6..091996ca682484569811828ee6ea71fd485ab442 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -556,14 +556,21 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { } } +/// Bungus +#[derive(Debug)] +pub struct Bigus { + /// Bungys 2 + pub location: &'static core::panic::Location<'static>, +} + /// This type is public so that our test macro can generate and use it, but it should not /// be considered part of our public API. #[doc(hidden)] pub trait PlatformDispatcher: Send + Sync { fn is_main_thread(&self) -> bool; - fn dispatch(&self, runnable: Runnable, label: Option); - fn dispatch_on_main_thread(&self, runnable: Runnable); - fn dispatch_after(&self, duration: Duration, runnable: Runnable); + fn dispatch(&self, runnable: Runnable, label: Option); + fn dispatch_on_main_thread(&self, runnable: Runnable); + fn dispatch_after(&self, duration: Duration, runnable: Runnable); fn now(&self) -> Instant { Instant::now() } diff --git a/crates/gpui/src/platform/windows/dispatcher.rs b/crates/gpui/src/platform/windows/dispatcher.rs index 8d3e6305f6b4bb60f6c282280bafa7f76f59eecb..8c140c289262d9204aae9326378feb1ecd6d4dc1 100644 --- a/crates/gpui/src/platform/windows/dispatcher.rs +++ b/crates/gpui/src/platform/windows/dispatcher.rs @@ -17,11 +17,11 @@ use windows::{ }; use crate::{ - HWND, PlatformDispatcher, SafeHwnd, TaskLabel, WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD, + Bigus, HWND, PlatformDispatcher, SafeHwnd, TaskLabel, WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD, }; pub(crate) struct WindowsDispatcher { - main_sender: Sender, + main_sender: Sender>, main_thread_id: ThreadId, platform_window_handle: SafeHwnd, validation_number: usize, @@ -29,7 +29,7 @@ pub(crate) struct WindowsDispatcher { impl WindowsDispatcher { pub(crate) fn new( - main_sender: Sender, + main_sender: Sender>, platform_window_handle: HWND, validation_number: usize, ) -> Self { @@ -44,7 +44,7 @@ impl WindowsDispatcher { } } - fn dispatch_on_threadpool(&self, runnable: Runnable) { + fn dispatch_on_threadpool(&self, runnable: Runnable) { let handler = { let mut task_wrapper = Some(runnable); WorkItemHandler::new(move |_| { @@ -55,7 +55,7 @@ impl WindowsDispatcher { ThreadPool::RunWithPriorityAsync(&handler, WorkItemPriority::High).log_err(); } - fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) { + fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) { let handler = { let mut task_wrapper = Some(runnable); TimerElapsedHandler::new(move |_| { @@ -72,14 +72,14 @@ impl PlatformDispatcher for WindowsDispatcher { current().id() == self.main_thread_id } - fn dispatch(&self, runnable: Runnable, label: Option) { + fn dispatch(&self, runnable: Runnable, label: Option) { self.dispatch_on_threadpool(runnable); if let Some(label) = label { log::debug!("TaskLabel: {label:?}"); } } - fn dispatch_on_main_thread(&self, runnable: Runnable) { + fn dispatch_on_main_thread(&self, runnable: Runnable) { match self.main_sender.send(runnable) { Ok(_) => unsafe { PostMessageW( @@ -104,7 +104,7 @@ impl PlatformDispatcher for WindowsDispatcher { } } - fn dispatch_after(&self, duration: Duration, runnable: Runnable) { + fn dispatch_after(&self, duration: Duration, runnable: Runnable) { self.dispatch_on_threadpool_after(runnable, duration); } } diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 9c10dcec4bb629bfbc78b76e74db099ed605d8be..c1b4e5d6b798a04d545bb3cd2ad4cb5410803845 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -1,4 +1,4 @@ -use std::rc::Rc; +use std::{rc::Rc, time::Instant}; use ::util::ResultExt; use anyhow::Context as _; @@ -240,9 +240,7 @@ impl WindowsWindowInner { fn handle_timer_msg(&self, handle: HWND, wparam: WPARAM) -> Option { if wparam.0 == SIZE_MOVE_LOOP_TIMER_ID { - for runnable in self.main_receiver.drain() { - runnable.run(); - } + drain_main_receiver(&self.main_receiver); self.handle_paint_msg(handle) } else { None @@ -1194,13 +1192,28 @@ impl WindowsWindowInner { #[inline] fn draw_window(&self, handle: HWND, force_render: bool) -> Option { + let cur_frame = FRAME_INDEX + .fetch_add(1, std::sync::atomic::Ordering::SeqCst) + .overflowing_add(1) + .0 + % FRAME_RING; + + FRAME_BUF[cur_frame].lock().timings.clear(); + let mut request_frame = self.state.borrow_mut().callbacks.request_frame.take()?; + let start = Instant::now(); request_frame(RequestFrameOptions { require_presentation: false, force_render, }); + let end = Instant::now(); + let duration = end.duration_since(start).as_secs_f64(); + self.state.borrow_mut().callbacks.request_frame = Some(request_frame); unsafe { ValidateRect(Some(handle), None).ok().log_err() }; + + FRAME_BUF[cur_frame].lock().frame_time = duration; + Some(0) } diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 361d8e114308323da8629fae93d257cc38147dba..9cc34cdae468abb5e6cb422254295291604c062e 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -4,15 +4,18 @@ use std::{ mem::ManuallyDrop, path::{Path, PathBuf}, rc::{Rc, Weak}, - sync::Arc, + sync::{Arc, LazyLock, atomic::AtomicUsize}, + time::Instant, }; use ::util::{ResultExt, paths::SanitizedPath}; use anyhow::{Context as _, Result, anyhow}; use async_task::Runnable; +use collections::HashMap; use futures::channel::oneshot::{self, Receiver}; use itertools::Itertools; -use parking_lot::RwLock; +use log::info; +use parking_lot::{Mutex, RwLock}; use smallvec::SmallVec; use windows::{ UI::ViewManagement::UISettings, @@ -47,7 +50,7 @@ struct WindowsPlatformInner { raw_window_handles: std::sync::Weak>>, // The below members will never change throughout the entire lifecycle of the app. validation_number: usize, - main_receiver: flume::Receiver, + main_receiver: flume::Receiver>, } pub(crate) struct WindowsPlatformState { @@ -93,7 +96,7 @@ impl WindowsPlatform { OleInitialize(None).context("unable to initialize Windows OLE")?; } let directx_devices = DirectXDevices::new().context("Creating DirectX devices")?; - let (main_sender, main_receiver) = flume::unbounded::(); + let (main_sender, main_receiver) = flume::unbounded::>(); let validation_number = if usize::BITS == 64 { rand::random::() as usize } else { @@ -746,9 +749,7 @@ impl WindowsPlatformInner { #[inline] fn run_foreground_task(&self) -> Option { - for runnable in self.main_receiver.drain() { - runnable.run(); - } + drain_main_receiver(&self.main_receiver); Some(0) } @@ -796,6 +797,26 @@ impl WindowsPlatformInner { } } +pub(crate) fn drain_main_receiver(main_receiver: &flume::Receiver>) { + let mut timings = HashMap::default(); + + for runnable in main_receiver.drain() { + let name = runnable.metadata().location; + let start = Instant::now(); + runnable.run(); + let end = Instant::now(); + *timings.entry(name).or_insert(0f64) += end.duration_since(start).as_secs_f64(); + } + + let frame_buf = { + let index = FRAME_INDEX.load(std::sync::atomic::Ordering::Acquire) % FRAME_RING; + let mut frame_buf = &*FRAME_BUF; + frame_buf[index].clone() + }; + let mut frame_buf = frame_buf.lock(); + frame_buf.timings.extend(timings); +} + impl Drop for WindowsPlatform { fn drop(&mut self) { unsafe { @@ -822,7 +843,7 @@ pub(crate) struct WindowCreationInfo { pub(crate) windows_version: WindowsVersion, pub(crate) drop_target_helper: IDropTargetHelper, pub(crate) validation_number: usize, - pub(crate) main_receiver: flume::Receiver, + pub(crate) main_receiver: flume::Receiver>, pub(crate) platform_window_handle: HWND, pub(crate) disable_direct_composition: bool, pub(crate) directx_devices: DirectXDevices, @@ -832,7 +853,7 @@ struct PlatformWindowCreateContext { inner: Option>>, raw_window_handles: std::sync::Weak>>, validation_number: usize, - main_receiver: Option>, + main_receiver: Option>>, directx_devices: Option, } diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index e765fa1a22d54a645d094f0df3250f75c94387af..40fb6af18004bafefe2288abf9feff1990e0fbbf 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -72,7 +72,7 @@ pub(crate) struct WindowsWindowInner { pub(crate) executor: ForegroundExecutor, pub(crate) windows_version: WindowsVersion, pub(crate) validation_number: usize, - pub(crate) main_receiver: flume::Receiver, + pub(crate) main_receiver: flume::Receiver>, pub(crate) platform_window_handle: HWND, } @@ -351,7 +351,7 @@ struct WindowCreateContext { windows_version: WindowsVersion, drop_target_helper: IDropTargetHelper, validation_number: usize, - main_receiver: flume::Receiver, + main_receiver: flume::Receiver>, platform_window_handle: HWND, appearance: WindowAppearance, disable_direct_composition: bool, diff --git a/crates/repl/src/repl.rs b/crates/repl/src/repl.rs index f6005f1ed73ac07697af48297643b30be113c83c..318ca48448f8aa6fd61a9aa7f2f35b1725925398 100644 --- a/crates/repl/src/repl.rs +++ b/crates/repl/src/repl.rs @@ -30,7 +30,7 @@ pub use crate::session::Session; pub const KERNEL_DOCS_URL: &str = "https://zed.dev/docs/repl#changing-kernels"; pub fn init(fs: Arc, cx: &mut App) { - set_dispatcher(zed_dispatcher(cx)); + // set_dispatcher(zed_dispatcher(cx)); JupyterSettings::register(cx); ::editor::init_settings(cx); ReplSettings::register(cx); @@ -38,26 +38,26 @@ pub fn init(fs: Arc, cx: &mut App) { ReplStore::init(fs, cx); } -fn zed_dispatcher(cx: &mut App) -> impl Dispatcher { - struct ZedDispatcher { - dispatcher: Arc, - } - - // PlatformDispatcher is _super_ close to the same interface we put in - // async-dispatcher, except for the task label in dispatch. Later we should - // just make that consistent so we have this dispatcher ready to go for - // other crates in Zed. - impl Dispatcher for ZedDispatcher { - fn dispatch(&self, runnable: Runnable) { - self.dispatcher.dispatch(runnable, None) - } - - fn dispatch_after(&self, duration: Duration, runnable: Runnable) { - self.dispatcher.dispatch_after(duration, runnable); - } - } - - ZedDispatcher { - dispatcher: cx.background_executor().dispatcher.clone(), - } -} +// fn zed_dispatcher(cx: &mut App) -> impl Dispatcher { +// struct ZedDispatcher { +// dispatcher: Arc, +// } + +// // PlatformDispatcher is _super_ close to the same interface we put in +// // async-dispatcher, except for the task label in dispatch. Later we should +// // just make that consistent so we have this dispatcher ready to go for +// // other crates in Zed. +// impl Dispatcher for ZedDispatcher { +// fn dispatch(&self, runnable: Runnable) { +// self.dispatcher.dispatch(runnable, None) +// } + +// fn dispatch_after(&self, duration: Duration, runnable: Runnable) { +// self.dispatcher.dispatch_after(duration, runnable); +// } +// } + +// ZedDispatcher { +// dispatcher: cx.background_executor().dispatcher.clone(), +// } +// } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 3cc6ec86473f09640f2ff35ae0457db65759d7c1..df42e8faf115509f66ae97c857a8beec2e027259 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -29,10 +29,11 @@ use git_ui::commit_view::CommitViewToolbar; use git_ui::git_panel::GitPanel; use git_ui::project_diff::ProjectDiffToolbar; use gpui::{ - Action, App, AppContext as _, Context, DismissEvent, Element, Entity, Focusable, KeyBinding, - ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString, Styled, Task, - TitlebarOptions, UpdateGlobal, Window, WindowKind, WindowOptions, actions, image_cache, point, - px, retain_all, + Action, App, AppContext as _, AsyncApp, Context, DismissEvent, Element, Entity, EventEmitter, + FRAME_RING, FocusHandle, Focusable, FrameTimings, Hsla, KeyBinding, MouseButton, ParentElement, + PathPromptOptions, PromptLevel, ReadGlobal, SharedString, Styled, Task, TitlebarOptions, + UpdateGlobal, Window, WindowKind, WindowOptions, actions, get_all_timings, get_frame_timings, + hsla, image_cache, point, px, retain_all, rgb, rgba, }; use image_viewer::ImageInfo; use language::Capability; @@ -72,12 +73,13 @@ use std::{ }; use terminal_view::terminal_panel::{self, TerminalPanel}; use theme::{ActiveTheme, GlobalTheme, SystemAppearance, ThemeRegistry, ThemeSettings}; -use ui::{PopoverMenuHandle, prelude::*}; +use ui::{PopoverMenuHandle, Tooltip, prelude::*}; use util::markdown::MarkdownString; use util::rel_path::RelPath; use util::{ResultExt, asset_str}; use uuid::Uuid; use vim_mode_setting::VimModeSetting; +use workspace::dock::PanelEvent; use workspace::notifications::{ NotificationId, SuppressEvent, dismiss_app_notification, show_app_notification, }; @@ -87,7 +89,7 @@ use workspace::{ open_new, }; use workspace::{ - CloseIntent, CloseWindow, NotificationFrame, RestoreBanner, with_active_or_new_workspace, + CloseIntent, CloseWindow, NotificationFrame, Panel, RestoreBanner, with_active_or_new_workspace, }; use workspace::{Pane, notifications::DetachAndPromptErr}; use zed_actions::{ @@ -568,6 +570,327 @@ fn show_software_emulation_warning_if_needed( } } +actions!(timings, [ToggleFocus,]); + +enum DataMode { + Realtime(Option), + Capture { + selected_index: usize, + data: Vec, + }, +} + +struct TimingsPanel { + position: workspace::dock::DockPosition, + focus_handle: FocusHandle, + data: DataMode, + width: Option, + _refresh: Option>, +} + +impl TimingsPanel { + fn new(cx: &mut App) -> Entity { + let entity = cx.new(|cx| Self { + position: workspace::dock::DockPosition::Right, + focus_handle: cx.focus_handle(), + data: DataMode::Realtime(None), + width: None, + _refresh: Some(Self::begin_listen(cx)), + }); + + entity + } + + fn begin_listen(cx: &mut Context) -> Task<()> { + cx.spawn(async move |this, cx| { + loop { + let data = get_frame_timings(); + + this.update(cx, |this: &mut TimingsPanel, cx| { + this.data = DataMode::Realtime(Some(data)); + cx.notify(); + }); + + cx.background_executor() + .timer(Duration::from_micros(1)) + .await; + } + }) + } + + fn get_timings(&self) -> Option<&FrameTimings> { + match &self.data { + DataMode::Realtime(data) => data.as_ref(), + DataMode::Capture { + data, + selected_index, + } => Some(&data[*selected_index]), + } + } + + fn render_bar(&self, max_value: f32, item: BarChartItem, cx: &App) -> impl IntoElement { + let fill_width = (item.value / max_value).max(0.02); // Minimum 2% width for visibility + let label = format!( + "{}:{}:{}", + item.location + .file() + .rsplit_once("/") + .unwrap_or(("", item.location.file())) + .1 + .rsplit_once("\\") + .unwrap_or(("", item.location.file())) + .1, + item.location.line(), + item.location.column() + ); + + h_flex() + .gap_2() + .w_full() + .h(px(32.0)) // Slightly taller for better visibility + .child( + // Label with flexible width and truncation + div() + .min_w(px(80.0)) + .max_w(px(200.0)) // Maximum width for labels + .flex_shrink_0() + .overflow_hidden() + .child(div().text_ellipsis().child(label)), + ) + .child( + // Bar container + div() + .flex_1() + .h(px(24.0)) + .bg(cx.theme().colors().background) + .rounded_md() + .p(px(2.0)) + .child( + // Bar fill with minimum width + div() + .h_full() + .rounded_sm() + .bg(item.color) + .min_w(px(4.0)) // Minimum width so tiny values are still visible + .w(relative(fill_width)), + ), + ) + .child( + // Value label - right-aligned + div() + .min_w(px(60.0)) + .flex_shrink_0() + .text_right() + .child(format!("{:.1}", fill_width)), + ) + } +} + +#[derive(IntoElement)] +struct DiscreteSlider { + value: usize, + count: usize, + on_change: Arc, +} + +impl DiscreteSlider { + pub fn new( + value: usize, + count: usize, + on_change: impl Fn(usize, &mut Window, &mut App) + 'static, + ) -> Self { + Self { + value: value.min(count - 1), + count, + on_change: Arc::new(on_change), + } + } +} + +impl RenderOnce for DiscreteSlider { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + h_flex() + .id("discrete-slider") + .overflow_scroll() + .w_full() + .p_2() + .children((0..self.count).map(|i| { + let is_active = i == self.value; + let on_change = self.on_change.clone(); + + div() + .w(px(24.0)) + .h(px(24.0)) + .rounded_md() + .cursor_pointer() + .bg(if is_active { + cx.theme().accents().color_for_index(i as u32) + } else { + cx.theme().colors().element_background + }) + .hover(|this| this.bg(cx.theme().colors().element_hover)) + .on_mouse_down(MouseButton::Left, { + let on_change = Arc::clone(&on_change); + move |_, window, cx| { + on_change(i, window, cx); + } + }) + // This handles drag selection + .on_mouse_move({ + move |event, window, cx| { + // If mouse button is held down + if event.pressed_button == Some(MouseButton::Left) { + on_change(i, window, cx); + } + } + }) + })) + } +} + +struct BarChartItem { + location: &'static core::panic::Location<'static>, + value: f32, + color: Hsla, +} + +impl Panel for TimingsPanel { + fn persistent_name() -> &'static str { + "Timings" + } + + fn panel_key() -> &'static str { + "timings-panel" + } + + fn position(&self, window: &Window, cx: &App) -> workspace::dock::DockPosition { + self.position + } + + fn position_is_valid(&self, position: workspace::dock::DockPosition) -> bool { + true + } + + fn set_position( + &mut self, + position: workspace::dock::DockPosition, + window: &mut Window, + cx: &mut Context, + ) { + self.position = position; + cx.notify(); + } + + fn size(&self, window: &Window, cx: &App) -> Pixels { + self.width.unwrap_or(px(200.0)) + } + + fn set_size(&mut self, size: Option, window: &mut Window, cx: &mut Context) { + self.width = size; + cx.notify(); + } + + fn icon(&self, window: &Window, cx: &App) -> Option { + Some(ui::IconName::Envelope) + } + + fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str> { + Some("Timings Panel") + } + + fn toggle_action(&self) -> Box { + Box::new(ToggleFocus) + } + + fn activation_priority(&self) -> u32 { + 2 + } +} + +impl Render for TimingsPanel { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .w_full() + .gap_2() + .child( + Button::new( + "switch-mode", + match self.data { + DataMode::Capture { .. } => "Realtime", + DataMode::Realtime(_) => "Capture", + }, + ) + .style(ButtonStyle::Filled) + .on_click(cx.listener(|this, _, window, cx| { + match this.data { + DataMode::Realtime(_) => { + let (data, selected_index) = get_all_timings(); + this._refresh = None; + this.data = DataMode::Capture { + selected_index, + data, + }; + } + DataMode::Capture { .. } => { + this._refresh = Some(Self::begin_listen(cx)); + this.data = DataMode::Realtime(None); + } + }; + cx.notify(); + })), + ) + .when(matches!(self.data, DataMode::Capture { .. }), |this| { + let DataMode::Capture { + selected_index, + data, + } = &self.data + else { + unreachable!(); + }; + + let entity = cx.entity().downgrade(); + this.child(DiscreteSlider::new( + *selected_index, + data.len(), + move |value, _window, cx| { + if let Some(entity) = entity.upgrade() { + entity.update(cx, |this, cx| { + if let DataMode::Capture { selected_index, .. } = &mut this.data { + *selected_index = value; + cx.notify(); + } + }); + } + }, + )) + }) + .when_some(self.get_timings(), |div, e| { + div.child(format!("{}", e.frame_time)) + .children(e.timings.iter().enumerate().map(|(i, (name, value))| { + self.render_bar( + e.frame_time as f32, + BarChartItem { + location: name, + value: *value as f32, + color: cx.theme().accents().color_for_index(i as u32), + }, + cx, + ) + })) + }) + } +} + +impl Focusable for TimingsPanel { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl EventEmitter for TimingsPanel {} + +impl EventEmitter for TimingsPanel {} + fn initialize_panels( prompt_builder: Arc, window: &mut Window, @@ -585,6 +908,15 @@ fn initialize_panels( cx.clone(), ); let debug_panel = DebugPanel::load(workspace_handle.clone(), cx); + let timings_panel = workspace_handle + .update_in(cx, |workspace, window, cx| TimingsPanel::new(cx)) + .unwrap(); + + workspace_handle.update_in(cx, |workspace, window, cx| { + workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { + workspace.toggle_panel_focus::(window, cx); + }); + }); let ( project_panel, @@ -612,6 +944,7 @@ fn initialize_panels( workspace.add_panel(channels_panel, window, cx); workspace.add_panel(notification_panel, window, cx); workspace.add_panel(debug_panel, window, cx); + workspace.add_panel(timings_panel, window, cx); })?; fn setup_or_teardown_agent_panel(