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:
Hibryda 2026-03-19 23:45:05 +01:00
parent ddec12db3f
commit 6f0f276400
3 changed files with 113 additions and 105 deletions

View file

@ -1,25 +1,25 @@
//! Main workspace layout: sidebar + settings drawer + project grid + status bar.
//! Workspace: root view composing sidebar + project boxes + status bar.
//!
//! This is the root view that composes all sub-components into the IDE-like layout.
//! ProjectBoxes are rendered DIRECTLY as children of Workspace (no intermediate
//! ProjectGrid entity). This keeps the dispatch tree at 3 levels:
//! Workspace → ProjectBox → StatusDotView (same depth as Zed's Workspace → Pane → Editor).
use gpui::*;
use crate::CachedView;
use crate::components::command_palette::CommandPalette;
use crate::components::project_grid::ProjectGrid;
use crate::components::project_box::ProjectBox;
use crate::components::settings::SettingsPanel;
use crate::components::sidebar::Sidebar;
use crate::components::status_bar::StatusBar;
use crate::state::AppState;
use crate::theme;
// ── Workspace View ──────────────────────────────────────────────────
pub struct Workspace {
#[allow(dead_code)]
app_state: Entity<AppState>,
sidebar: Entity<Sidebar>,
settings_panel: Entity<SettingsPanel>,
project_grid: Entity<ProjectGrid>,
project_boxes: Vec<Entity<ProjectBox>>,
status_bar: Entity<StatusBar>,
command_palette: Entity<CommandPalette>,
}
@ -34,10 +34,6 @@ impl Workspace {
let state = app_state.clone();
|_cx| SettingsPanel::new(state)
});
let project_grid = cx.new({
let state = app_state.clone();
|cx| ProjectGrid::new(state, cx)
});
let status_bar = cx.new({
let state = app_state.clone();
|_cx| StatusBar::new(state)
@ -47,14 +43,24 @@ impl Workspace {
|_cx| CommandPalette::new(state)
});
// NOTE: removed cx.observe(&app_state) — reading app_state.read(cx) in render
// already subscribes to changes. The observe was causing redundant re-renders.
// Create ProjectBoxes directly (no intermediate ProjectGrid entity)
let projects: Vec<_> = app_state.read(cx).projects.clone();
let project_boxes: Vec<Entity<ProjectBox>> = projects
.into_iter()
.map(|proj| {
cx.new(|cx| {
let mut pb = ProjectBox::new(proj);
pb.init_subviews(cx);
pb
})
})
.collect();
Self {
app_state,
sidebar,
settings_panel,
project_grid,
project_boxes,
status_bar,
command_palette,
}
@ -62,8 +68,7 @@ impl Workspace {
}
impl Render for Workspace {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
// Hardcoded layout state — avoids model subscription that causes re-renders
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let sidebar_open = true;
let settings_open = false;
let palette_open = false;
@ -76,7 +81,7 @@ impl Render for Workspace {
.bg(theme::CRUST)
.font_family("Inter");
// ── Main content row (sidebar + settings? + grid) ───
// Main content row
let mut main_row = div()
.id("main-row")
.flex_1()
@ -85,32 +90,34 @@ impl Render for Workspace {
.flex_row()
.overflow_hidden();
// Sidebar (icon rail) — uncached (tiny, cheap to render)
if sidebar_open {
main_row = main_row.child(self.sidebar.clone());
}
// Settings drawer (between sidebar and grid)
if settings_open {
main_row = main_row.child(self.settings_panel.clone());
}
// Project grid (fills remaining space) — cached with flex-1
// ProjectGrid cached — when StatusDotView notifies, ProjectGrid IS in dirty_views
// (ancestor walk), but the cached wrapper checks ProjectGrid's own entity_id.
// Since ProjectGrid wasn't directly notified, the cache should hit.
// Wait — mark_view_dirty inserts ALL ancestors including ProjectGrid.
// So the cache WILL miss for ProjectGrid. We need it uncached.
main_row = main_row.child(
div().flex_1().h_full().child(self.project_grid.clone()),
);
// Project grid area — inline, no intermediate entity
let mut grid = div()
.id("project-grid")
.flex_1()
.h_full()
.flex()
.flex_row()
.flex_wrap()
.gap(px(8.0))
.p(px(8.0))
.bg(theme::CRUST)
.overflow_y_scroll();
for pb in &self.project_boxes {
grid = grid.child(pb.clone());
}
main_row = main_row.child(grid);
root = root.child(main_row);
// Status bar (bottom) — uncached (tiny, cheap to render)
root = root.child(self.status_bar.clone());
// ── Command palette overlay (if open) ───────────────
if palette_open {
root = root.child(self.command_palette.clone());
}