Add a setting to show time to first window draw and frames per second in status bar (#16422)

Nathan Sobo , Max Brunsfeld , Kirill Bulatov , David Soria Parra , and Danny Hua created

I want to showcase Zed's performance via videos, and this seemed like a
good way to demonstrate it.


https://github.com/user-attachments/assets/f4a5fabc-efe7-4b48-9ba5-719882fdc856

Release Notes:

- On macOS, you can now set assign `performance.show_in_status_bar:
true` in your settings to show the time to the first window draw on
startup and then current FPS of the containing window's renderer.

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: David Soria Parra <167242713+dsp-ant@users.noreply.github.com>
Co-authored-by: Danny Hua <danny.hua@hey.com>

Change summary

Cargo.lock                                       |  16 +
Cargo.toml                                       |   2 
crates/gpui/src/app.rs                           |  27 ++
crates/gpui/src/platform.rs                      |   5 
crates/gpui/src/platform/blade/blade_renderer.rs |  13 +
crates/gpui/src/platform/fps.rs                  |  94 ++++++++
crates/gpui/src/platform/linux/wayland/window.rs |  12 
crates/gpui/src/platform/linux/x11/window.rs     |  13 
crates/gpui/src/platform/mac/metal_renderer.rs   |  37 ++
crates/gpui/src/platform/mac/window.rs           |  68 +++---
crates/gpui/src/platform/test/window.rs          |  11 
crates/gpui/src/platform/windows/window.rs       |   8 
crates/gpui/src/window.rs                        |  33 ++
crates/performance/Cargo.toml                    |  36 +++
crates/performance/LICENSE-GPL                   |   1 
crates/performance/src/performance.rs            | 189 ++++++++++++++++++
crates/workspace/src/status_bar.rs               |  11 +
crates/workspace/src/workspace.rs                |   2 
crates/zed/Cargo.toml                            |   1 
crates/zed/src/main.rs                           |   6 
20 files changed, 524 insertions(+), 61 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -7592,6 +7592,21 @@ version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
 
+[[package]]
+name = "performance"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "collections",
+ "gpui",
+ "log",
+ "schemars",
+ "serde",
+ "settings",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "pest"
 version = "2.7.11"
@@ -13877,6 +13892,7 @@ dependencies = [
  "outline_panel",
  "parking_lot",
  "paths",
+ "performance",
  "profiling",
  "project",
  "project_panel",

Cargo.toml 🔗

@@ -70,6 +70,7 @@ members = [
     "crates/outline",
     "crates/outline_panel",
     "crates/paths",
+    "crates/performance",
     "crates/picker",
     "crates/prettier",
     "crates/project",
@@ -241,6 +242,7 @@ open_ai = { path = "crates/open_ai" }
 outline = { path = "crates/outline" }
 outline_panel = { path = "crates/outline_panel" }
 paths = { path = "crates/paths" }
+performance = { path = "crates/performance" }
 picker = { path = "crates/picker" }
 plugin = { path = "crates/plugin" }
 plugin_macros = { path = "crates/plugin_macros" }

crates/gpui/src/app.rs 🔗

@@ -6,7 +6,7 @@ use std::{
     path::{Path, PathBuf},
     rc::{Rc, Weak},
     sync::{atomic::Ordering::SeqCst, Arc},
-    time::Duration,
+    time::{Duration, Instant},
 };
 
 use anyhow::{anyhow, Result};
@@ -142,6 +142,12 @@ impl App {
         self
     }
 
+    /// Sets a start time for tracking time to first window draw.
+    pub fn measure_time_to_first_window_draw(self, start: Instant) -> Self {
+        self.0.borrow_mut().time_to_first_window_draw = Some(TimeToFirstWindowDraw::Pending(start));
+        self
+    }
+
     /// Start the application. The provided callback will be called once the
     /// app is fully launched.
     pub fn run<F>(self, on_finish_launching: F)
@@ -247,6 +253,7 @@ pub struct AppContext {
     pub(crate) layout_id_buffer: Vec<LayoutId>, // We recycle this memory across layout requests.
     pub(crate) propagate_event: bool,
     pub(crate) prompt_builder: Option<PromptBuilder>,
+    pub(crate) time_to_first_window_draw: Option<TimeToFirstWindowDraw>,
 }
 
 impl AppContext {
@@ -300,6 +307,7 @@ impl AppContext {
                 layout_id_buffer: Default::default(),
                 propagate_event: true,
                 prompt_builder: Some(PromptBuilder::Default),
+                time_to_first_window_draw: None,
             }),
         });
 
@@ -1302,6 +1310,14 @@ impl AppContext {
 
         (task, is_first)
     }
+
+    /// Returns the time to first window draw, if available.
+    pub fn time_to_first_window_draw(&self) -> Option<Duration> {
+        match self.time_to_first_window_draw {
+            Some(TimeToFirstWindowDraw::Done(duration)) => Some(duration),
+            _ => None,
+        }
+    }
 }
 
 impl Context for AppContext {
@@ -1465,6 +1481,15 @@ impl<G: Global> DerefMut for GlobalLease<G> {
     }
 }
 
+/// Represents the initialization duration of the application.
+#[derive(Clone, Copy)]
+pub enum TimeToFirstWindowDraw {
+    /// The application is still initializing, and contains the start time.
+    Pending(Instant),
+    /// The application has finished initializing, and contains the total duration.
+    Done(Duration),
+}
+
 /// Contains state associated with an active drag operation, started by dragging an element
 /// within the window or by dragging into the app from the underlying platform.
 pub struct AnyDrag {

crates/gpui/src/platform.rs 🔗

@@ -16,6 +16,7 @@ mod blade;
 #[cfg(any(test, feature = "test-support"))]
 mod test;
 
+mod fps;
 #[cfg(target_os = "windows")]
 mod windows;
 
@@ -51,6 +52,7 @@ use strum::EnumIter;
 use uuid::Uuid;
 
 pub use app_menu::*;
+pub use fps::*;
 pub use keystroke::*;
 
 #[cfg(target_os = "linux")]
@@ -354,7 +356,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
     fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
     fn on_close(&self, callback: Box<dyn FnOnce()>);
     fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
-    fn draw(&self, scene: &Scene);
+    fn draw(&self, scene: &Scene, on_complete: Option<oneshot::Sender<()>>);
     fn completed_frame(&self) {}
     fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
 
@@ -379,6 +381,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
     }
     fn set_client_inset(&self, _inset: Pixels) {}
     fn gpu_specs(&self) -> Option<GPUSpecs>;
+    fn fps(&self) -> Option<f32>;
 
     #[cfg(any(test, feature = "test-support"))]
     fn as_test(&mut self) -> Option<&mut TestWindow> {

crates/gpui/src/platform/blade/blade_renderer.rs 🔗

@@ -9,6 +9,7 @@ use crate::{
 };
 use bytemuck::{Pod, Zeroable};
 use collections::HashMap;
+use futures::channel::oneshot;
 #[cfg(target_os = "macos")]
 use media::core_video::CVMetalTextureCache;
 #[cfg(target_os = "macos")]
@@ -537,7 +538,12 @@ impl BladeRenderer {
         self.gpu.destroy_command_encoder(&mut self.command_encoder);
     }
 
-    pub fn draw(&mut self, scene: &Scene) {
+    pub fn draw(
+        &mut self,
+        scene: &Scene,
+        // Required to compile on macOS, but not currently supported.
+        _on_complete: Option<oneshot::Sender<()>>,
+    ) {
         self.command_encoder.start();
         self.atlas.before_frame(&mut self.command_encoder);
         self.rasterize_paths(scene.paths());
@@ -766,4 +772,9 @@ impl BladeRenderer {
         self.wait_for_gpu();
         self.last_sync_point = Some(sync_point);
     }
+
+    /// Required to compile on macOS, but not currently supported.
+    pub fn fps(&self) -> f32 {
+        0.0
+    }
 }

crates/gpui/src/platform/fps.rs 🔗

@@ -0,0 +1,94 @@
+use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
+use std::sync::Arc;
+
+const NANOS_PER_SEC: u64 = 1_000_000_000;
+const WINDOW_SIZE: usize = 128;
+
+/// Represents a rolling FPS (Frames Per Second) counter.
+///
+/// This struct provides a lock-free mechanism to measure and calculate FPS
+/// continuously, updating with every frame. It uses atomic operations to
+/// ensure thread-safety without the need for locks.
+pub struct FpsCounter {
+    frame_times: [AtomicU64; WINDOW_SIZE],
+    head: AtomicUsize,
+    tail: AtomicUsize,
+}
+
+impl FpsCounter {
+    /// Creates a new `Fps` counter.
+    ///
+    /// Returns an `Arc<Fps>` for safe sharing across threads.
+    pub fn new() -> Arc<Self> {
+        Arc::new(Self {
+            frame_times: std::array::from_fn(|_| AtomicU64::new(0)),
+            head: AtomicUsize::new(0),
+            tail: AtomicUsize::new(0),
+        })
+    }
+
+    /// Increments the FPS counter with a new frame timestamp.
+    ///
+    /// This method updates the internal state to maintain a rolling window
+    /// of frame data for the last second. It uses atomic operations to
+    /// ensure thread-safety.
+    ///
+    /// # Arguments
+    ///
+    /// * `timestamp_ns` - The timestamp of the new frame in nanoseconds.
+    pub fn increment(&self, timestamp_ns: u64) {
+        let mut head = self.head.load(Ordering::Relaxed);
+        let mut tail = self.tail.load(Ordering::Relaxed);
+
+        // Add new timestamp
+        self.frame_times[head].store(timestamp_ns, Ordering::Relaxed);
+        // Increment head and wrap around to 0 if it reaches WINDOW_SIZE
+        head = (head + 1) % WINDOW_SIZE;
+        self.head.store(head, Ordering::Relaxed);
+
+        // Remove old timestamps (older than 1 second)
+        while tail != head {
+            let oldest = self.frame_times[tail].load(Ordering::Relaxed);
+            if timestamp_ns.wrapping_sub(oldest) <= NANOS_PER_SEC {
+                break;
+            }
+            // Increment tail and wrap around to 0 if it reaches WINDOW_SIZE
+            tail = (tail + 1) % WINDOW_SIZE;
+            self.tail.store(tail, Ordering::Relaxed);
+        }
+    }
+
+    /// Calculates and returns the current FPS.
+    ///
+    /// This method computes the FPS based on the frames recorded in the last second.
+    /// It uses atomic loads to ensure thread-safety.
+    ///
+    /// # Returns
+    ///
+    /// The calculated FPS as a `f32`, or 0.0 if no frames have been recorded.
+    pub fn fps(&self) -> f32 {
+        let head = self.head.load(Ordering::Relaxed);
+        let tail = self.tail.load(Ordering::Relaxed);
+
+        if head == tail {
+            return 0.0;
+        }
+
+        let newest =
+            self.frame_times[head.wrapping_sub(1) & (WINDOW_SIZE - 1)].load(Ordering::Relaxed);
+        let oldest = self.frame_times[tail].load(Ordering::Relaxed);
+
+        let time_diff = newest.wrapping_sub(oldest) as f32;
+        if time_diff == 0.0 {
+            return 0.0;
+        }
+
+        let frame_count = if head > tail {
+            head - tail
+        } else {
+            WINDOW_SIZE - tail + head
+        };
+
+        (frame_count as f32 - 1.0) * NANOS_PER_SEC as f32 / time_diff
+    }
+}

crates/gpui/src/platform/linux/wayland/window.rs 🔗

@@ -6,7 +6,7 @@ use std::sync::Arc;
 
 use blade_graphics as gpu;
 use collections::HashMap;
-use futures::channel::oneshot::Receiver;
+use futures::channel::oneshot;
 
 use raw_window_handle as rwh;
 use wayland_backend::client::ObjectId;
@@ -827,7 +827,7 @@ impl PlatformWindow for WaylandWindow {
         _msg: &str,
         _detail: Option<&str>,
         _answers: &[&str],
-    ) -> Option<Receiver<usize>> {
+    ) -> Option<oneshot::Receiver<usize>> {
         None
     }
 
@@ -934,9 +934,9 @@ impl PlatformWindow for WaylandWindow {
         self.0.callbacks.borrow_mut().appearance_changed = Some(callback);
     }
 
-    fn draw(&self, scene: &Scene) {
+    fn draw(&self, scene: &Scene, on_complete: Option<oneshot::Sender<()>>) {
         let mut state = self.borrow_mut();
-        state.renderer.draw(scene);
+        state.renderer.draw(scene, on_complete);
     }
 
     fn completed_frame(&self) {
@@ -1009,6 +1009,10 @@ impl PlatformWindow for WaylandWindow {
     fn gpu_specs(&self) -> Option<GPUSpecs> {
         self.borrow().renderer.gpu_specs().into()
     }
+
+    fn fps(&self) -> Option<f32> {
+        None
+    }
 }
 
 fn update_window(mut state: RefMut<WaylandWindowState>) {

crates/gpui/src/platform/linux/x11/window.rs 🔗

@@ -1,5 +1,3 @@
-use anyhow::Context;
-
 use crate::{
     platform::blade::{BladeRenderer, BladeSurfaceConfig},
     px, size, AnyWindowHandle, Bounds, Decorations, DevicePixels, ForegroundExecutor, GPUSpecs,
@@ -9,7 +7,9 @@ use crate::{
     X11ClientStatePtr,
 };
 
+use anyhow::Context;
 use blade_graphics as gpu;
+use futures::channel::oneshot;
 use raw_window_handle as rwh;
 use util::{maybe, ResultExt};
 use x11rb::{
@@ -1210,9 +1210,10 @@ impl PlatformWindow for X11Window {
         self.0.callbacks.borrow_mut().appearance_changed = Some(callback);
     }
 
-    fn draw(&self, scene: &Scene) {
+    // TODO: on_complete not yet supported for X11 windows
+    fn draw(&self, scene: &Scene, on_complete: Option<oneshot::Sender<()>>) {
         let mut inner = self.0.state.borrow_mut();
-        inner.renderer.draw(scene);
+        inner.renderer.draw(scene, on_complete);
     }
 
     fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
@@ -1398,4 +1399,8 @@ impl PlatformWindow for X11Window {
     fn gpu_specs(&self) -> Option<GPUSpecs> {
         self.0.state.borrow().renderer.gpu_specs().into()
     }
+
+    fn fps(&self) -> Option<f32> {
+        None
+    }
 }

crates/gpui/src/platform/mac/metal_renderer.rs 🔗

@@ -1,7 +1,7 @@
 use super::metal_atlas::MetalAtlas;
 use crate::{
     point, size, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels,
-    Hsla, MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite,
+    FpsCounter, Hsla, MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite,
     PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline,
 };
 use anyhow::{anyhow, Result};
@@ -14,6 +14,7 @@ use cocoa::{
 use collections::HashMap;
 use core_foundation::base::TCFType;
 use foreign_types::ForeignType;
+use futures::channel::oneshot;
 use media::core_video::CVMetalTextureCache;
 use metal::{CAMetalLayer, CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange};
 use objc::{self, msg_send, sel, sel_impl};
@@ -105,6 +106,7 @@ pub(crate) struct MetalRenderer {
     instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>,
     sprite_atlas: Arc<MetalAtlas>,
     core_video_texture_cache: CVMetalTextureCache,
+    fps_counter: Arc<FpsCounter>,
 }
 
 impl MetalRenderer {
@@ -250,6 +252,7 @@ impl MetalRenderer {
             instance_buffer_pool,
             sprite_atlas,
             core_video_texture_cache,
+            fps_counter: FpsCounter::new(),
         }
     }
 
@@ -292,7 +295,8 @@ impl MetalRenderer {
         // nothing to do
     }
 
-    pub fn draw(&mut self, scene: &Scene) {
+    pub fn draw(&mut self, scene: &Scene, on_complete: Option<oneshot::Sender<()>>) {
+        let on_complete = Arc::new(Mutex::new(on_complete));
         let layer = self.layer.clone();
         let viewport_size = layer.drawable_size();
         let viewport_size: Size<DevicePixels> = size(
@@ -319,13 +323,24 @@ impl MetalRenderer {
                 Ok(command_buffer) => {
                     let instance_buffer_pool = self.instance_buffer_pool.clone();
                     let instance_buffer = Cell::new(Some(instance_buffer));
-                    let block = ConcreteBlock::new(move |_| {
-                        if let Some(instance_buffer) = instance_buffer.take() {
-                            instance_buffer_pool.lock().release(instance_buffer);
-                        }
-                    });
-                    let block = block.copy();
-                    command_buffer.add_completed_handler(&block);
+                    let device = self.device.clone();
+                    let fps_counter = self.fps_counter.clone();
+                    let completed_handler =
+                        ConcreteBlock::new(move |_: &metal::CommandBufferRef| {
+                            let mut cpu_timestamp = 0;
+                            let mut gpu_timestamp = 0;
+                            device.sample_timestamps(&mut cpu_timestamp, &mut gpu_timestamp);
+
+                            fps_counter.increment(gpu_timestamp);
+                            if let Some(on_complete) = on_complete.lock().take() {
+                                on_complete.send(()).ok();
+                            }
+                            if let Some(instance_buffer) = instance_buffer.take() {
+                                instance_buffer_pool.lock().release(instance_buffer);
+                            }
+                        });
+                    let completed_handler = completed_handler.copy();
+                    command_buffer.add_completed_handler(&completed_handler);
 
                     if self.presents_with_transaction {
                         command_buffer.commit();
@@ -1117,6 +1132,10 @@ impl MetalRenderer {
         }
         true
     }
+
+    pub fn fps(&self) -> f32 {
+        self.fps_counter.fps()
+    }
 }
 
 fn build_pipeline_state(

crates/gpui/src/platform/mac/window.rs 🔗

@@ -784,14 +784,14 @@ impl PlatformWindow for MacWindow {
         self.0.as_ref().lock().bounds()
     }
 
-    fn window_bounds(&self) -> WindowBounds {
-        self.0.as_ref().lock().window_bounds()
-    }
-
     fn is_maximized(&self) -> bool {
         self.0.as_ref().lock().is_maximized()
     }
 
+    fn window_bounds(&self) -> WindowBounds {
+        self.0.as_ref().lock().window_bounds()
+    }
+
     fn content_size(&self) -> Size<Pixels> {
         self.0.as_ref().lock().content_size()
     }
@@ -975,8 +975,6 @@ impl PlatformWindow for MacWindow {
         }
     }
 
-    fn set_app_id(&mut self, _app_id: &str) {}
-
     fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
         let mut this = self.0.as_ref().lock();
         this.renderer
@@ -1007,30 +1005,6 @@ impl PlatformWindow for MacWindow {
         }
     }
 
-    fn set_edited(&mut self, edited: bool) {
-        unsafe {
-            let window = self.0.lock().native_window;
-            msg_send![window, setDocumentEdited: edited as BOOL]
-        }
-
-        // Changing the document edited state resets the traffic light position,
-        // so we have to move it again.
-        self.0.lock().move_traffic_light();
-    }
-
-    fn show_character_palette(&self) {
-        let this = self.0.lock();
-        let window = this.native_window;
-        this.executor
-            .spawn(async move {
-                unsafe {
-                    let app = NSApplication::sharedApplication(nil);
-                    let _: () = msg_send![app, orderFrontCharacterPalette: window];
-                }
-            })
-            .detach();
-    }
-
     fn minimize(&self) {
         let window = self.0.lock().native_window;
         unsafe {
@@ -1107,18 +1081,48 @@ impl PlatformWindow for MacWindow {
         self.0.lock().appearance_changed_callback = Some(callback);
     }
 
-    fn draw(&self, scene: &crate::Scene) {
+    fn draw(&self, scene: &crate::Scene, on_complete: Option<oneshot::Sender<()>>) {
         let mut this = self.0.lock();
-        this.renderer.draw(scene);
+        this.renderer.draw(scene, on_complete);
     }
 
     fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
         self.0.lock().renderer.sprite_atlas().clone()
     }
 
+    fn set_edited(&mut self, edited: bool) {
+        unsafe {
+            let window = self.0.lock().native_window;
+            msg_send![window, setDocumentEdited: edited as BOOL]
+        }
+
+        // Changing the document edited state resets the traffic light position,
+        // so we have to move it again.
+        self.0.lock().move_traffic_light();
+    }
+
+    fn show_character_palette(&self) {
+        let this = self.0.lock();
+        let window = this.native_window;
+        this.executor
+            .spawn(async move {
+                unsafe {
+                    let app = NSApplication::sharedApplication(nil);
+                    let _: () = msg_send![app, orderFrontCharacterPalette: window];
+                }
+            })
+            .detach();
+    }
+
+    fn set_app_id(&mut self, _app_id: &str) {}
+
     fn gpu_specs(&self) -> Option<crate::GPUSpecs> {
         None
     }
+
+    fn fps(&self) -> Option<f32> {
+        Some(self.0.lock().renderer.fps())
+    }
 }
 
 impl rwh::HasWindowHandle for MacWindow {

crates/gpui/src/platform/test/window.rs 🔗

@@ -251,7 +251,12 @@ impl PlatformWindow for TestWindow {
 
     fn on_appearance_changed(&self, _callback: Box<dyn FnMut()>) {}
 
-    fn draw(&self, _scene: &crate::Scene) {}
+    fn draw(
+        &self,
+        _scene: &crate::Scene,
+        _on_complete: Option<futures::channel::oneshot::Sender<()>>,
+    ) {
+    }
 
     fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> {
         self.0.lock().sprite_atlas.clone()
@@ -277,6 +282,10 @@ impl PlatformWindow for TestWindow {
     fn gpu_specs(&self) -> Option<GPUSpecs> {
         None
     }
+
+    fn fps(&self) -> Option<f32> {
+        None
+    }
 }
 
 pub(crate) struct TestAtlasState {

crates/gpui/src/platform/windows/window.rs 🔗

@@ -660,8 +660,8 @@ impl PlatformWindow for WindowsWindow {
         self.0.state.borrow_mut().callbacks.appearance_changed = Some(callback);
     }
 
-    fn draw(&self, scene: &Scene) {
-        self.0.state.borrow_mut().renderer.draw(scene)
+    fn draw(&self, scene: &Scene, on_complete: Option<oneshot::Sender<()>>) {
+        self.0.state.borrow_mut().renderer.draw(scene, on_complete)
     }
 
     fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
@@ -675,6 +675,10 @@ impl PlatformWindow for WindowsWindow {
     fn gpu_specs(&self) -> Option<GPUSpecs> {
         Some(self.0.state.borrow().renderer.gpu_specs())
     }
+
+    fn fps(&self) -> Option<f32> {
+        None
+    }
 }
 
 #[implement(IDropTarget)]

crates/gpui/src/window.rs 🔗

@@ -11,9 +11,9 @@ use crate::{
     PromptLevel, Quad, Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams,
     Replay, ResizeEdge, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style,
     SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement,
-    TransformationMatrix, Underline, UnderlineStyle, View, VisualContext, WeakView,
-    WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations,
-    WindowOptions, WindowParams, WindowTextSystem, SUBPIXEL_VARIANTS,
+    TimeToFirstWindowDraw, TransformationMatrix, Underline, UnderlineStyle, View, VisualContext,
+    WeakView, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls,
+    WindowDecorations, WindowOptions, WindowParams, WindowTextSystem, SUBPIXEL_VARIANTS,
 };
 use anyhow::{anyhow, Context as _, Result};
 use collections::{FxHashMap, FxHashSet};
@@ -544,6 +544,8 @@ pub struct Window {
     hovered: Rc<Cell<bool>>,
     pub(crate) dirty: Rc<Cell<bool>>,
     pub(crate) needs_present: Rc<Cell<bool>>,
+    /// We assign this to be notified when the platform graphics backend fires the next completion callback for drawing the window.
+    present_completed: RefCell<Option<oneshot::Sender<()>>>,
     pub(crate) last_input_timestamp: Rc<Cell<Instant>>,
     pub(crate) refreshing: bool,
     pub(crate) draw_phase: DrawPhase,
@@ -820,6 +822,7 @@ impl Window {
             hovered,
             dirty,
             needs_present,
+            present_completed: RefCell::default(),
             last_input_timestamp,
             refreshing: false,
             draw_phase: DrawPhase::None,
@@ -1489,13 +1492,29 @@ impl<'a> WindowContext<'a> {
         self.window.refreshing = false;
         self.window.draw_phase = DrawPhase::None;
         self.window.needs_present.set(true);
+
+        if let Some(TimeToFirstWindowDraw::Pending(start)) = self.app.time_to_first_window_draw {
+            let (tx, rx) = oneshot::channel();
+            *self.window.present_completed.borrow_mut() = Some(tx);
+            self.spawn(|mut cx| async move {
+                rx.await.ok();
+                cx.update(|cx| {
+                    let duration = start.elapsed();
+                    cx.time_to_first_window_draw = Some(TimeToFirstWindowDraw::Done(duration));
+                    log::info!("time to first window draw: {:?}", duration);
+                    cx.push_effect(Effect::Refresh);
+                })
+            })
+            .detach();
+        }
     }
 
     #[profiling::function]
     fn present(&self) {
+        let on_complete = self.window.present_completed.take();
         self.window
             .platform_window
-            .draw(&self.window.rendered_frame.scene);
+            .draw(&self.window.rendered_frame.scene, on_complete);
         self.window.needs_present.set(false);
         profiling::finish_frame!();
     }
@@ -3718,6 +3737,12 @@ impl<'a> WindowContext<'a> {
     pub fn gpu_specs(&self) -> Option<GPUSpecs> {
         self.window.platform_window.gpu_specs()
     }
+
+    /// Get the current FPS (frames per second) of the window.
+    /// This is only supported on macOS currently.
+    pub fn fps(&self) -> Option<f32> {
+        self.window.platform_window.fps()
+    }
 }
 
 #[cfg(target_os = "windows")]

crates/performance/Cargo.toml 🔗

@@ -0,0 +1,36 @@
+[package]
+name = "performance"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/performance.rs"
+doctest = false
+
+[features]
+test-support = [
+    "collections/test-support",
+    "gpui/test-support",
+    "workspace/test-support",
+]
+
+[dependencies]
+anyhow.workspace = true
+gpui.workspace = true
+log.workspace = true
+schemars.workspace = true
+serde.workspace = true
+settings.workspace = true
+workspace.workspace = true
+
+[dev-dependencies]
+collections = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+settings = { workspace = true, features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }

crates/performance/src/performance.rs 🔗

@@ -0,0 +1,189 @@
+use std::time::Instant;
+
+use anyhow::Result;
+use gpui::{
+    div, AppContext, InteractiveElement as _, Render, StatefulInteractiveElement as _,
+    Subscription, ViewContext, VisualContext,
+};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::{Settings, SettingsSources, SettingsStore};
+use workspace::{
+    ui::{Label, LabelCommon, LabelSize, Tooltip},
+    ItemHandle, StatusItemView, Workspace,
+};
+
+const SHOW_STARTUP_TIME_DURATION: std::time::Duration = std::time::Duration::from_secs(5);
+
+pub fn init(cx: &mut AppContext) {
+    PerformanceSettings::register(cx);
+
+    let mut enabled = PerformanceSettings::get_global(cx)
+        .show_in_status_bar
+        .unwrap_or(false);
+    let start_time = Instant::now();
+    let mut _observe_workspaces = toggle_status_bar_items(enabled, start_time, cx);
+
+    cx.observe_global::<SettingsStore>(move |cx| {
+        let new_value = PerformanceSettings::get_global(cx)
+            .show_in_status_bar
+            .unwrap_or(false);
+        if new_value != enabled {
+            enabled = new_value;
+            _observe_workspaces = toggle_status_bar_items(enabled, start_time, cx);
+        }
+    })
+    .detach();
+}
+
+fn toggle_status_bar_items(
+    enabled: bool,
+    start_time: Instant,
+    cx: &mut AppContext,
+) -> Option<Subscription> {
+    for window in cx.windows() {
+        if let Some(workspace) = window.downcast::<Workspace>() {
+            workspace
+                .update(cx, |workspace, cx| {
+                    toggle_status_bar_item(workspace, enabled, start_time, cx);
+                })
+                .ok();
+        }
+    }
+
+    if enabled {
+        log::info!("performance metrics display enabled");
+        Some(cx.observe_new_views::<Workspace>(move |workspace, cx| {
+            toggle_status_bar_item(workspace, true, start_time, cx);
+        }))
+    } else {
+        log::info!("performance metrics display disabled");
+        None
+    }
+}
+
+struct PerformanceStatusBarItem {
+    display_mode: DisplayMode,
+}
+
+#[derive(Copy, Clone, Debug)]
+enum DisplayMode {
+    StartupTime,
+    Fps,
+}
+
+impl PerformanceStatusBarItem {
+    fn new(start_time: Instant, cx: &mut ViewContext<Self>) -> Self {
+        let now = Instant::now();
+        let display_mode = if now < start_time + SHOW_STARTUP_TIME_DURATION {
+            DisplayMode::StartupTime
+        } else {
+            DisplayMode::Fps
+        };
+
+        let this = Self { display_mode };
+
+        if let DisplayMode::StartupTime = display_mode {
+            cx.spawn(|this, mut cx| async move {
+                let now = Instant::now();
+                let remaining_duration =
+                    (start_time + SHOW_STARTUP_TIME_DURATION).saturating_duration_since(now);
+                cx.background_executor().timer(remaining_duration).await;
+                this.update(&mut cx, |this, cx| {
+                    this.display_mode = DisplayMode::Fps;
+                    cx.notify();
+                })
+                .ok();
+            })
+            .detach();
+        }
+
+        this
+    }
+}
+
+impl Render for PerformanceStatusBarItem {
+    fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
+        let text = match self.display_mode {
+            DisplayMode::StartupTime => cx
+                .time_to_first_window_draw()
+                .map_or("Pending".to_string(), |duration| {
+                    format!("{}ms", duration.as_millis())
+                }),
+            DisplayMode::Fps => cx.fps().map_or("".to_string(), |fps| {
+                format!("{:3} FPS", fps.round() as u32)
+            }),
+        };
+
+        use gpui::ParentElement;
+        let display_mode = self.display_mode;
+        div()
+            .id("performance status")
+            .child(Label::new(text).size(LabelSize::Small))
+            .tooltip(move |cx| match display_mode {
+                DisplayMode::StartupTime => Tooltip::text("Time to first window draw", cx),
+                DisplayMode::Fps => cx
+                    .new_view(|cx| {
+                        let tooltip = Tooltip::new("Current FPS");
+                        if let Some(time_to_first) = cx.time_to_first_window_draw() {
+                            tooltip.meta(format!(
+                                "Time to first window draw: {}ms",
+                                time_to_first.as_millis()
+                            ))
+                        } else {
+                            tooltip
+                        }
+                    })
+                    .into(),
+            })
+    }
+}
+
+impl StatusItemView for PerformanceStatusBarItem {
+    fn set_active_pane_item(
+        &mut self,
+        _active_pane_item: Option<&dyn ItemHandle>,
+        _cx: &mut gpui::ViewContext<Self>,
+    ) {
+        // This is not currently used.
+    }
+}
+
+fn toggle_status_bar_item(
+    workspace: &mut Workspace,
+    enabled: bool,
+    start_time: Instant,
+    cx: &mut ViewContext<Workspace>,
+) {
+    if enabled {
+        workspace.status_bar().update(cx, |bar, cx| {
+            bar.add_right_item(
+                cx.new_view(|cx| PerformanceStatusBarItem::new(start_time, cx)),
+                cx,
+            )
+        });
+    } else {
+        workspace.status_bar().update(cx, |bar, cx| {
+            bar.remove_items_of_type::<PerformanceStatusBarItem>(cx);
+        });
+    }
+}
+
+/// Configuration of the display of performance details.
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct PerformanceSettings {
+    /// Display the time to first window draw and frame rate in the status bar.
+    ///
+    /// Default: false
+    pub show_in_status_bar: Option<bool>,
+}
+
+impl Settings for PerformanceSettings {
+    const KEY: Option<&'static str> = Some("performance");
+
+    type FileContent = Self;
+
+    fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
+        sources.json_merge()
+    }
+}

crates/workspace/src/status_bar.rs 🔗

@@ -153,6 +153,17 @@ impl StatusBar {
         cx.notify();
     }
 
+    pub fn remove_items_of_type<T>(&mut self, cx: &mut ViewContext<Self>)
+    where
+        T: 'static + StatusItemView,
+    {
+        self.left_items
+            .retain(|item| item.item_type() != TypeId::of::<T>());
+        self.right_items
+            .retain(|item| item.item_type() != TypeId::of::<T>());
+        cx.notify();
+    }
+
     pub fn add_right_item<T>(&mut self, item: View<T>, cx: &mut ViewContext<Self>)
     where
         T: 'static + StatusItemView,

crates/workspace/src/workspace.rs 🔗

@@ -38,7 +38,7 @@ use gpui::{
     ResizeEdge, Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds,
     WindowHandle, WindowId, WindowOptions,
 };
-use item::{
+pub use item::{
     FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
     ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
 };

crates/zed/Cargo.toml 🔗

@@ -72,6 +72,7 @@ outline.workspace = true
 outline_panel.workspace = true
 parking_lot.workspace = true
 paths.workspace = true
+performance.workspace = true
 profiling.workspace = true
 project.workspace = true
 project_panel.workspace = true

crates/zed/src/main.rs 🔗

@@ -266,6 +266,7 @@ fn init_ui(
     welcome::init(cx);
     settings_ui::init(cx);
     extensions_ui::init(cx);
+    performance::init(cx);
 
     cx.observe_global::<SettingsStore>({
         let languages = app_state.languages.clone();
@@ -315,6 +316,7 @@ fn init_ui(
 }
 
 fn main() {
+    let start_time = std::time::Instant::now();
     menu::init();
     zed_actions::init();
 
@@ -326,7 +328,9 @@ fn main() {
     init_logger();
 
     log::info!("========== starting zed ==========");
-    let app = App::new().with_assets(Assets);
+    let app = App::new()
+        .with_assets(Assets)
+        .measure_time_to_first_window_draw(start_time);
 
     let (installation_id, existing_installation_id_found) = app
         .background_executor()