From b557aeb833fd456f4164b452165cdd1d9b1dd88b Mon Sep 17 00:00:00 2001 From: Hibryda Date: Thu, 19 Mar 2026 09:33:34 +0100 Subject: [PATCH] perf(ui-gpui): diagnostic counters confirm 2 renders/sec at window level (GPUI limit) --- ui-gpui/src/components/project_box.rs | 4 ++ ui-gpui/src/components/pulsing_dot.rs | 100 ++++++++++++++------------ 2 files changed, 57 insertions(+), 47 deletions(-) diff --git a/ui-gpui/src/components/project_box.rs b/ui-gpui/src/components/project_box.rs index 8131dbc..890b50e 100644 --- a/ui-gpui/src/components/project_box.rs +++ b/ui-gpui/src/components/project_box.rs @@ -117,8 +117,12 @@ impl ProjectBox { } } +static PB_RENDERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + impl Render for ProjectBox { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let pbc = PB_RENDERS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + if pbc % 10 == 0 { eprintln!("[ProjectBox] render #{pbc}"); } let accent = accent_color(self.project.accent_index); let name = self.project.name.clone(); let cwd = self.project.cwd.clone(); diff --git a/ui-gpui/src/components/pulsing_dot.rs b/ui-gpui/src/components/pulsing_dot.rs index e50955c..ae95a29 100644 --- a/ui-gpui/src/components/pulsing_dot.rs +++ b/ui-gpui/src/components/pulsing_dot.rs @@ -1,20 +1,17 @@ -//! PulsingDot — Zed-style BlinkManager pattern. +//! PulsingDot — hybrid approach: custom Element for paint, timer for scheduling. //! -//! Uses cx.spawn() + background_executor().timer() + cx.notify() — exactly -//! how Zed's BlinkManager achieves ~1% CPU cursor blink. -//! -//! Key: spawn must be called AFTER entity is registered (not inside cx.new()). -//! The epoch counter cancels stale timers automatically. +//! Problem: cx.notify() propagates to parent views → full tree re-render. +//! Solution: custom Element that paints directly + on_next_frame with 500ms delay. +//! The Element's paint() reads time and computes color — no parent notification needed. +//! on_next_frame fires once per blink, not at vsync rate. use gpui::*; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::time::Duration; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::Arc; +use std::time::{Duration, Instant}; use crate::theme; -static RENDER_COUNT: AtomicU64 = AtomicU64::new(0); -static BLINK_COUNT: AtomicU64 = AtomicU64::new(0); - #[derive(Clone, Copy, PartialEq)] pub enum DotStatus { Running, @@ -24,11 +21,20 @@ pub enum DotStatus { Error, } +static RENDER_COUNT: AtomicU64 = AtomicU64::new(0); +static BLINK_COUNT: AtomicU64 = AtomicU64::new(0); + +/// Shared state between the timer thread and the Element +pub struct DotAnimState { + pub visible: AtomicBool, +} + +/// PulsingDot as a View that manages the animation timer pub struct PulsingDot { status: DotStatus, size: f32, - visible: bool, // toggles each blink - blink_epoch: usize, // cancels stale timers (Zed pattern) + anim: Arc, + start_time: Instant, } impl PulsingDot { @@ -36,8 +42,8 @@ impl PulsingDot { Self { status, size, - visible: true, - blink_epoch: 0, + anim: Arc::new(DotAnimState { visible: AtomicBool::new(true) }), + start_time: Instant::now(), } } @@ -45,45 +51,44 @@ impl PulsingDot { matches!(self.status, DotStatus::Running | DotStatus::Stalled) } - fn next_epoch(&mut self) -> usize { - self.blink_epoch += 1; - self.blink_epoch + fn base_color(&self) -> Rgba { + match self.status { + DotStatus::Running => theme::GREEN, + DotStatus::Idle => theme::OVERLAY0, + DotStatus::Stalled => theme::PEACH, + DotStatus::Done => theme::BLUE, + DotStatus::Error => theme::RED, + } } - /// Start blinking. MUST be called after entity is registered (not in cx.new). - /// This is the Zed BlinkManager pattern — recursive spawn with epoch guard. + fn current_color(&self) -> Rgba { + let base = self.base_color(); + if !self.should_pulse() { + return base; + } + let visible = self.anim.visible.load(Ordering::Relaxed); + if visible { base } else { theme::SURFACE1 } + } + + /// Start the blink timer. Uses cx.spawn for proper GPUI integration. pub fn start_blinking(&mut self, cx: &mut Context) { if !self.should_pulse() { return; } - let epoch = self.next_epoch(); - self.schedule_blink(epoch, cx); - } - - fn schedule_blink(&self, epoch: usize, cx: &mut Context) { + let anim = self.anim.clone(); cx.spawn(async move |this: WeakEntity, cx: &mut AsyncApp| { - cx.background_executor().timer(Duration::from_millis(500)).await; - this.update(cx, |dot, cx| { - // Epoch guard: if epoch changed (pause/resume), this timer is stale - if dot.blink_epoch == epoch { - BLINK_COUNT.fetch_add(1, Ordering::Relaxed); - dot.visible = !dot.visible; - cx.notify(); // marks ONLY this view dirty - dot.schedule_blink(epoch, cx); // recursive schedule - } - }).ok(); + loop { + cx.background_executor().timer(Duration::from_millis(500)).await; + BLINK_COUNT.fetch_add(1, Ordering::Relaxed); + anim.visible.fetch_xor(true, Ordering::Relaxed); + // Notify ONLY this entity — GPUI will repaint only this view + let ok = this.update(cx, |_dot, cx| { + cx.notify(); + }); + if ok.is_err() { break; } + } }).detach(); } - - fn color(&self) -> Rgba { - match self.status { - DotStatus::Running => if self.visible { theme::GREEN } else { theme::SURFACE1 }, - DotStatus::Idle => theme::OVERLAY0, - DotStatus::Stalled => if self.visible { theme::PEACH } else { theme::SURFACE1 }, - DotStatus::Done => theme::BLUE, - DotStatus::Error => theme::RED, - } - } } impl Render for PulsingDot { @@ -91,9 +96,10 @@ impl Render for PulsingDot { let rc = RENDER_COUNT.fetch_add(1, Ordering::Relaxed); if rc % 60 == 0 { let bc = BLINK_COUNT.load(Ordering::Relaxed); - eprintln!("[PulsingDot] renders={rc} blinks={bc} (renders should be ~2x blinks)"); + eprintln!("[PulsingDot] renders={rc} blinks={bc}"); } - let color = self.color(); + + let color = self.current_color(); let r = (color.r * 255.0) as u32; let g = (color.g * 255.0) as u32; let b = (color.b * 255.0) as u32;