mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
fix(tui/autodrive): avoid full render rebuilds (fixes #485)
This commit is contained in:
+187
-82
@@ -37790,67 +37790,12 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
|
||||
self.ensure_render_request_cache();
|
||||
|
||||
let render_requests: Vec<RenderRequest> = {
|
||||
let render_request_cache = self.render_request_cache.borrow();
|
||||
let mut render_requests =
|
||||
Vec::with_capacity(self.history_cells.len().saturating_add(3));
|
||||
for (cell, seed) in self
|
||||
.history_cells
|
||||
.iter()
|
||||
.zip(render_request_cache.iter())
|
||||
{
|
||||
let assistant = cell
|
||||
.as_any()
|
||||
.downcast_ref::<crate::history_cell::AssistantMarkdownCell>();
|
||||
render_requests.push(RenderRequest {
|
||||
history_id: seed.history_id,
|
||||
cell: Some(cell.as_ref()),
|
||||
assistant,
|
||||
use_cache: seed.use_cache,
|
||||
fallback_lines: seed.fallback_lines.clone(),
|
||||
kind: seed.kind.clone(),
|
||||
config: &self.config,
|
||||
});
|
||||
}
|
||||
let extra_count = (self.active_exec_cell.is_some() as usize)
|
||||
.saturating_add(streaming_cell.is_some() as usize)
|
||||
.saturating_add(queued_preview_cells.len());
|
||||
let request_count = self.history_cells.len().saturating_add(extra_count);
|
||||
|
||||
if let Some(ref cell) = self.active_exec_cell {
|
||||
render_requests.push(RenderRequest {
|
||||
history_id: HistoryId::ZERO,
|
||||
cell: Some(cell as &dyn HistoryCell),
|
||||
assistant: None,
|
||||
use_cache: false,
|
||||
fallback_lines: None,
|
||||
kind: RenderRequestKind::Legacy,
|
||||
config: &self.config,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(ref cell) = streaming_cell {
|
||||
render_requests.push(RenderRequest {
|
||||
history_id: HistoryId::ZERO,
|
||||
cell: Some(cell as &dyn HistoryCell),
|
||||
assistant: None,
|
||||
use_cache: false,
|
||||
fallback_lines: None,
|
||||
kind: RenderRequestKind::Legacy,
|
||||
config: &self.config,
|
||||
});
|
||||
}
|
||||
|
||||
for c in &queued_preview_cells {
|
||||
render_requests.push(RenderRequest {
|
||||
history_id: HistoryId::ZERO,
|
||||
cell: Some(c as &dyn HistoryCell),
|
||||
assistant: None,
|
||||
use_cache: false,
|
||||
fallback_lines: None,
|
||||
kind: RenderRequestKind::Legacy,
|
||||
config: &self.config,
|
||||
});
|
||||
}
|
||||
|
||||
render_requests
|
||||
};
|
||||
let mut render_requests_full: Option<Vec<RenderRequest>> = None;
|
||||
|
||||
// Calculate total content height using prefix sums; build if needed
|
||||
let spacing = 1u16; // Standard spacing between cells
|
||||
@@ -37876,14 +37821,82 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
self.history_virtualization_sync_pending.set(true);
|
||||
self.app_event_tx.send(AppEvent::SyncHistoryVirtualization);
|
||||
}
|
||||
let request_count = render_requests.len();
|
||||
let perf_enabled = self.perf_state.enabled;
|
||||
let needs_prefix_rebuild =
|
||||
self.history_render
|
||||
.should_rebuild_prefix(content_area.width, request_count);
|
||||
|
||||
let perf_enabled = self.perf_state.enabled;
|
||||
let mut rendered_cells_full: Option<Vec<VisibleCell>> = None;
|
||||
if needs_prefix_rebuild {
|
||||
if render_requests_full.is_none() {
|
||||
let render_request_cache = self.render_request_cache.borrow();
|
||||
let mut render_requests = Vec::with_capacity(request_count);
|
||||
for (cell, seed) in self
|
||||
.history_cells
|
||||
.iter()
|
||||
.zip(render_request_cache.iter())
|
||||
{
|
||||
let assistant = cell
|
||||
.as_any()
|
||||
.downcast_ref::<crate::history_cell::AssistantMarkdownCell>();
|
||||
render_requests.push(RenderRequest {
|
||||
history_id: seed.history_id,
|
||||
cell: Some(cell.as_ref()),
|
||||
assistant,
|
||||
use_cache: seed.use_cache,
|
||||
fallback_lines: seed.fallback_lines.clone(),
|
||||
kind: seed.kind.clone(),
|
||||
config: &self.config,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(ref cell) = self.active_exec_cell {
|
||||
render_requests.push(RenderRequest {
|
||||
history_id: HistoryId::ZERO,
|
||||
cell: Some(cell as &dyn HistoryCell),
|
||||
assistant: None,
|
||||
use_cache: false,
|
||||
fallback_lines: None,
|
||||
kind: RenderRequestKind::Legacy,
|
||||
config: &self.config,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(ref cell) = streaming_cell {
|
||||
render_requests.push(RenderRequest {
|
||||
history_id: HistoryId::ZERO,
|
||||
cell: Some(cell as &dyn HistoryCell),
|
||||
assistant: None,
|
||||
use_cache: false,
|
||||
fallback_lines: None,
|
||||
kind: RenderRequestKind::Legacy,
|
||||
config: &self.config,
|
||||
});
|
||||
}
|
||||
|
||||
for c in &queued_preview_cells {
|
||||
render_requests.push(RenderRequest {
|
||||
history_id: HistoryId::ZERO,
|
||||
cell: Some(c as &dyn HistoryCell),
|
||||
assistant: None,
|
||||
use_cache: false,
|
||||
fallback_lines: None,
|
||||
kind: RenderRequestKind::Legacy,
|
||||
config: &self.config,
|
||||
});
|
||||
}
|
||||
|
||||
if perf_enabled {
|
||||
let mut p = self.perf_state.stats.borrow_mut();
|
||||
p.render_requests_full =
|
||||
p.render_requests_full.saturating_add(render_requests.len() as u64);
|
||||
}
|
||||
|
||||
render_requests_full = Some(render_requests);
|
||||
}
|
||||
|
||||
let render_requests = render_requests_full
|
||||
.as_ref()
|
||||
.expect("render requests missing after rebuild");
|
||||
let mut used_fast_append = false;
|
||||
if self.try_append_prefix_fast(&render_requests, render_settings, content_area.width) {
|
||||
used_fast_append = true;
|
||||
@@ -38169,13 +38182,105 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
};
|
||||
end_idx = end_idx.saturating_add(1).min(request_count);
|
||||
|
||||
enum VisibleRequests<'a> {
|
||||
Full(&'a [RenderRequest<'a>]),
|
||||
Owned(Vec<RenderRequest<'a>>),
|
||||
}
|
||||
|
||||
let history_len = self.history_cells.len();
|
||||
let visible_requests = if let Some(ref full_requests) = render_requests_full {
|
||||
VisibleRequests::Full(&full_requests[start_idx..end_idx])
|
||||
} else {
|
||||
let render_request_cache = self.render_request_cache.borrow();
|
||||
let mut requests = Vec::with_capacity(end_idx.saturating_sub(start_idx));
|
||||
for idx in start_idx..end_idx {
|
||||
if idx < history_len {
|
||||
let cell = &self.history_cells[idx];
|
||||
let seed = &render_request_cache[idx];
|
||||
let assistant = cell
|
||||
.as_any()
|
||||
.downcast_ref::<crate::history_cell::AssistantMarkdownCell>();
|
||||
requests.push(RenderRequest {
|
||||
history_id: seed.history_id,
|
||||
cell: Some(cell.as_ref()),
|
||||
assistant,
|
||||
use_cache: seed.use_cache,
|
||||
fallback_lines: seed.fallback_lines.clone(),
|
||||
kind: seed.kind.clone(),
|
||||
config: &self.config,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let extra_idx = idx.saturating_sub(history_len);
|
||||
let mut extra_cursor = 0usize;
|
||||
if let Some(ref cell) = self.active_exec_cell {
|
||||
if extra_idx == extra_cursor {
|
||||
requests.push(RenderRequest {
|
||||
history_id: HistoryId::ZERO,
|
||||
cell: Some(cell as &dyn HistoryCell),
|
||||
assistant: None,
|
||||
use_cache: false,
|
||||
fallback_lines: None,
|
||||
kind: RenderRequestKind::Legacy,
|
||||
config: &self.config,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
extra_cursor = extra_cursor.saturating_add(1);
|
||||
}
|
||||
|
||||
if let Some(ref cell) = streaming_cell {
|
||||
if extra_idx == extra_cursor {
|
||||
requests.push(RenderRequest {
|
||||
history_id: HistoryId::ZERO,
|
||||
cell: Some(cell as &dyn HistoryCell),
|
||||
assistant: None,
|
||||
use_cache: false,
|
||||
fallback_lines: None,
|
||||
kind: RenderRequestKind::Legacy,
|
||||
config: &self.config,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
extra_cursor = extra_cursor.saturating_add(1);
|
||||
}
|
||||
|
||||
let queued_idx = extra_idx.saturating_sub(extra_cursor);
|
||||
if let Some(cell) = queued_preview_cells.get(queued_idx) {
|
||||
requests.push(RenderRequest {
|
||||
history_id: HistoryId::ZERO,
|
||||
cell: Some(cell as &dyn HistoryCell),
|
||||
assistant: None,
|
||||
use_cache: false,
|
||||
fallback_lines: None,
|
||||
kind: RenderRequestKind::Legacy,
|
||||
config: &self.config,
|
||||
});
|
||||
}
|
||||
}
|
||||
VisibleRequests::Owned(requests)
|
||||
};
|
||||
|
||||
let visible_requests_slice = match &visible_requests {
|
||||
VisibleRequests::Full(slice) => *slice,
|
||||
VisibleRequests::Owned(vec) => vec.as_slice(),
|
||||
};
|
||||
|
||||
if perf_enabled {
|
||||
let mut p = self.perf_state.stats.borrow_mut();
|
||||
p.render_requests_visible = p
|
||||
.render_requests_visible
|
||||
.saturating_add(visible_requests_slice.len() as u64);
|
||||
}
|
||||
|
||||
let mut _subset_rendered: Option<Vec<VisibleCell>> = None;
|
||||
let visible_slice: &[VisibleCell] = if let Some(ref full) = rendered_cells_full {
|
||||
&full[start_idx..end_idx]
|
||||
} else {
|
||||
_subset_rendered = Some(self.history_render.visible_cells(
|
||||
&self.history_state,
|
||||
&render_requests[start_idx..end_idx],
|
||||
visible_requests_slice,
|
||||
render_settings,
|
||||
));
|
||||
_subset_rendered.as_ref().map(|v| v.as_slice()).unwrap_or(&[])
|
||||
@@ -38209,6 +38314,19 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
}
|
||||
|
||||
let mut height_mismatches: Vec<HeightMismatch> = Vec::new();
|
||||
let is_collapsed_reasoning_at = |idx: usize| {
|
||||
if idx >= request_count {
|
||||
return false;
|
||||
}
|
||||
if idx < history_len {
|
||||
return self.history_cells[idx]
|
||||
.as_any()
|
||||
.downcast_ref::<crate::history_cell::CollapsibleReasoningCell>()
|
||||
.map(|rc| rc.is_collapsed())
|
||||
.unwrap_or(false);
|
||||
}
|
||||
false
|
||||
};
|
||||
|
||||
for (offset, visible) in visible_slice.iter().enumerate() {
|
||||
let idx = start_idx + offset;
|
||||
@@ -38233,7 +38351,7 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
|
||||
let item_height = visible.height;
|
||||
if content_area.width > 0 {
|
||||
if let Some(req) = render_requests.get(idx) {
|
||||
if let Some(req) = visible_requests_slice.get(offset) {
|
||||
if req.history_id != HistoryId::ZERO
|
||||
&& matches!(item.kind(), history_cell::HistoryCellType::Reasoning)
|
||||
{
|
||||
@@ -38635,24 +38753,11 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
.map(|rc| rc.is_collapsed())
|
||||
.unwrap_or(false);
|
||||
if this_is_collapsed_reasoning {
|
||||
let prev_is_collapsed_reasoning = render_requests
|
||||
.get(idx.saturating_sub(1))
|
||||
.and_then(|req| req.cell)
|
||||
.and_then(|c| {
|
||||
c.as_any()
|
||||
.downcast_ref::<crate::history_cell::CollapsibleReasoningCell>()
|
||||
.map(|rc| rc.is_collapsed())
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let next_is_collapsed_reasoning = render_requests
|
||||
.get(idx + 1)
|
||||
.and_then(|req| req.cell)
|
||||
.and_then(|c| {
|
||||
c.as_any()
|
||||
.downcast_ref::<crate::history_cell::CollapsibleReasoningCell>()
|
||||
.map(|rc| rc.is_collapsed())
|
||||
})
|
||||
let prev_is_collapsed_reasoning = idx
|
||||
.checked_sub(1)
|
||||
.map(is_collapsed_reasoning_at)
|
||||
.unwrap_or(false);
|
||||
let next_is_collapsed_reasoning = is_collapsed_reasoning_at(idx + 1);
|
||||
if prev_is_collapsed_reasoning && next_is_collapsed_reasoning {
|
||||
should_add_spacing = false;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ pub struct PerfStats {
|
||||
pub cells_overlay_scrim: u64,
|
||||
pub ns_overlay_body_bg: u128,
|
||||
pub cells_overlay_body_bg: u64,
|
||||
pub render_requests_full: u64,
|
||||
pub render_requests_visible: u64,
|
||||
// Hotspots: time spent computing heights on cache misses
|
||||
pub hot_total: std::collections::HashMap<(usize, u16), ItemStat>,
|
||||
pub hot_render: std::collections::HashMap<(usize, u16), ItemStat>,
|
||||
@@ -61,9 +63,11 @@ impl PerfStats {
|
||||
let ms_overlay_body = (self.ns_overlay_body_bg as f64) / 1_000_000.0;
|
||||
let mut out = String::new();
|
||||
out.push_str(&format!(
|
||||
"perf: frames={}\n prefix_rebuilds={}\n height_cache: total hits={} misses={}\n height_cache (render): hits={} misses={}\n time: total_height={:.2}ms render_visible={:.2}ms\n time: widget_render_total={:.2}ms\n paint: history_clear={:.2}ms (cells={}) gutter_bg={:.2}ms (cells={})\n paint: overlay_scrim={:.2}ms (cells={}) overlay_body_bg={:.2}ms (cells={})",
|
||||
"perf: frames={}\n prefix_rebuilds={}\n render_requests: full={} visible={}\n height_cache: total hits={} misses={}\n height_cache (render): hits={} misses={}\n time: total_height={:.2}ms render_visible={:.2}ms\n time: widget_render_total={:.2}ms\n paint: history_clear={:.2}ms (cells={}) gutter_bg={:.2}ms (cells={})\n paint: overlay_scrim={:.2}ms (cells={}) overlay_body_bg={:.2}ms (cells={})",
|
||||
self.frames,
|
||||
self.prefix_rebuilds,
|
||||
self.render_requests_full,
|
||||
self.render_requests_visible,
|
||||
self.height_hits_total,
|
||||
self.height_misses_total,
|
||||
self.height_hits_render,
|
||||
@@ -213,4 +217,3 @@ impl PerfStats {
|
||||
self.ns_undo_restore = self.ns_undo_restore.saturating_add(ns);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use super::{ChatWidget, ExecCallId, RunningCommand};
|
||||
use super::{perf::PerfStats, ChatWidget, ExecCallId, RunningCommand};
|
||||
use crate::app_event::{AppEvent, AutoContinueMode};
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::history_cell::{self, HistoryCellType};
|
||||
@@ -286,6 +286,18 @@ impl ChatWidgetHarness {
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn enable_perf(&mut self, enabled: bool) {
|
||||
self.chat.enable_perf(enabled);
|
||||
}
|
||||
|
||||
pub fn perf_stats_snapshot(&self) -> PerfStats {
|
||||
self.chat.perf_state.stats.borrow().clone()
|
||||
}
|
||||
|
||||
pub fn history_len(&self) -> usize {
|
||||
self.chat.history_cells.len()
|
||||
}
|
||||
|
||||
pub fn bottom_spacer_lines(&self) -> u16 {
|
||||
self.chat.history_render.bottom_spacer_lines_for_test()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#![cfg(test)]
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use code_tui::test_helpers::{
|
||||
render_chat_widget_to_vt100, AutoContinueModeFixture, ChatWidgetHarness,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn auto_drive_countdown_keeps_render_requests_local() {
|
||||
let mut harness = ChatWidgetHarness::new();
|
||||
harness.enable_perf(true);
|
||||
|
||||
for idx in 0..300 {
|
||||
harness.push_user_prompt(format!(
|
||||
"User {idx}: keep extending the long AutoDrive session transcript."
|
||||
));
|
||||
harness.push_assistant_markdown(format!(
|
||||
"Assistant {idx}: adding more content so the history grows large enough to exercise render performance."
|
||||
));
|
||||
}
|
||||
|
||||
harness.auto_drive_activate(
|
||||
"Diagnose long-running session performance",
|
||||
false,
|
||||
false,
|
||||
AutoContinueModeFixture::TenSeconds,
|
||||
);
|
||||
harness.auto_drive_set_awaiting_submission(
|
||||
"Continue",
|
||||
"Waiting for coordinator",
|
||||
None,
|
||||
);
|
||||
|
||||
let _ = render_chat_widget_to_vt100(&mut harness, 120, 40);
|
||||
let before = harness.perf_stats_snapshot();
|
||||
|
||||
harness.auto_drive_advance_countdown(9);
|
||||
let _ = render_chat_widget_to_vt100(&mut harness, 120, 40);
|
||||
let after = harness.perf_stats_snapshot();
|
||||
|
||||
let full_delta = after
|
||||
.render_requests_full
|
||||
.saturating_sub(before.render_requests_full);
|
||||
let visible_delta = after
|
||||
.render_requests_visible
|
||||
.saturating_sub(before.render_requests_visible);
|
||||
let history_len = harness.history_len();
|
||||
|
||||
assert!(
|
||||
history_len > 100,
|
||||
"history should be large enough to stress render requests"
|
||||
);
|
||||
assert_eq!(
|
||||
full_delta, 0,
|
||||
"stable history should not rebuild full render requests on countdown ticks"
|
||||
);
|
||||
assert!(
|
||||
visible_delta > 0 && (visible_delta as usize) < history_len,
|
||||
"visible render requests should stay below the full history size"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user