[1 of 2] Optimize TUI startup terminal probes (#23175)

## Why

Codex TUI startup still feels slower than 0.117.0 after the app-server
move in 0.118.0. A visible chunk of launch-to-input latency comes from
serial terminal startup probes: cursor position, keyboard enhancement
support, and default foreground/background color queries can each wait
on terminal responses before the first usable frame.

Refs #16335.

## What

This PR batches the terminal startup probes into one bounded probe. It
also reuses the probed cursor position and default colors during TUI
setup, fast-paths the primary-device-attributes fallback as keyboard
enhancement unsupported, and keeps lightweight startup timing logs for
future tuning.

The startup telemetry is intentionally left in production: it records
phase timings for terminal probes and initial-frame scheduling so future
startup regressions can be diagnosed from normal logs rather than
re-adding one-off debug instrumentation.

## Benchmark

In the local pty startup benchmark, the pre-optimization `main` baseline
was about 250.5ms median from launch to accepted chat input. This
probe-only branch measured about 152ms median, for an approximate
savings of 95-100ms.

## Stack

1. [#23175: [1 of 2] Optimize TUI startup terminal
probes](https://github.com/openai/codex/pull/23175) — this PR
2. [#23176: [2 of 2] Start fresh TUI thread in
background](https://github.com/openai/codex/pull/23176) — layered on
this PR

## Verification

- `cargo test -p codex-tui`
This commit is contained in:
Eric Traut
2026-05-18 09:04:02 -07:00
committed by GitHub
Unverified
parent e734cb5713
commit adca1b643f
5 changed files with 254 additions and 73 deletions
+19
View File
@@ -660,6 +660,7 @@ impl App {
startup_hooks_browser: Option<HooksListEntry>,
) -> Result<AppExitInfo> {
use tokio_stream::StreamExt;
let startup_started_at = Instant::now();
let (app_event_tx, mut app_event_rx) = unbounded_channel();
let app_event_tx = AppEventSender::new(app_event_tx);
emit_project_config_warnings(&app_event_tx, &config);
@@ -703,7 +704,9 @@ impl App {
});
}
};
let bootstrap_started_at = Instant::now();
let bootstrap = app_server.bootstrap(&config).await?;
let bootstrap_ms = bootstrap_started_at.elapsed().as_millis();
let mut model = bootstrap.default_model;
let available_models = bootstrap.available_models;
let exit_info = handle_model_migration_prompt_if_needed(
@@ -760,8 +763,10 @@ impl App {
let workspace_command_runner: WorkspaceCommandRunner = Arc::new(
AppServerWorkspaceCommandRunner::new(app_server.request_handle()),
);
let runtime_model_provider_started_at = Instant::now();
let runtime_model_provider_base_url =
resolve_runtime_model_provider_base_url(&config.model_provider).await;
let runtime_model_provider_ms = runtime_model_provider_started_at.elapsed().as_millis();
let enhanced_keys_supported = tui.enhanced_keys_supported();
let wait_for_initial_session_configured =
@@ -772,6 +777,7 @@ impl App {
&initial_prompt,
&initial_images,
);
let thread_and_widget_started_at = Instant::now();
let (mut chat_widget, initial_started_thread) = match session_selection {
SessionSelection::StartFresh | SessionSelection::Exit => {
let started = app_server.start_thread(&config).await?;
@@ -888,6 +894,7 @@ impl App {
(ChatWidget::new_with_app_event(init), Some(forked))
}
};
let thread_and_widget_ms = thread_and_widget_started_at.elapsed().as_millis();
if let Some(message) = external_agent_config_migration_message {
chat_widget.add_info_message(message, /*hint*/ None);
}
@@ -958,6 +965,7 @@ See the Codex keymap documentation for supported actions and examples."
if let Some(entry) = startup_hooks_browser {
app.chat_widget.open_hooks_browser(entry);
}
let initial_session_started_at = Instant::now();
if let Some(started) = initial_started_thread {
let thread_id = started.session.thread_id;
app.enqueue_primary_thread_session(started.session, started.turns)
@@ -967,6 +975,7 @@ See the Codex keymap documentation for supported actions and examples."
.await;
}
}
let initial_session_ms = initial_session_started_at.elapsed().as_millis();
// On startup, if a managed filesystem sandbox is active, warn about
// world-writable dirs on Windows.
@@ -996,10 +1005,20 @@ See the Codex keymap documentation for supported actions and examples."
}
}
let event_stream_started_at = Instant::now();
let tui_events = tui.event_stream();
tokio::pin!(tui_events);
tui.frame_requester().schedule_frame();
tracing::info!(
duration_ms = %startup_started_at.elapsed().as_millis(),
bootstrap_ms = %bootstrap_ms,
runtime_model_provider_ms = %runtime_model_provider_ms,
thread_and_widget_ms = %thread_and_widget_ms,
initial_session_ms = %initial_session_ms,
event_stream_ms = %event_stream_started_at.elapsed().as_millis(),
"tui startup initial frame scheduled"
);
app.refresh_startup_skills(&app_server);
// Kick off a non-blocking rate-limit prefetch so the first `/status`
// already has data, without delaying the initial frame render.
+6 -3
View File
@@ -1219,10 +1219,13 @@ async fn run_ratatui_app(
tracing::error!("panic: {info}");
prev_hook(info);
}));
let mut terminal = tui::init()?;
terminal.clear()?;
let mut initialized_terminal = tui::init()?;
initialized_terminal.terminal.clear()?;
let mut tui = Tui::new(terminal);
let mut tui = Tui::new(
initialized_terminal.terminal,
initialized_terminal.enhanced_keys_supported,
);
let mut terminal_restore_guard = TerminalRestoreGuard::new();
#[cfg(not(debug_assertions))]
+25
View File
@@ -68,6 +68,13 @@ pub fn default_bg() -> Option<(u8, u8, u8)> {
default_colors().map(|c| c.bg)
}
#[cfg(unix)]
pub(crate) fn set_default_colors_from_startup_probe(
colors: Option<crate::terminal_probe::DefaultColors>,
) {
imp::set_default_colors_from_startup_probe(colors);
}
#[cfg(all(unix, not(test)))]
mod imp {
use super::DefaultColors;
@@ -112,6 +119,18 @@ mod imp {
cache.get_or_init_with(query_default_colors)
}
pub(super) fn set_default_colors_from_startup_probe(
colors: Option<crate::terminal_probe::DefaultColors>,
) {
if let Ok(mut cache) = default_colors_cache().lock() {
cache.value = colors.map(|colors| DefaultColors {
fg: colors.fg,
bg: colors.bg,
});
cache.attempted = true;
}
}
pub(super) fn requery_default_colors() {
if let Ok(mut cache) = default_colors_cache().lock() {
// Don't try to refresh if the cache is already attempted and failed.
@@ -166,6 +185,12 @@ mod imp {
None
}
#[cfg(unix)]
pub(super) fn set_default_colors_from_startup_probe(
_colors: Option<crate::terminal_probe::DefaultColors>,
) {
}
pub(super) fn requery_default_colors() {}
}
+143 -47
View File
@@ -39,6 +39,21 @@ mod imp {
pub(crate) bg: (u8, u8, u8),
}
/// Results from the TUI's one-shot startup terminal probe.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub(crate) struct StartupProbe {
pub(crate) cursor_position: Option<Position>,
pub(crate) default_colors: Option<DefaultColors>,
pub(crate) keyboard_enhancement_supported: Option<bool>,
}
/// Whether the startup probe should query keyboard enhancement support.
#[derive(Clone, Copy, Eq, PartialEq)]
pub(crate) enum StartupKeyboardEnhancementProbe {
Query,
Skip,
}
/// Temporary terminal handle used while a startup probe owns terminal input.
///
/// The preferred path is duplicated stdin/stdout, because terminal replies are delivered to the
@@ -198,19 +213,6 @@ mod imp {
Ok(unsafe { File::from_raw_fd(duplicated) })
}
/// Queries the current cursor position and returns a zero-based Ratatui position.
///
/// A timeout or a non-CPR response is not fatal. Callers should treat `Ok(None)` as "terminal
/// did not answer this optional query" and choose a conservative fallback.
pub(crate) fn cursor_position(timeout: Duration) -> io::Result<Option<Position>> {
let mut tty = Tty::open()?;
tty.write_all(b"\x1B[6n")?;
let Some(response) = read_until(&mut tty, timeout, parse_cursor_position)? else {
return Ok(None);
};
Ok(Some(response))
}
/// Queries OSC 10 and OSC 11 default colors under one shared deadline.
///
/// Foreground and background are only useful as a pair for palette calculations, so a missing
@@ -226,16 +228,24 @@ mod imp {
Ok(Some(colors))
}
/// Checks whether the terminal reports support for keyboard enhancement flags.
/// Runs the optional terminal queries needed during TUI startup under one shared deadline.
///
/// The probe sends the kitty keyboard-status query followed by primary-device-attributes as a
/// fallback. A PDA response proves that the terminal answered but does not prove that keyboard
/// enhancement is unsupported until the bounded wait has expired; flags that arrive later in
/// the same deadline must still win.
pub(crate) fn keyboard_enhancement_supported(timeout: Duration) -> io::Result<Option<bool>> {
/// Keeping these queries batched avoids paying one timeout per unsupported capability before
/// the first frame can render.
pub(crate) fn startup(
timeout: Duration,
keyboard_probe: StartupKeyboardEnhancementProbe,
) -> io::Result<StartupProbe> {
let mut tty = Tty::open()?;
tty.write_all(b"\x1B[?u\x1B[c")?;
read_keyboard_enhancement_supported(&mut tty, timeout)
match keyboard_probe {
StartupKeyboardEnhancementProbe::Query => {
tty.write_all(b"\x1B[6n\x1B]10;?\x1B\\\x1B]11;?\x1B\\\x1B[?u\x1B[c")?;
}
StartupKeyboardEnhancementProbe::Skip => {
tty.write_all(b"\x1B[6n\x1B]10;?\x1B\\\x1B]11;?\x1B\\")?;
}
}
read_startup_probe(&mut tty, timeout, keyboard_probe)
}
/// Reads available terminal bytes until `parse` recognizes a probe response or time expires.
@@ -265,42 +275,95 @@ mod imp {
}
}
/// Reads keyboard-enhancement responses while giving flags the full bounded window to arrive.
fn read_keyboard_enhancement_supported(
fn read_startup_probe(
tty: &mut Tty,
timeout: Duration,
) -> io::Result<Option<bool>> {
keyboard_probe: StartupKeyboardEnhancementProbe,
) -> io::Result<StartupProbe> {
let deadline = Instant::now() + timeout;
let mut buffer = Vec::new();
let mut saw_supported = false;
let mut saw_unsupported_fallback = false;
let mut probe = StartupProbe {
cursor_position: None,
default_colors: None,
keyboard_enhancement_supported: None,
};
let mut saw_supported_keyboard = false;
loop {
tty.read_available(&mut buffer)?;
match parse_keyboard_enhancement_support(&buffer) {
KeyboardProbeState::SupportedAndFallback => return Ok(Some(true)),
KeyboardProbeState::Supported => saw_supported = true,
KeyboardProbeState::UnsupportedFallback => saw_unsupported_fallback = true,
KeyboardProbeState::Pending => {}
}
if saw_supported && saw_unsupported_fallback {
return Ok(Some(true));
update_startup_probe(
&mut probe,
&mut saw_supported_keyboard,
&buffer,
keyboard_probe,
);
if startup_probe_complete(&probe, keyboard_probe) {
return Ok(probe);
}
let now = Instant::now();
if now >= deadline {
if saw_supported {
return Ok(Some(true));
}
return Ok(saw_unsupported_fallback.then_some(false));
finish_startup_probe(&mut probe, keyboard_probe, saw_supported_keyboard);
return Ok(probe);
}
if !tty.poll_readable(deadline.saturating_duration_since(now))? {
if saw_supported {
return Ok(Some(true));
}
return Ok(saw_unsupported_fallback.then_some(false));
finish_startup_probe(&mut probe, keyboard_probe, saw_supported_keyboard);
return Ok(probe);
}
}
}
fn update_startup_probe(
probe: &mut StartupProbe,
saw_supported_keyboard: &mut bool,
buffer: &[u8],
keyboard_probe: StartupKeyboardEnhancementProbe,
) {
if probe.cursor_position.is_none() {
probe.cursor_position = parse_cursor_position(buffer);
}
if probe.default_colors.is_none() {
probe.default_colors = parse_default_colors(buffer);
}
if keyboard_probe == StartupKeyboardEnhancementProbe::Skip
|| probe.keyboard_enhancement_supported.is_some()
{
return;
}
match parse_keyboard_enhancement_support(buffer) {
KeyboardProbeState::SupportedAndFallback => {
probe.keyboard_enhancement_supported = Some(true);
}
KeyboardProbeState::Supported => {
*saw_supported_keyboard = true;
}
KeyboardProbeState::UnsupportedFallback => {
probe.keyboard_enhancement_supported = Some(false);
}
KeyboardProbeState::Pending => {}
}
}
fn startup_probe_complete(
probe: &StartupProbe,
keyboard_probe: StartupKeyboardEnhancementProbe,
) -> bool {
probe.cursor_position.is_some()
&& probe.default_colors.is_some()
&& (keyboard_probe == StartupKeyboardEnhancementProbe::Skip
|| probe.keyboard_enhancement_supported.is_some())
}
fn finish_startup_probe(
probe: &mut StartupProbe,
keyboard_probe: StartupKeyboardEnhancementProbe,
saw_supported_keyboard: bool,
) {
if keyboard_probe == StartupKeyboardEnhancementProbe::Query
&& probe.keyboard_enhancement_supported.is_none()
{
probe.keyboard_enhancement_supported = saw_supported_keyboard.then_some(true);
}
}
fn parse_cursor_position(buffer: &[u8]) -> Option<Position> {
for start in find_all_subslices(buffer, b"\x1B[") {
let rest = &buffer[start + 2..];
@@ -382,11 +445,12 @@ mod imp {
/// Parser state for the keyboard enhancement probe.
///
/// `UnsupportedFallback` records that a primary-device-attributes response arrived, but the
/// caller should keep waiting until the deadline because a later keyboard-flags response is
/// more specific. `Supported` records that keyboard flags arrived, but the caller should still
/// drain the PDA fallback response if it arrives before the deadline so those bytes do not leak
/// into the normal event stream.
/// `UnsupportedFallback` records that a primary-device-attributes response arrived without
/// keyboard flags. Startup treats that as unsupported immediately, matching crossterm's
/// previous behavior and avoiding a fixed delay in terminals without the keyboard protocol.
/// `Supported` records that keyboard flags arrived, but the caller should still drain the PDA
/// fallback response if it arrives before the deadline so those bytes do not leak into the
/// normal event stream.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum KeyboardProbeState {
Pending,
@@ -556,6 +620,38 @@ mod imp {
KeyboardProbeState::Pending
);
}
#[test]
fn startup_probe_parses_batched_terminal_responses() {
let mut probe = StartupProbe {
cursor_position: None,
default_colors: None,
keyboard_enhancement_supported: None,
};
let mut saw_supported_keyboard = false;
update_startup_probe(
&mut probe,
&mut saw_supported_keyboard,
b"\x1B[20;10R\x1B]11;rgb:1111/1111/1111\x07\x1B[?64;1;2c\x1B]10;rgb:eeee/eeee/eeee\x1B\\\x1B[?7u",
StartupKeyboardEnhancementProbe::Query,
);
assert_eq!(
probe,
StartupProbe {
cursor_position: Some(Position { x: 9, y: 19 }),
default_colors: Some(DefaultColors {
fg: (238, 238, 238),
bg: (17, 17, 17),
}),
keyboard_enhancement_supported: Some(true),
}
);
assert!(startup_probe_complete(
&probe,
StartupKeyboardEnhancementProbe::Query
));
}
}
}
+61 -23
View File
@@ -63,6 +63,11 @@ pub(crate) const TARGET_FRAME_INTERVAL: Duration = frame_rate_limiter::MIN_FRAME
/// A type alias for the terminal type used in this application
pub type Terminal = CustomTerminal<CrosstermBackend<Stdout>>;
pub(crate) struct InitializedTerminal {
pub(crate) terminal: Terminal,
pub(crate) enhanced_keys_supported: bool,
}
pub(crate) fn running_in_vscode_terminal() -> bool {
keyboard_modes::running_in_vscode_terminal()
}
@@ -334,7 +339,7 @@ fn flush_terminal_input_buffer() {
pub(crate) fn flush_terminal_input_buffer() {}
/// Initialize the terminal (inline viewport; history stays in normal scrollback)
pub fn init() -> Result<Terminal> {
pub(crate) fn init() -> Result<InitializedTerminal> {
if !stdin().is_terminal() {
return Err(std::io::Error::other("stdin is not a terminal"));
}
@@ -351,20 +356,57 @@ pub fn init() -> Result<Terminal> {
let backend = CrosstermBackend::new(stdout());
#[cfg(unix)]
let cursor_pos =
match crate::terminal_probe::cursor_position(crate::terminal_probe::DEFAULT_TIMEOUT) {
Ok(Some(pos)) => pos,
Ok(None) => {
tracing::warn!("initial cursor position probe timed out; defaulting to origin");
Position { x: 0, y: 0 }
let startup_probe = {
use crate::terminal_probe::StartupKeyboardEnhancementProbe;
let started_at = std::time::Instant::now();
let keyboard_probe = if keyboard_modes::keyboard_enhancement_disabled() {
StartupKeyboardEnhancementProbe::Skip
} else {
StartupKeyboardEnhancementProbe::Query
};
match crate::terminal_probe::startup(crate::terminal_probe::DEFAULT_TIMEOUT, keyboard_probe)
{
Ok(probe) => {
tracing::info!(
duration_ms = %started_at.elapsed().as_millis(),
cursor_position = probe.cursor_position.is_some(),
default_colors = probe.default_colors.is_some(),
keyboard_enhancement_supported = ?probe.keyboard_enhancement_supported,
"terminal startup probes completed"
);
probe
}
Err(err) => {
tracing::warn!(
"failed to read initial cursor position; defaulting to origin: {err}"
duration_ms = %started_at.elapsed().as_millis(),
"terminal startup probes failed: {err}"
);
Position { x: 0, y: 0 }
crate::terminal_probe::StartupProbe {
cursor_position: None,
default_colors: None,
keyboard_enhancement_supported: None,
}
}
};
}
};
#[cfg(unix)]
crate::terminal_palette::set_default_colors_from_startup_probe(startup_probe.default_colors);
#[cfg(unix)]
let cursor_pos = match startup_probe.cursor_position {
Some(pos) => pos,
None => {
tracing::warn!("initial cursor position probe timed out; defaulting to origin");
Position { x: 0, y: 0 }
}
};
#[cfg(unix)]
let enhanced_keys_supported = startup_probe
.keyboard_enhancement_supported
.unwrap_or(/*default*/ false);
#[cfg(not(unix))]
let mut backend = CrosstermBackend::new(stdout());
@@ -372,8 +414,15 @@ pub fn init() -> Result<Terminal> {
#[cfg(not(unix))]
let cursor_pos = cursor_position_with_crossterm(&mut backend);
#[cfg(not(unix))]
let enhanced_keys_supported =
!keyboard_modes::keyboard_enhancement_disabled() && detect_keyboard_enhancement_supported();
let tui = CustomTerminal::with_options_and_cursor_position(backend, cursor_pos)?;
Ok(tui)
Ok(InitializedTerminal {
terminal: tui,
enhanced_keys_supported,
})
}
#[cfg(not(unix))]
@@ -384,13 +433,6 @@ fn cursor_position_with_crossterm(backend: &mut CrosstermBackend<Stdout>) -> Pos
})
}
#[cfg(unix)]
fn detect_keyboard_enhancement_supported() -> bool {
crate::terminal_probe::keyboard_enhancement_supported(crate::terminal_probe::DEFAULT_TIMEOUT)
.unwrap_or(/*default*/ None)
.unwrap_or(/*default*/ false)
}
#[cfg(not(unix))]
fn detect_keyboard_enhancement_supported() -> bool {
// Non-Unix startup keeps the existing crossterm path because the bounded probe implementation
@@ -461,14 +503,10 @@ where
}
impl Tui {
pub fn new(terminal: Terminal) -> Self {
pub fn new(terminal: Terminal, enhanced_keys_supported: bool) -> Self {
let (draw_tx, _) = broadcast::channel(1);
let frame_requester = FrameRequester::new(draw_tx.clone());
// Detect keyboard enhancement support before any EventStream is created so the
// crossterm poller can acquire its lock without contention.
let enhanced_keys_supported = !keyboard_modes::keyboard_enhancement_disabled()
&& detect_keyboard_enhancement_supported();
// Cache this to avoid contention with the event reader.
supports_color::on_cached(supports_color::Stream::Stdout);
let _ = crate::terminal_palette::default_colors();