perf: skip plain text when scanning for OSC, and measure the read path - #25
Merged
Conversation
There was no way to measure the other half of the terminal: how fast bytes coming out of the pty are consumed. Added `--throughput`, which feeds nine synthetic workloads modelled on alacritty's vtebench through the real read path (OscScanner, then the VT parser, then the grid) without opening a window, and times the scan and the parse separately. vtebench itself has to run inside the terminal under test, which this machine cannot drive, so the payloads are reproduced instead: results are not comparable with other terminals, but they are comparable across commits. What it found: - The read path is not a bottleneck: 100-270 MiB/s, two orders of magnitude above what a flooding build produces. - OSC scanning walked every byte one at a time. Most output is plain text, so jump to the next ESC with memchr: 8.7ms -> 0.35ms per 16 MiB of plain text (25x), and light_cells overall 193ms -> 153-170ms. - Scrolling inside a DECSTBM region runs at 25 MiB/s against 89 MiB/s without a region. That is inside alacritty_terminal, so it is recorded rather than fixed. `--bench` also gained an estimate for the "one TextArea per row instead of per cell" idea that ghostty's performance discussion suggests. It loses: fewer areas cut prepare from 0.55ms to 0.22ms, but reshaping a row every frame costs 0.84ms against 0.02ms for cached single glyphs. Measured and rejected, written down so it is not tried again. docs/references/performance-techniques.md is the new catalogue: every technique found in other terminals with a verdict of adopted, rejected with the measurement, or still open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvMacYD6T9EoWqc7C247Hr
tkc
force-pushed
the
measure-read-throughput
branch
from
September 10, 2026 01:45
c0699be to
abafd78
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #24, from reading what other terminals do about performance.
New measurement:
termit --throughputEverything measured so far was the drawing half. This measures the other half — how fast bytes from the pty are consumed — through the real read path (
OscScannerscan →vte→ grid), without opening a window, timing the scan and the parse separately.alacritty's vtebench has to run inside the terminal under test and this machine cannot drive termit's window, so its workloads are reproduced instead (
light_cells,medium_cells,dense_cells,scrolling,scrolling_region,cursor_motion,unicode,sync_cells, plus anosc_heavyof our own). Results are not comparable with other terminals; they are comparable across commits.163x47, 10,000 lines of scrollback, 16 MiB per workload:
What that measures is how fast the parser and the grid can accept bytes: 100–270 MiB/s. How fast the pty actually delivers them was not measured, so this does not say where the slowness a user feels comes from. It does say there is no headroom left to win in the read path itself, apart from the scan below.
The one win it did find: memchr
OSC 7 / OSC 133 are picked up by scanning the same bytes the VT parser gets, one byte at a time. Almost all of it is plain text, so jump to the next
ESC:light_cellsend to end: 193ms → 153–170ms per 16 MiB. Two tests added for the skip: offsets stay right after a long plain run, and an OSC directly after a non-OSC escape is still picked up.Measured and rejected: one TextArea per row
ghostty's performance discussion optimises for ~64 distinct styles; termit hands glyphon one area per cell (7,661 full screen), so batching by row looked promising.
--benchnow estimates it:Fewer areas do cut prepare (~43ns of fixed cost per area), but a row's contents change every frame, so the row has to be reshaped every frame. Per-cell shaping happens once, ever. 1.9x worse — not adopted, and written down so it is not tried again.
Also recorded, not fixable here
Scrolling inside a
DECSTBMregion runs at 25 MiB/s against 89 MiB/s without a region (3.5x), because it becomes a line rotation instead of a push into history. tmux, vim and agent UIs all use regions. That code is insidealacritty_terminal.Docs
docs/references/performance-techniques.md— new: every technique found in other terminals (alacritty damage tracking, foot's memmove scroll, ghostty's vsync-off default and renderer thread, kitty's repaint delay, wgpu frame latency), each marked adopted / rejected-with-measurement / still open.docs/performance.md7.12 (row batching), 7.13 (read path), and 2.3 for the new flag.docs/BUILD.md—--throughput.Test
cargo test— 202 pass.cargo clippy --all-targets -- -D warningsandcargo fmt --check— clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01GvMacYD6T9EoWqc7C247Hr