Status indicator redraws at 32 ms while only waiting on a background terminal

Open 💬 3 comments Opened Aug 9, 2026 by DioNanos

Summary

The TUI status indicator schedules a new animation frame every 32 ms whenever animations are enabled, including while the agent is only waiting on a background terminal and has nothing to animate.

Reported downstream by @rebroad with measurements and a tested patch: DioNanos/codex-termux#16. The analysis and the numbers below are his; I am filing here because the code is upstream's and the contributing policy asks for an issue rather than a pull request.

Measurement

On Android/Termux, roughly 30% CPU in the codex-main thread while waiting for a separate rustc process. The compiler was not a child of the TUI process — the CPU was the redraws themselves, at about 31 frames per second with nothing changing on screen.

Root cause

StatusIndicatorWidget::render in codex-rs/tui/src/status_indicator_widget.rs:

if self.animations_enabled {
    self.frame_requester
        .schedule_frame_in(Duration::from_millis(32));
}

The interval does not depend on what the indicator is showing. The Waiting for background terminal state, set by ChatWidget::on_terminal_interaction while polling background output, animates at the same rate as an active turn.

Outline of a fix

The original suggestion was a flat 200 ms. That also slows the indicator while the model is working, so a narrower version keys off the state:

let interval = if self.header == WAITING_ON_BACKGROUND_TERMINAL_HEADER {
    Duration::from_millis(200)
} else {
    Duration::from_millis(32)
};
self.frame_requester.schedule_frame_in(interval);

About 5 fps for a waiting indicator and an elapsed-time display, unchanged everywhere else. Making the header label a shared constant, used both where the state is set and where the rate is chosen, keeps the two from drifting.

Battery and thermal cost is most visible on phones, but the idle redraws are not platform-specific.

View original on GitHub ↗

3 Comments

rebroad · 18 days ago

200ms is probably good enough for every animation IMHO, assuming it's primarily there to reassure users that codex is running.

DioNanos · 18 days ago

Fair point. We kept the narrow version on purpose: it fixes the waiting state you measured and leaves everything else alone. If maintainers prefer flat 200 ms, it's a one-line change.

jdcodes1 · 9 days ago

Verified on main (1f41cc5d92): the flat 32 ms reschedule is still there, inside render() itself with no dependency on indicator state (https://github.com/openai/codex/blob/1f41cc5d92/codex-rs/tui/src/status_indicator_widget.rs#L243-L247) — so every render schedules the next render, ~31 fps for as long as the widget is visible, including pure waiting states. The downstream measurement generalizes: this is the whole-session cost floor whenever a turn is active, not just Termux.

One suggestion beyond the state-keyed interval: the principled version is deriving the rate from what's actually animating. The spinner glyph is the only thing that needs ~30 fps; the elapsed-time text changes once per second. So render could ask its activity_indicator/MotionMode for the next-change deadline (spinner period vs. next whole second) and schedule exactly that — waiting states with no spinner fall out at 1 fps automatically, no header-string comparison to drift out of sync (the proposed shared-constant guard is fixing a fragility the design doesn't need to have). Also worth noting animations_enabled = false already avoids this entirely, so battery-sensitive users have a config workaround today — but the default shouldn't burn 30% of a phone core to display a static "waiting" line.