wip(tui): measure initial replay scrollback cost

This commit is contained in:
Felipe Coury
2026-04-22 23:27:23 -03:00
parent 3aa637c475
commit 6cbd2adbaa
9 changed files with 157 additions and 6 deletions
+15
View File
@@ -510,6 +510,18 @@ struct SessionSummary {
resume_command: Option<String>,
}
#[derive(Debug, Default)]
struct InitialHistoryReplayMetrics {
replay_elapsed: Option<Duration>,
render_elapsed: Duration,
enqueue_elapsed: Duration,
flush_elapsed: Duration,
cell_count: usize,
rendered_line_count: usize,
flushed_line_count: usize,
replay_finished: bool,
}
pub(crate) struct App {
model_catalog: Arc<ModelCatalog>,
pub(crate) session_telemetry: SessionTelemetry,
@@ -532,6 +544,7 @@ pub(crate) struct App {
pub(crate) deferred_history_lines: Vec<Line<'static>>,
has_emitted_history_lines: bool,
transcript_reflow: TranscriptReflowState,
initial_history_replay_metrics: Option<InitialHistoryReplayMetrics>,
pub(crate) enhanced_keys_supported: bool,
@@ -918,6 +931,7 @@ impl App {
deferred_history_lines: Vec::new(),
has_emitted_history_lines: false,
transcript_reflow: TranscriptReflowState::default(),
initial_history_replay_metrics: None,
commit_anim_running: Arc::new(AtomicBool::new(false)),
status_line_invalid_items_warned: status_line_invalid_items_warned.clone(),
terminal_title_invalid_items_warned: terminal_title_invalid_items_warned.clone(),
@@ -1159,6 +1173,7 @@ impl App {
}
})?;
self.maybe_disable_slow_resize_reflow_flush(stats);
self.maybe_finish_initial_history_replay_measurement(stats);
} else {
tui.draw(desired_height, |frame| {
self.chat_widget.render(frame.area(), frame.buffer);
+19 -5
View File
@@ -176,6 +176,9 @@ impl App {
tui.frame_requester().schedule_frame();
}
AppEvent::BeginInitialHistoryReplayMeasurement => {
self.begin_initial_history_replay_measurement();
}
AppEvent::InsertHistoryCell(cell) => {
let cell: Arc<dyn HistoryCell> = cell.into();
if let Some(Overlay::Transcript(t)) = &mut self.overlay {
@@ -183,11 +186,22 @@ impl App {
tui.frame_requester().schedule_frame();
}
self.transcript_cells.push(cell.clone());
self.insert_history_cell_lines(
tui,
cell.as_ref(),
tui.terminal.last_known_screen_size.width,
);
if self.initial_history_replay_metrics.is_some() {
self.insert_history_cell_lines_with_initial_replay_measurement(
tui,
cell.as_ref(),
tui.terminal.last_known_screen_size.width,
);
} else {
self.insert_history_cell_lines(
tui,
cell.as_ref(),
tui.terminal.last_known_screen_size.width,
);
}
}
AppEvent::EndInitialHistoryReplayMeasurement { replay_elapsed } => {
self.finish_initial_history_replay_measurement(replay_elapsed);
}
AppEvent::ConsolidateAgentMessage { source, cwd } => {
if !self.terminal_resize_reflow_enabled() {
+1
View File
@@ -92,6 +92,7 @@ impl App {
self.deferred_history_lines.clear();
self.has_emitted_history_lines = false;
self.transcript_reflow.clear();
self.initial_history_replay_metrics = None;
self.backtrack = BacktrackState::default();
self.backtrack_render_pending = false;
}
+88
View File
@@ -19,6 +19,7 @@ use color_eyre::eyre::Result;
use ratatui::text::Line;
use super::App;
use super::InitialHistoryReplayMetrics;
use super::trailing_run_start;
use crate::history_cell;
use crate::history_cell::HistoryCell;
@@ -94,6 +95,93 @@ impl App {
self.terminal_resize_reflow_enabled() && !self.transcript_reflow.is_runtime_disabled()
}
pub(super) fn begin_initial_history_replay_measurement(&mut self) {
if self.terminal_resize_reflow_active() && self.overlay.is_none() {
self.initial_history_replay_metrics = Some(Default::default());
}
}
pub(super) fn finish_initial_history_replay_measurement(&mut self, replay_elapsed: Duration) {
let Some(metrics) = &mut self.initial_history_replay_metrics else {
return;
};
metrics.replay_elapsed = Some(replay_elapsed);
metrics.replay_finished = true;
}
pub(super) fn insert_history_cell_lines_with_initial_replay_measurement(
&mut self,
tui: &mut tui::Tui,
cell: &dyn HistoryCell,
width: u16,
) {
let render_started = Instant::now();
let display = self.display_lines_for_history_insert(cell, width);
let render_elapsed = render_started.elapsed();
let rendered_line_count = display.len();
if let Some(metrics) = &mut self.initial_history_replay_metrics {
metrics.cell_count += 1;
metrics.rendered_line_count += rendered_line_count;
metrics.render_elapsed += render_elapsed;
}
if display.is_empty() {
return;
}
let enqueue_started = Instant::now();
if self.overlay.is_some() {
self.deferred_history_lines.extend(display);
} else {
tui.insert_history_lines(display);
}
if let Some(metrics) = &mut self.initial_history_replay_metrics {
metrics.enqueue_elapsed += enqueue_started.elapsed();
}
}
pub(super) fn maybe_finish_initial_history_replay_measurement(
&mut self,
draw_stats: tui::ResizeReflowDrawStats,
) {
let Some(metrics) = &mut self.initial_history_replay_metrics else {
return;
};
if draw_stats.flushed_history {
metrics.flush_elapsed += draw_stats.history_flush_elapsed;
metrics.flushed_line_count += draw_stats.history_flush_line_count;
}
if !metrics.replay_finished
|| (metrics.rendered_line_count > 0
&& metrics.flushed_line_count < metrics.rendered_line_count)
{
return;
}
let metrics = self
.initial_history_replay_metrics
.take()
.expect("metrics checked above");
self.show_initial_history_replay_measurement(metrics);
}
fn show_initial_history_replay_measurement(&mut self, metrics: InitialHistoryReplayMetrics) {
let replay_ms = metrics.replay_elapsed.unwrap_or_default().as_millis();
self.chat_widget.add_info_message(
format!(
"Initial transcript replay took {replay_ms}ms to queue, {}ms to render, {}ms to enqueue, and {}ms to flush ({} rows from {} cells; {} rows flushed).",
metrics.render_elapsed.as_millis(),
metrics.enqueue_elapsed.as_millis(),
metrics.flush_elapsed.as_millis(),
metrics.rendered_line_count,
metrics.cell_count,
metrics.flushed_line_count,
),
/*hint*/ None,
);
}
fn schedule_resize_reflow(
&mut self,
kind: TranscriptReflowKind,
+1
View File
@@ -31,6 +31,7 @@ pub(super) async fn make_test_app() -> App {
deferred_history_lines: Vec::new(),
has_emitted_history_lines: false,
transcript_reflow: TranscriptReflowState::default(),
initial_history_replay_metrics: None,
enhanced_keys_supported: false,
commit_anim_running: Arc::new(AtomicBool::new(false)),
status_line_invalid_items_warned: Arc::new(AtomicBool::new(false)),
+2
View File
@@ -3570,6 +3570,7 @@ async fn make_test_app() -> App {
deferred_history_lines: Vec::new(),
has_emitted_history_lines: false,
transcript_reflow: TranscriptReflowState::default(),
initial_history_replay_metrics: None,
enhanced_keys_supported: false,
commit_anim_running: Arc::new(AtomicBool::new(false)),
status_line_invalid_items_warned: Arc::new(AtomicBool::new(false)),
@@ -3628,6 +3629,7 @@ async fn make_test_app_with_channels() -> (
deferred_history_lines: Vec::new(),
has_emitted_history_lines: false,
transcript_reflow: TranscriptReflowState::default(),
initial_history_replay_metrics: None,
enhanced_keys_supported: false,
commit_anim_running: Arc::new(AtomicBool::new(false)),
status_line_invalid_items_warned: Arc::new(AtomicBool::new(false)),
+13
View File
@@ -1029,8 +1029,21 @@ impl App {
self.chat_widget
.set_initial_user_message_submit_suppressed(/*suppressed*/ true);
self.chat_widget.handle_thread_session(session);
let should_measure_initial_replay =
self.terminal_resize_reflow_active() && !turns.is_empty();
let replay_started = should_measure_initial_replay.then(Instant::now);
if should_measure_initial_replay {
self.app_event_tx
.send(AppEvent::BeginInitialHistoryReplayMeasurement);
}
self.chat_widget
.replay_thread_turns(turns, ReplayKind::ResumeInitialMessages);
if let Some(started) = replay_started {
self.app_event_tx
.send(AppEvent::EndInitialHistoryReplayMeasurement {
replay_elapsed: started.elapsed(),
});
}
let pending = std::mem::take(&mut self.pending_primary_events);
for pending_event in pending {
match pending_event {
+8
View File
@@ -354,8 +354,16 @@ pub(crate) enum AppEvent {
result: Result<SkillsListResponse, String>,
},
/// Begin measuring initial resume replay without changing the replay behavior.
BeginInitialHistoryReplayMeasurement,
InsertHistoryCell(Box<dyn HistoryCell>),
/// Finish measuring initial resume replay after all replay events have been queued.
EndInitialHistoryReplayMeasurement {
replay_elapsed: std::time::Duration,
},
/// Replace the contiguous run of streaming `AgentMessageCell`s at the end of
/// the transcript with a single `AgentMarkdownCell` that stores the raw
/// markdown source and re-renders from it on resize.
+10 -1
View File
@@ -484,6 +484,9 @@ pub struct Tui {
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct ResizeReflowDrawStats {
pub(crate) history_flush_elapsed: Duration,
pub(crate) history_flush_line_count: usize,
pub(crate) flushed_history: bool,
pub(crate) reflow_flush_elapsed: Duration,
pub(crate) flushed_reflow_history: bool,
}
@@ -933,11 +936,17 @@ impl Tui {
let terminal = &mut self.terminal;
let mut needs_full_repaint =
Self::update_inline_viewport_for_resize_reflow(terminal, height, self.is_zellij)?;
needs_full_repaint |= Self::flush_pending_history_lines(
let history_flush_line_count = self.pending_history_lines.len();
let history_flush_start = Instant::now();
let flushed_history = Self::flush_pending_history_lines(
terminal,
&mut self.pending_history_lines,
self.is_zellij,
)?;
stats.history_flush_elapsed = history_flush_start.elapsed();
stats.history_flush_line_count = history_flush_line_count;
stats.flushed_history = history_flush_line_count > 0;
needs_full_repaint |= flushed_history;
let reflow_flush_start = Instant::now();
let flushed_reflow_history = Self::flush_pending_reflow_history_lines(
terminal,