Detailed changes
@@ -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",
@@ -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" }
@@ -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 {
@@ -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> {
@@ -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
+ }
}
@@ -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
+ }
+}
@@ -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>) {
@@ -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
+ }
}
@@ -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(
@@ -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 {
@@ -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 {
@@ -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)]
@@ -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")]
@@ -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"] }
@@ -0,0 +1 @@
+../../LICENSE-GPL
@@ -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()
+ }
+}
@@ -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,
@@ -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,
};
@@ -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
@@ -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()