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 {
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 name = self.project.name.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
//! 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<DotAnimState>,
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<Self>) {
if !self.should_pulse() {
return;
}
let epoch = self.next_epoch();
self.schedule_blink(epoch, cx);
}
fn schedule_blink(&self, epoch: usize, cx: &mut Context<Self>) {
let anim = self.anim.clone();
cx.spawn(async move |this: WeakEntity<Self>, cx: &mut AsyncApp| {
loop {
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
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; }
}
}).ok();
}).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;