diff --git a/Cargo.lock b/Cargo.lock index 52bf0e4059..a99e38a30b 100644 --- a/Cargo.lock +++ b/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", diff --git a/Cargo.toml b/Cargo.toml index 329688b34b..e6aa17c503 100644 --- a/Cargo.toml +++ b/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" } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 5e3922061d..6d011a7675 100644 --- a/crates/gpui/src/app.rs +++ b/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(self, on_finish_launching: F) @@ -247,6 +253,7 @@ pub struct AppContext { pub(crate) layout_id_buffer: Vec, // We recycle this memory across layout requests. pub(crate) propagate_event: bool, pub(crate) prompt_builder: Option, + pub(crate) time_to_first_window_draw: Option, } 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 { + match self.time_to_first_window_draw { + Some(TimeToFirstWindowDraw::Done(duration)) => Some(duration), + _ => None, + } + } } impl Context for AppContext { @@ -1465,6 +1481,15 @@ impl DerefMut for GlobalLease { } } +/// 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 { diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index d19a6b745a..d10f386419 100644 --- a/crates/gpui/src/platform.rs +++ b/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 bool>); fn on_close(&self, callback: Box); fn on_appearance_changed(&self, callback: Box); - fn draw(&self, scene: &Scene); + fn draw(&self, scene: &Scene, on_complete: Option>); fn completed_frame(&self) {} fn sprite_atlas(&self) -> Arc; @@ -379,6 +381,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { } fn set_client_inset(&self, _inset: Pixels) {} fn gpu_specs(&self) -> Option; + fn fps(&self) -> Option; #[cfg(any(test, feature = "test-support"))] fn as_test(&mut self) -> Option<&mut TestWindow> { diff --git a/crates/gpui/src/platform/blade/blade_renderer.rs b/crates/gpui/src/platform/blade/blade_renderer.rs index 27e76b9778..afb065895d 100644 --- a/crates/gpui/src/platform/blade/blade_renderer.rs +++ b/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>, + ) { 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 + } } diff --git a/crates/gpui/src/platform/fps.rs b/crates/gpui/src/platform/fps.rs new file mode 100644 index 0000000000..9776e0d454 --- /dev/null +++ b/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` for safe sharing across threads. + pub fn new() -> Arc { + 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 + } +} diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 2bd57adafc..30e5d8a172 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/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> { + ) -> Option> { 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>) { 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 { self.borrow().renderer.gpu_specs().into() } + + fn fps(&self) -> Option { + None + } } fn update_window(mut state: RefMut) { diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index b3c8ea7cc7..eb0d784ca3 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/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>) { let mut inner = self.0.state.borrow_mut(); - inner.renderer.draw(scene); + inner.renderer.draw(scene, on_complete); } fn sprite_atlas(&self) -> Arc { @@ -1398,4 +1399,8 @@ impl PlatformWindow for X11Window { fn gpu_specs(&self) -> Option { self.0.state.borrow().renderer.gpu_specs().into() } + + fn fps(&self) -> Option { + None + } } diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index 401734e253..e8d92057af 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/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>, sprite_atlas: Arc, core_video_texture_cache: CVMetalTextureCache, + fps_counter: Arc, } 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>) { + let on_complete = Arc::new(Mutex::new(on_complete)); let layer = self.layer.clone(); let viewport_size = layer.drawable_size(); let viewport_size: Size = 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( diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 0df9f3936e..bc9ace81d3 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/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 { 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>) { let mut this = self.0.lock(); - this.renderer.draw(scene); + this.renderer.draw(scene, on_complete); } fn sprite_atlas(&self) -> Arc { 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 { None } + + fn fps(&self) -> Option { + Some(self.0.lock().renderer.fps()) + } } impl rwh::HasWindowHandle for MacWindow { diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index f79421b12a..2680d1ce23 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -251,7 +251,12 @@ impl PlatformWindow for TestWindow { fn on_appearance_changed(&self, _callback: Box) {} - fn draw(&self, _scene: &crate::Scene) {} + fn draw( + &self, + _scene: &crate::Scene, + _on_complete: Option>, + ) { + } fn sprite_atlas(&self) -> sync::Arc { self.0.lock().sprite_atlas.clone() @@ -277,6 +282,10 @@ impl PlatformWindow for TestWindow { fn gpu_specs(&self) -> Option { None } + + fn fps(&self) -> Option { + None + } } pub(crate) struct TestAtlasState { diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index d2db91b5cb..5d1ba28db9 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/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>) { + self.0.state.borrow_mut().renderer.draw(scene, on_complete) } fn sprite_atlas(&self) -> Arc { @@ -675,6 +675,10 @@ impl PlatformWindow for WindowsWindow { fn gpu_specs(&self) -> Option { Some(self.0.state.borrow().renderer.gpu_specs()) } + + fn fps(&self) -> Option { + None + } } #[implement(IDropTarget)] diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index ae454ae022..7aae3708d1 100644 --- a/crates/gpui/src/window.rs +++ b/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>, pub(crate) dirty: Rc>, pub(crate) needs_present: Rc>, + /// We assign this to be notified when the platform graphics backend fires the next completion callback for drawing the window. + present_completed: RefCell>>, pub(crate) last_input_timestamp: Rc>, 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 { 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 { + self.window.platform_window.fps() + } } #[cfg(target_os = "windows")] diff --git a/crates/performance/Cargo.toml b/crates/performance/Cargo.toml new file mode 100644 index 0000000000..33f4bdc565 --- /dev/null +++ b/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"] } diff --git a/crates/performance/LICENSE-GPL b/crates/performance/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/performance/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/performance/src/performance.rs b/crates/performance/src/performance.rs new file mode 100644 index 0000000000..43e97a0dd3 --- /dev/null +++ b/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::(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 { + for window in cx.windows() { + if let Some(workspace) = window.downcast::() { + 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::(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 { + 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) -> 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, + ) { + // This is not currently used. + } +} + +fn toggle_status_bar_item( + workspace: &mut Workspace, + enabled: bool, + start_time: Instant, + cx: &mut ViewContext, +) { + 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::(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, +} + +impl Settings for PerformanceSettings { + const KEY: Option<&'static str> = Some("performance"); + + type FileContent = Self; + + fn load(sources: SettingsSources, _: &mut AppContext) -> Result { + sources.json_merge() + } +} diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index ea92451bf0..29ba4352ed 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -153,6 +153,17 @@ impl StatusBar { cx.notify(); } + pub fn remove_items_of_type(&mut self, cx: &mut ViewContext) + where + T: 'static + StatusItemView, + { + self.left_items + .retain(|item| item.item_type() != TypeId::of::()); + self.right_items + .retain(|item| item.item_type() != TypeId::of::()); + cx.notify(); + } + pub fn add_right_item(&mut self, item: View, cx: &mut ViewContext) where T: 'static + StatusItemView, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8da3ae929d..07e60754a8 100644 --- a/crates/workspace/src/workspace.rs +++ b/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, }; diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index f04ab62a3f..bc32230138 100644 --- a/crates/zed/Cargo.toml +++ b/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 diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 173387b840..727b11dbc4 100644 --- a/crates/zed/src/main.rs +++ b/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::({ 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()