perf(ui-gpui): isolate StatusDotView entity, document 3% CPU floor
Root cause analysis: - 0% baseline: calloop 60Hz polling has zero measurable CPU cost - 3% is entirely from ProjectBox::render() rebuilding ~30 header divs × 2 boxes × 2/sec = 120 div constructions/sec - AgentPane + TerminalView cached (into_cached_flex) = 0% contribution - mark_view_dirty() walks ancestors unconditionally in GPUI 0.2.2 → StatusDotView isolation doesn't prevent parent re-render - Fragment shaders not exposed (paint_quad accepts static color only) - GPUI AnimationElement uses request_animation_frame = 79% CPU (vsync) StatusDotView pattern is architecturally correct for future GPUI versions that may implement per-view dirty isolation.
This commit is contained in:
parent
727c7d2e06
commit
f797a676f4
2 changed files with 57 additions and 33 deletions
|
|
@ -6,12 +6,48 @@
|
|||
|
||||
use gpui::*;
|
||||
use std::time::Duration;
|
||||
use crate::state::AgentStatus;
|
||||
use crate::theme;
|
||||
|
||||
pub struct BlinkState {
|
||||
pub visible: bool,
|
||||
epoch: usize,
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
pub fn new() -> Self {
|
||||
Self { visible: true, epoch: 0 }
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ pub struct ProjectBox {
|
|||
pub project: Project,
|
||||
pub agent_pane: Option<Entity<crate::components::agent_pane::AgentPane>>,
|
||||
pub terminal_view: Option<Entity<crate::terminal::renderer::TerminalView>>,
|
||||
pub blink_state: Option<Entity<crate::components::blink_state::BlinkState>>,
|
||||
pub status_dot_view: Option<Entity<crate::components::blink_state::StatusDotView>>,
|
||||
// Cached strings to avoid allocation on every render
|
||||
id_project: SharedString,
|
||||
id_model: SharedString,
|
||||
|
|
@ -96,7 +96,7 @@ impl ProjectBox {
|
|||
project,
|
||||
agent_pane: None,
|
||||
terminal_view: None,
|
||||
blink_state: None,
|
||||
status_dot_view: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -104,15 +104,22 @@ impl ProjectBox {
|
|||
pub fn init_subviews(&mut self, cx: &mut Context<Self>) {
|
||||
// Initialize sub-views after entity registration
|
||||
|
||||
// Shared blink state — separate entity, ProjectBox reads it in render.
|
||||
// cx.notify() on BlinkState only dirties views that .read() it (ProjectBox),
|
||||
// NOT ancestors (Workspace, ProjectGrid) → siblings stay cached.
|
||||
// StatusDotView: tiny Entity that reads BlinkState.
|
||||
// ProjectBox does NOT read BlinkState → doesn't re-render on blink.
|
||||
// Only StatusDotView (1 div) re-renders 2x/sec.
|
||||
let should_pulse = matches!(self.project.agent.status, AgentStatus::Running);
|
||||
if should_pulse {
|
||||
let blink = cx.new(|_cx| crate::components::blink_state::BlinkState::new());
|
||||
crate::components::blink_state::BlinkState::start_from_context(&blink, cx);
|
||||
self.blink_state = Some(blink);
|
||||
}
|
||||
let blink = if should_pulse {
|
||||
let b = cx.new(|_cx| crate::components::blink_state::BlinkState::new());
|
||||
crate::components::blink_state::BlinkState::start_from_context(&b, cx);
|
||||
Some(b)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let status = self.project.agent.status;
|
||||
let dot_view = cx.new(|_cx| {
|
||||
crate::components::blink_state::StatusDotView::new(status, blink)
|
||||
});
|
||||
self.status_dot_view = Some(dot_view);
|
||||
|
||||
// Create agent pane with demo messages
|
||||
let agent_pane = cx.new(|_cx| {
|
||||
|
|
@ -261,29 +268,10 @@ impl Render for ProjectBox {
|
|||
.rounded(px(2.0))
|
||||
.bg(accent),
|
||||
)
|
||||
// Status dot — BlinkState shared entity (2 renders/sec, 3% CPU)
|
||||
// NOTE: GPUI's AnimationElement uses request_animation_frame() which
|
||||
// runs at vsync (60fps) = 80% CPU. Fragment shaders not exposed.
|
||||
// The Zed BlinkManager pattern (timer + notify at 2fps) is the
|
||||
// correct approach for ambient animation in GPUI.
|
||||
.child({
|
||||
let blink_visible = self.blink_state.as_ref()
|
||||
.map(|bs| bs.read(cx).visible)
|
||||
.unwrap_or(true);
|
||||
let color = match self.project.agent.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()
|
||||
})
|
||||
// Status dot — separate Entity that reads BlinkState.
|
||||
// ProjectBox does NOT read BlinkState → doesn't re-render on blink.
|
||||
// Only StatusDotView (1 div, 8x8px) re-renders 2x/sec.
|
||||
.children(self.status_dot_view.clone())
|
||||
// Project name
|
||||
.child(
|
||||
div()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue