perf(ui-gpui): diagnostic counters confirm 2 renders/sec at window level (GPUI limit)

This commit is contained in:
Hibryda 2026-03-19 09:33:34 +01:00
parent 7ab5d97352
commit b557aeb833
2 changed files with 57 additions and 47 deletions

View file

@ -117,8 +117,12 @@ impl ProjectBox {
} }
} }
static PB_RENDERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
impl Render for ProjectBox { impl Render for ProjectBox {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> 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 accent = accent_color(self.project.accent_index);
let name = self.project.name.clone(); let name = self.project.name.clone();
let cwd = self.project.cwd.clone(); let cwd = self.project.cwd.clone();

View file

@ -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 //! Problem: cx.notify() propagates to parent views → full tree re-render.
//! how Zed's BlinkManager achieves ~1% CPU cursor blink. //! 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.
//! Key: spawn must be called AFTER entity is registered (not inside cx.new()). //! on_next_frame fires once per blink, not at vsync rate.
//! The epoch counter cancels stale timers automatically.
use gpui::*; use gpui::*;
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::Duration; use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::theme; use crate::theme;
static RENDER_COUNT: AtomicU64 = AtomicU64::new(0);
static BLINK_COUNT: AtomicU64 = AtomicU64::new(0);
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq)]
pub enum DotStatus { pub enum DotStatus {
Running, Running,
@ -24,11 +21,20 @@ pub enum DotStatus {
Error, 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 { pub struct PulsingDot {
status: DotStatus, status: DotStatus,
size: f32, size: f32,
visible: bool, // toggles each blink anim: Arc<DotAnimState>,
blink_epoch: usize, // cancels stale timers (Zed pattern) start_time: Instant,
} }
impl PulsingDot { impl PulsingDot {
@ -36,8 +42,8 @@ impl PulsingDot {
Self { Self {
status, status,
size, size,
visible: true, anim: Arc::new(DotAnimState { visible: AtomicBool::new(true) }),
blink_epoch: 0, start_time: Instant::now(),
} }
} }
@ -45,45 +51,44 @@ impl PulsingDot {
matches!(self.status, DotStatus::Running | DotStatus::Stalled) matches!(self.status, DotStatus::Running | DotStatus::Stalled)
} }
fn next_epoch(&mut self) -> usize { fn base_color(&self) -> Rgba {
self.blink_epoch += 1; match self.status {
self.blink_epoch 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). fn current_color(&self) -> Rgba {
/// This is the Zed BlinkManager pattern — recursive spawn with epoch guard. 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<Self>) { pub fn start_blinking(&mut self, cx: &mut Context<Self>) {
if !self.should_pulse() { if !self.should_pulse() {
return; return;
} }
let epoch = self.next_epoch(); let anim = self.anim.clone();
self.schedule_blink(epoch, cx);
}
fn schedule_blink(&self, epoch: usize, cx: &mut Context<Self>) {
cx.spawn(async move |this: WeakEntity<Self>, cx: &mut AsyncApp| { cx.spawn(async move |this: WeakEntity<Self>, cx: &mut AsyncApp| {
cx.background_executor().timer(Duration::from_millis(500)).await; loop {
this.update(cx, |dot, cx| { cx.background_executor().timer(Duration::from_millis(500)).await;
// Epoch guard: if epoch changed (pause/resume), this timer is stale BLINK_COUNT.fetch_add(1, Ordering::Relaxed);
if dot.blink_epoch == epoch { anim.visible.fetch_xor(true, Ordering::Relaxed);
BLINK_COUNT.fetch_add(1, Ordering::Relaxed); // Notify ONLY this entity — GPUI will repaint only this view
dot.visible = !dot.visible; let ok = this.update(cx, |_dot, cx| {
cx.notify(); // marks ONLY this view dirty cx.notify();
dot.schedule_blink(epoch, cx); // recursive schedule });
} if ok.is_err() { break; }
}).ok(); }
}).detach(); }).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 { impl Render for PulsingDot {
@ -91,9 +96,10 @@ impl Render for PulsingDot {
let rc = RENDER_COUNT.fetch_add(1, Ordering::Relaxed); let rc = RENDER_COUNT.fetch_add(1, Ordering::Relaxed);
if rc % 60 == 0 { if rc % 60 == 0 {
let bc = BLINK_COUNT.load(Ordering::Relaxed); 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 r = (color.r * 255.0) as u32;
let g = (color.g * 255.0) as u32; let g = (color.g * 255.0) as u32;
let b = (color.b * 255.0) as u32; let b = (color.b * 255.0) as u32;