initial mini-profiler implementation

Kate created

Change summary

crates/gpui/src/executor.rs                    |  53 ++
crates/gpui/src/gpui.rs                        |  49 ++
crates/gpui/src/platform.rs                    |  13 
crates/gpui/src/platform/windows/dispatcher.rs |  16 
crates/gpui/src/platform/windows/events.rs     |  21 
crates/gpui/src/platform/windows/platform.rs   |  39 +
crates/gpui/src/platform/windows/window.rs     |   4 
crates/repl/src/repl.rs                        |  48 +-
crates/zed/src/zed.rs                          | 345 +++++++++++++++++++
9 files changed, 514 insertions(+), 74 deletions(-)

Detailed changes

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<T> {
     Ready(Option<T>),
 
     /// A task that is currently running.
-    Spawned(async_task::Task<T>),
+    Spawned(async_task::Task<T, Bigus>),
 }
 
 impl<T> Task<T> {
@@ -168,8 +168,13 @@ impl BackgroundExecutor {
         label: Option<TaskLabel>,
     ) -> Task<R> {
         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<R>(&self, future: impl Future<Output = R> + 'static) -> Task<R>
+    pub fn spawn<F, R>(&self, future: F) -> Task<R>
     where
+        F: Future<Output = R> + 'static,
         R: 'static,
     {
         let dispatcher = self.dispatcher.clone();
+        let location = core::panic::Location::caller();
 
         #[track_caller]
         fn inner<R: 'static>(
             dispatcher: Arc<dyn PlatformDispatcher>,
             future: AnyLocalFuture<R>,
+            location: &'static core::panic::Location<'static>,
         ) -> Task<R> {
-            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::<R>(dispatcher, Box::pin(future))
+        inner::<R>(dispatcher, Box::pin(future), location)
     }
 }
 
@@ -494,14 +507,16 @@ impl ForegroundExecutor {
 /// Copy-modified from:
 /// <https://github.com/smol-rs/async-task/blob/ca9dbe1db9c422fd765847fa91306e30a6bb58a9/src/runnable.rs#L405>
 #[track_caller]
-fn spawn_local_with_source_location<Fut, S>(
+fn spawn_local_with_source_location<Fut, S, M>(
     future: Fut,
     schedule: S,
-) -> (Runnable<()>, async_task::Task<Fut::Output, ()>)
+    metadata: M,
+) -> (Runnable<M>, async_task::Task<Fut::Output, M>)
 where
     Fut: Future + 'static,
     Fut::Output: 'static,
-    S: async_task::Schedule<()> + Send + Sync + 'static,
+    S: async_task::Schedule<M> + 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`].

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<Mutex<FrameTimings>>; 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<FrameTimings>, 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,
+    )
+}

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<TaskLabel>);
-    fn dispatch_on_main_thread(&self, runnable: Runnable);
-    fn dispatch_after(&self, duration: Duration, runnable: Runnable);
+    fn dispatch(&self, runnable: Runnable<Bigus>, label: Option<TaskLabel>);
+    fn dispatch_on_main_thread(&self, runnable: Runnable<Bigus>);
+    fn dispatch_after(&self, duration: Duration, runnable: Runnable<Bigus>);
     fn now(&self) -> Instant {
         Instant::now()
     }

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<Runnable>,
+    main_sender: Sender<Runnable<Bigus>>,
     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<Runnable>,
+        main_sender: Sender<Runnable<Bigus>>,
         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<Bigus>) {
         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<Bigus>, 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<TaskLabel>) {
+    fn dispatch(&self, runnable: Runnable<Bigus>, label: Option<TaskLabel>) {
         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<Bigus>) {
         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<Bigus>) {
         self.dispatch_on_threadpool_after(runnable, duration);
     }
 }

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<isize> {
         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<isize> {
+        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)
     }
 

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<RwLock<SmallVec<[SafeHwnd; 4]>>>,
     // The below members will never change throughout the entire lifecycle of the app.
     validation_number: usize,
-    main_receiver: flume::Receiver<Runnable>,
+    main_receiver: flume::Receiver<Runnable<Bigus>>,
 }
 
 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::<Runnable>();
+        let (main_sender, main_receiver) = flume::unbounded::<Runnable<Bigus>>();
         let validation_number = if usize::BITS == 64 {
             rand::random::<u64>() as usize
         } else {
@@ -746,9 +749,7 @@ impl WindowsPlatformInner {
 
     #[inline]
     fn run_foreground_task(&self) -> Option<isize> {
-        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<Runnable<Bigus>>) {
+    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<Runnable>,
+    pub(crate) main_receiver: flume::Receiver<Runnable<Bigus>>,
     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<Result<Rc<WindowsPlatformInner>>>,
     raw_window_handles: std::sync::Weak<RwLock<SmallVec<[SafeHwnd; 4]>>>,
     validation_number: usize,
-    main_receiver: Option<flume::Receiver<Runnable>>,
+    main_receiver: Option<flume::Receiver<Runnable<Bigus>>>,
     directx_devices: Option<DirectXDevices>,
 }
 

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<Runnable>,
+    pub(crate) main_receiver: flume::Receiver<Runnable<Bigus>>,
     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<Runnable>,
+    main_receiver: flume::Receiver<Runnable<Bigus>>,
     platform_window_handle: HWND,
     appearance: WindowAppearance,
     disable_direct_composition: bool,

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<dyn Fs>, 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<dyn Fs>, cx: &mut App) {
     ReplStore::init(fs, cx);
 }
 
-fn zed_dispatcher(cx: &mut App) -> impl Dispatcher {
-    struct ZedDispatcher {
-        dispatcher: Arc<dyn PlatformDispatcher>,
-    }
-
-    // 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<dyn PlatformDispatcher>,
+//     }
+
+//     // 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(),
+//     }
+// }

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<FrameTimings>),
+    Capture {
+        selected_index: usize,
+        data: Vec<FrameTimings>,
+    },
+}
+
+struct TimingsPanel {
+    position: workspace::dock::DockPosition,
+    focus_handle: FocusHandle,
+    data: DataMode,
+    width: Option<Pixels>,
+    _refresh: Option<Task<()>>,
+}
+
+impl TimingsPanel {
+    fn new(cx: &mut App) -> Entity<Self> {
+        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<Self>) -> 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<dyn Fn(usize, &mut Window, &mut App)>,
+}
+
+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>,
+    ) {
+        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<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
+        self.width = size;
+        cx.notify();
+    }
+
+    fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName> {
+        Some(ui::IconName::Envelope)
+    }
+
+    fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str> {
+        Some("Timings Panel")
+    }
+
+    fn toggle_action(&self) -> Box<dyn Action> {
+        Box::new(ToggleFocus)
+    }
+
+    fn activation_priority(&self) -> u32 {
+        2
+    }
+}
+
+impl Render for TimingsPanel {
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> 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<workspace::Event> for TimingsPanel {}
+
+impl EventEmitter<PanelEvent> for TimingsPanel {}
+
 fn initialize_panels(
     prompt_builder: Arc<PromptBuilder>,
     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::<TimingsPanel>(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(