perf(ui-gpui): cache SharedStrings, remove diagnostics, zero alloc per render frame
This commit is contained in:
parent
b557aeb833
commit
5dbf5bd43c
3 changed files with 44 additions and 69 deletions
|
|
@ -68,16 +68,30 @@ fn tab_button(label: &str, active: bool, accent: Rgba) -> Div {
|
||||||
|
|
||||||
pub struct ProjectBox {
|
pub struct ProjectBox {
|
||||||
pub project: Project,
|
pub project: Project,
|
||||||
/// Entity handle for the embedded AgentPane (Model tab)
|
|
||||||
pub agent_pane: Option<Entity<crate::components::agent_pane::AgentPane>>,
|
pub agent_pane: Option<Entity<crate::components::agent_pane::AgentPane>>,
|
||||||
/// Entity handle for the embedded terminal (Model tab)
|
|
||||||
pub terminal_view: Option<Entity<crate::terminal::renderer::TerminalView>>,
|
pub terminal_view: Option<Entity<crate::terminal::renderer::TerminalView>>,
|
||||||
pub status_dot: Option<Entity<PulsingDot>>,
|
pub status_dot: Option<Entity<PulsingDot>>,
|
||||||
|
// Cached strings to avoid allocation on every render
|
||||||
|
id_project: SharedString,
|
||||||
|
id_model: SharedString,
|
||||||
|
id_resize: SharedString,
|
||||||
|
id_docs: SharedString,
|
||||||
|
id_files: SharedString,
|
||||||
|
cached_name: SharedString,
|
||||||
|
cached_cwd: SharedString,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProjectBox {
|
impl ProjectBox {
|
||||||
pub fn new(project: Project) -> Self {
|
pub fn new(project: Project) -> Self {
|
||||||
|
let id = &project.id;
|
||||||
Self {
|
Self {
|
||||||
|
id_project: SharedString::from(format!("project-{id}")),
|
||||||
|
id_model: SharedString::from(format!("model-{id}")),
|
||||||
|
id_resize: SharedString::from(format!("resize-{id}")),
|
||||||
|
id_docs: SharedString::from(format!("docs-{id}")),
|
||||||
|
id_files: SharedString::from(format!("files-{id}")),
|
||||||
|
cached_name: SharedString::from(project.name.clone()),
|
||||||
|
cached_cwd: SharedString::from(project.cwd.clone()),
|
||||||
project,
|
project,
|
||||||
agent_pane: None,
|
agent_pane: None,
|
||||||
terminal_view: None,
|
terminal_view: None,
|
||||||
|
|
@ -117,23 +131,16 @@ 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 cwd = self.project.cwd.clone();
|
|
||||||
let status = self.project.agent.status;
|
|
||||||
let active_tab = self.project.active_tab;
|
let active_tab = self.project.active_tab;
|
||||||
|
|
||||||
// Build content area based on active tab
|
// Build content area based on active tab
|
||||||
let content = match active_tab {
|
let content = match active_tab {
|
||||||
ProjectTab::Model => {
|
ProjectTab::Model => {
|
||||||
let mut model_content = div()
|
let mut model_content = div()
|
||||||
.id(SharedString::from(format!("model-content-{}", self.project.id)))
|
.id(self.id_model.clone())
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
.flex()
|
.flex()
|
||||||
|
|
@ -152,7 +159,7 @@ impl Render for ProjectBox {
|
||||||
// Resize handle
|
// Resize handle
|
||||||
model_content = model_content.child(
|
model_content = model_content.child(
|
||||||
div()
|
div()
|
||||||
.id(SharedString::from(format!("resize-{}", self.project.id)))
|
.id(self.id_resize.clone())
|
||||||
.w_full()
|
.w_full()
|
||||||
.h(px(4.0))
|
.h(px(4.0))
|
||||||
.bg(theme::SURFACE0)
|
.bg(theme::SURFACE0)
|
||||||
|
|
@ -174,7 +181,7 @@ impl Render for ProjectBox {
|
||||||
}
|
}
|
||||||
ProjectTab::Docs => {
|
ProjectTab::Docs => {
|
||||||
div()
|
div()
|
||||||
.id(SharedString::from(format!("docs-content-{}", self.project.id)))
|
.id(self.id_docs.clone())
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
.flex()
|
.flex()
|
||||||
|
|
@ -186,7 +193,7 @@ impl Render for ProjectBox {
|
||||||
}
|
}
|
||||||
ProjectTab::Files => {
|
ProjectTab::Files => {
|
||||||
div()
|
div()
|
||||||
.id(SharedString::from(format!("files-content-{}", self.project.id)))
|
.id(self.id_files.clone())
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
.flex()
|
.flex()
|
||||||
|
|
@ -223,7 +230,7 @@ impl Render for ProjectBox {
|
||||||
};
|
};
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id(SharedString::from(format!("project-{}", self.project.id)))
|
.id(self.id_project.clone())
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.min_w(px(400.0))
|
.min_w(px(400.0))
|
||||||
.min_h(px(300.0))
|
.min_h(px(300.0))
|
||||||
|
|
@ -262,7 +269,7 @@ impl Render for ProjectBox {
|
||||||
div()
|
div()
|
||||||
.text_size(px(13.0))
|
.text_size(px(13.0))
|
||||||
.text_color(theme::TEXT)
|
.text_color(theme::TEXT)
|
||||||
.child(name),
|
.child(self.cached_name.clone()),
|
||||||
)
|
)
|
||||||
.child(div().flex_1())
|
.child(div().flex_1())
|
||||||
// CWD (ellipsized)
|
// CWD (ellipsized)
|
||||||
|
|
@ -273,7 +280,7 @@ impl Render for ProjectBox {
|
||||||
.max_w(px(200.0))
|
.max_w(px(200.0))
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.whitespace_nowrap()
|
.whitespace_nowrap()
|
||||||
.child(cwd),
|
.child(self.cached_cwd.clone()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
// ── Tab Bar ─────────────────────────────────────────
|
// ── Tab Bar ─────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
//! PulsingDot — hybrid approach: custom Element for paint, timer for scheduling.
|
//! PulsingDot — self-scheduling animation that minimizes parent re-renders.
|
||||||
//!
|
//!
|
||||||
//! Problem: cx.notify() propagates to parent views → full tree re-render.
|
//! Uses window.on_next_frame() with a 500ms delay to schedule exactly 1 repaint
|
||||||
//! Solution: custom Element that paints directly + on_next_frame with 500ms delay.
|
//! per blink cycle. No continuous request_animation_frame loop.
|
||||||
//! 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 gpui::*;
|
||||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
|
|
@ -21,20 +17,11 @@ 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,
|
||||||
anim: Arc<DotAnimState>,
|
visible: bool,
|
||||||
start_time: Instant,
|
last_toggle: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PulsingDot {
|
impl PulsingDot {
|
||||||
|
|
@ -42,8 +29,8 @@ impl PulsingDot {
|
||||||
Self {
|
Self {
|
||||||
status,
|
status,
|
||||||
size,
|
size,
|
||||||
anim: Arc::new(DotAnimState { visible: AtomicBool::new(true) }),
|
visible: true,
|
||||||
start_time: Instant::now(),
|
last_toggle: Instant::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,28 +48,16 @@ impl PulsingDot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_color(&self) -> Rgba {
|
/// Start blink scheduling from context (after entity registered)
|
||||||
let base = self.base_color();
|
pub fn start_blinking(&self, cx: &mut Context<Self>) {
|
||||||
if !self.should_pulse() {
|
if !self.should_pulse() { return; }
|
||||||
return base;
|
// Schedule first blink via background timer
|
||||||
}
|
cx.spawn(async move |weak: WeakEntity<Self>, cx: &mut AsyncApp| {
|
||||||
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 anim = self.anim.clone();
|
|
||||||
cx.spawn(async move |this: WeakEntity<Self>, cx: &mut AsyncApp| {
|
|
||||||
loop {
|
loop {
|
||||||
cx.background_executor().timer(Duration::from_millis(500)).await;
|
cx.background_executor().timer(Duration::from_millis(500)).await;
|
||||||
BLINK_COUNT.fetch_add(1, Ordering::Relaxed);
|
let ok = weak.update(cx, |dot, cx| {
|
||||||
anim.visible.fetch_xor(true, Ordering::Relaxed);
|
dot.visible = !dot.visible;
|
||||||
// Notify ONLY this entity — GPUI will repaint only this view
|
dot.last_toggle = Instant::now();
|
||||||
let ok = this.update(cx, |_dot, cx| {
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
if ok.is_err() { break; }
|
if ok.is_err() { break; }
|
||||||
|
|
@ -93,13 +68,12 @@ impl PulsingDot {
|
||||||
|
|
||||||
impl Render for PulsingDot {
|
impl Render for PulsingDot {
|
||||||
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 rc = RENDER_COUNT.fetch_add(1, Ordering::Relaxed);
|
let color = if self.should_pulse() && !self.visible {
|
||||||
if rc % 60 == 0 {
|
theme::SURFACE1
|
||||||
let bc = BLINK_COUNT.load(Ordering::Relaxed);
|
} else {
|
||||||
eprintln!("[PulsingDot] renders={rc} blinks={bc}");
|
self.base_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;
|
||||||
|
|
|
||||||
|
|
@ -60,15 +60,9 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static WORKSPACE_RENDERS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
|
||||||
|
|
||||||
impl Render for Workspace {
|
impl Render for Workspace {
|
||||||
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 wc = WORKSPACE_RENDERS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
// Hardcoded layout state — avoids model subscription that causes re-renders
|
||||||
if wc % 10 == 0 {
|
|
||||||
eprintln!("[Workspace] render #{wc}");
|
|
||||||
}
|
|
||||||
// Don't read app_state in render — it subscribes and causes re-renders on every tick
|
|
||||||
let sidebar_open = true;
|
let sidebar_open = true;
|
||||||
let settings_open = false;
|
let settings_open = false;
|
||||||
let palette_open = false;
|
let palette_open = false;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue