diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 6f85debe67..6904ae14ed 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -510,6 +510,18 @@ struct SessionSummary { resume_command: Option, } +#[derive(Debug, Default)] +struct InitialHistoryReplayMetrics { + replay_elapsed: Option, + 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, pub(crate) session_telemetry: SessionTelemetry, @@ -532,6 +544,7 @@ pub(crate) struct App { pub(crate) deferred_history_lines: Vec>, has_emitted_history_lines: bool, transcript_reflow: TranscriptReflowState, + initial_history_replay_metrics: Option, 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); diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index 99677f9651..f7a8c920dd 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -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 = 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() { diff --git a/codex-rs/tui/src/app/history_ui.rs b/codex-rs/tui/src/app/history_ui.rs index 3eeb315cbc..d1c2f75702 100644 --- a/codex-rs/tui/src/app/history_ui.rs +++ b/codex-rs/tui/src/app/history_ui.rs @@ -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; } diff --git a/codex-rs/tui/src/app/resize_reflow.rs b/codex-rs/tui/src/app/resize_reflow.rs index 4208918f65..3ea43bf2ea 100644 --- a/codex-rs/tui/src/app/resize_reflow.rs +++ b/codex-rs/tui/src/app/resize_reflow.rs @@ -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, diff --git a/codex-rs/tui/src/app/test_support.rs b/codex-rs/tui/src/app/test_support.rs index d0c24a56a0..2e5c28c42f 100644 --- a/codex-rs/tui/src/app/test_support.rs +++ b/codex-rs/tui/src/app/test_support.rs @@ -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)), diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index b6c95fb667..831a270701 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -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)), diff --git a/codex-rs/tui/src/app/thread_routing.rs b/codex-rs/tui/src/app/thread_routing.rs index f6a4513d65..1abbf13499 100644 --- a/codex-rs/tui/src/app/thread_routing.rs +++ b/codex-rs/tui/src/app/thread_routing.rs @@ -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 { diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 4492adac21..7b9ff2342d 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -354,8 +354,16 @@ pub(crate) enum AppEvent { result: Result, }, + /// Begin measuring initial resume replay without changing the replay behavior. + BeginInitialHistoryReplayMeasurement, + InsertHistoryCell(Box), + /// 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. diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index d23ff732ab..9f91b02110 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -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,