Skip to content

Reduce text input latency and CPU usage - #768

Open
choongng wants to merge 3 commits into
huggingface:mainfrom
choongng:upstream/tui-stream-cpu
Open

choongng wants to merge 3 commits into
huggingface:mainfrom
choongng:upstream/tui-stream-cpu

Conversation

@choongng

@choongng choongng commented Oct 13, 2026 •

Copy link
Copy Markdown

I noticed that tau's TUI text input had erratic latency, and that while a session has activity CPU usage would often climb to roughly 100% while stalling input entirely for 100s of ms. The behavior was most visible during long outputs or with thinking shown. The core cause was a combination of 1) updating the UI causes Textual to re-compute layout, which is expensive, and 2) updates were happening sometimes in excess of 100Hz. GC pauses also contributed. This change:

  1. coalesces updates to 10Hz with a TranscriptMarkdownStream designed for the transcript's requirements
  2. uses gc.freeze() to reduce the scope of the GC's work while results are streaming in
  3. reduces the number of active elements in the transcript display

In combination this reduces the stalls to <15ms P99, which in my subjective opinion feels much better, and reduces max CPU usage to about 25% of one Zen 5 core. There seems to be a pre-existing race condition in the autocomplete, this change does not fix that but it makes it much less visible. No visual change, all tests passing, tested on Python 3.13.5.

choong added 3 commits October 13, 2026 08:33
Every Markdown append re-lays out every mounted transcript row, and
Textual's MarkdownStream appends once per provider fragment (one per
token, 50-150/s). With a long history that kept most of a core busy
while text streamed: 64-78% CPU at 400 items in a headless probe.

TranscriptMarkdownStream renders the first fragment at once and then
takes whatever piled up every 100ms. The same probe drops to ~38%.
stop() waits for an append already in flight before its final flush,
since Markdown.append reads its parse position before taking its lock.
The long UI pauses while text streams (130-430ms, about one a second
at 400 transcript items) were Python's automatic gen-2 collection
walking a few hundred thousand objects, not rendering.

Collect once after startup and after every settled turn, then freeze
the survivors so the automatic collector only scans what the next turn
creates. Each pass unfreezes first, so nothing frozen leaks past a
turn. With coalesced appends, the headless probe's longest stall drops
from 200-315ms to ~19-34ms.

Tests stub the collection out: it acts on the whole pytest process.
Every streamed append re-lays out each mounted row, so the window
size sets the cost of one streaming update. With coalescing and
idle-time GC in place, the headless probe at 400 items (6s runs)
measured:

  window 200: ~31% CPU, p99 stall 18-24ms, 3-5 frames >16ms/s
  window  80: ~24% CPU, p99 stall 12-14ms, <1 frame  >16ms/s

The longest stalls (30-50ms) are now gen-2 GC passes over objects
created during the turn, about the same either way. Paging scales in
proportion (32 page, 16 overscan); it only starts at the scroll edge,
so 80 items still leaves several screens of scrollback.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant