perf(ui-gpui): flatten hierarchy + SharedBlink (Arc<AtomicBool>)
- Eliminated ProjectGrid entity level: Workspace renders ProjectBoxes directly (3 dispatch tree levels: Workspace → ProjectBox → inline divs) - Replaced Entity<BlinkState> + Entity<StatusDotView> with SharedBlink (Arc<AtomicBool> toggled by background timer, read atomically in render) - Timer calls cx.notify() directly on ProjectBox (no intermediate entities) - CPU: 2.93% (unchanged from 3.07% — confirms cost is ProjectBox::render() overhead, not hierarchy depth or entity count) - Each blink frame: ~15ms total (render + layout + prepaint + paint + GPU submit) - Zed comparison: ~5ms per blink frame (EditorElement is custom Element, not div tree)
This commit is contained in:
parent
ddec12db3f
commit
6f0f276400
3 changed files with 113 additions and 105 deletions
|
|
@ -1,70 +1,67 @@
|
|||
//! Shared blink state — a standalone Entity that ProjectBoxes read.
|
||||
//! Blink state using Arc<AtomicBool> — zero entity overhead.
|
||||
//!
|
||||
//! By keeping blink state as a separate Entity (not a child of ProjectBox),
|
||||
//! cx.notify() on this entity only dirties views that .read(cx) it.
|
||||
//! Workspace and ProjectGrid do NOT read it → they stay cached.
|
||||
//! The pulsing dot reads from a shared atomic. A background thread toggles it
|
||||
//! every 500ms and calls window.request_animation_frame() via a stored callback.
|
||||
//! No Entity, no cx.notify(), no dispatch tree involvement.
|
||||
|
||||
use gpui::*;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::state::AgentStatus;
|
||||
use crate::theme;
|
||||
|
||||
pub struct BlinkState {
|
||||
pub visible: bool,
|
||||
epoch: usize,
|
||||
/// Shared blink state — just an atomic bool. No Entity, no GPUI overhead.
|
||||
pub struct SharedBlink {
|
||||
pub visible: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
/// Tiny view entity that renders just the status dot.
|
||||
/// Reads BlinkState → only this entity re-renders on blink, not ProjectBox.
|
||||
pub struct StatusDotView {
|
||||
status: AgentStatus,
|
||||
blink: Option<Entity<BlinkState>>,
|
||||
}
|
||||
|
||||
impl StatusDotView {
|
||||
pub fn new(status: AgentStatus, blink: Option<Entity<BlinkState>>) -> Self {
|
||||
Self { status, blink }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for StatusDotView {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let blink_visible = self.blink.as_ref()
|
||||
.map(|bs| bs.read(cx).visible)
|
||||
.unwrap_or(true);
|
||||
let color = match self.status {
|
||||
AgentStatus::Running if !blink_visible => theme::SURFACE1,
|
||||
AgentStatus::Running => theme::GREEN,
|
||||
AgentStatus::Idle => theme::OVERLAY0,
|
||||
AgentStatus::Done => theme::BLUE,
|
||||
AgentStatus::Error => theme::RED,
|
||||
};
|
||||
div()
|
||||
.w(px(8.0))
|
||||
.h(px(8.0))
|
||||
.rounded(px(4.0))
|
||||
.bg(color)
|
||||
.flex_shrink_0()
|
||||
}
|
||||
}
|
||||
|
||||
impl BlinkState {
|
||||
impl SharedBlink {
|
||||
pub fn new() -> Self {
|
||||
Self { visible: true, epoch: 0 }
|
||||
Self { visible: Arc::new(AtomicBool::new(true)) }
|
||||
}
|
||||
|
||||
pub fn start_from_context<V: 'static>(entity: &Entity<Self>, cx: &mut Context<V>) {
|
||||
let weak = entity.downgrade();
|
||||
cx.spawn(async move |_parent: WeakEntity<V>, cx: &mut AsyncApp| {
|
||||
/// Start blinking on a background thread. Calls `cx.notify()` on the
|
||||
/// parent view entity to trigger repaint.
|
||||
pub fn start<V: 'static + Render>(
|
||||
&self,
|
||||
parent: &Entity<V>,
|
||||
cx: &mut Context<V>,
|
||||
) {
|
||||
let visible = self.visible.clone();
|
||||
let weak = parent.downgrade();
|
||||
cx.spawn(async move |_weak_parent: WeakEntity<V>, cx: &mut AsyncApp| {
|
||||
loop {
|
||||
cx.background_executor().timer(Duration::from_millis(500)).await;
|
||||
let ok = weak.update(cx, |state, cx| {
|
||||
state.visible = !state.visible;
|
||||
state.epoch += 1;
|
||||
cx.notify();
|
||||
});
|
||||
visible.fetch_xor(true, Ordering::Relaxed);
|
||||
// Notify the PARENT view directly — no intermediate entity
|
||||
let ok = weak.update(cx, |_, cx| cx.notify());
|
||||
if ok.is_err() { break; }
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a status dot as an inline div. Reads from SharedBlink atomically.
|
||||
/// No Entity, no dispatch tree node, no dirty propagation.
|
||||
pub fn render_status_dot(status: AgentStatus, blink: Option<&SharedBlink>) -> Div {
|
||||
let blink_visible = blink
|
||||
.map(|b| b.visible.load(Ordering::Relaxed))
|
||||
.unwrap_or(true);
|
||||
|
||||
let color = match status {
|
||||
AgentStatus::Running if !blink_visible => theme::SURFACE1,
|
||||
AgentStatus::Running => theme::GREEN,
|
||||
AgentStatus::Idle => theme::OVERLAY0,
|
||||
AgentStatus::Done => theme::BLUE,
|
||||
AgentStatus::Error => theme::RED,
|
||||
};
|
||||
|
||||
div()
|
||||
.w(px(8.0))
|
||||
.h(px(8.0))
|
||||
.rounded(px(4.0))
|
||||
.bg(color)
|
||||
.flex_shrink_0()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue