Skip to content

perf: hot-path throughput, locking, and allocation optimizations#5

Merged
simota merged 8 commits into
mainfrom
perf/hot-path-optimizations
Jul 13, 2026
Merged

perf: hot-path throughput, locking, and allocation optimizations#5
simota merged 8 commits into
mainfrom
perf/hot-path-optimizations

Conversation

@simota

@simota simota commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

Measurement-driven optimization pass over the pty → vt → grid → render hot path, plus a synchronized-output fix the locking work exposed. Every shipped change was gated on interleaved same-path A/B benchmarks (≥2% end-to-end on bench_throughput) and independently re-verified; rejected attempts and their numbers are recorded in .agents/nexus.md.

  • perf(grid): scalar print overwrites reuse combining-String buffers (+45–69% on a combining-heavy overwrite microbench)
  • perf(render): dirty-row rebuilds reuse row instance buffer capacity
  • perf(app): cursor-blink gate reads a cached state instead of locking the terminal on every wake
  • perf(app): io thread feeds the terminal per 64KiB chunk with unlock_fair (worst-case lock hold was a full 1MiB batch)
  • perf(grid): terminal modes stored in a linear-scan Vec (+6–12% e2e — per-print_str/C0 SipHash lookups eliminated)
  • perf(vt): SWAR word scan + ASCII fast path for printable runs (+7.7% e2e; the unsafe is validated by a 2.5M-case adversarial fuzz against a naive oracle)
  • perf(grid): erase family blanks cells in place via Cell::set_from (+3.5–4% e2e)
  • fix(app): externally-triggered redraws no longer tear a pane mid-update under synchronized output (DECSET 2026)

bench_throughput: ~169 → ~198–215 MiB/s across the campaign (absolute numbers vary with machine load; every step verified by interleaved A/B).

Known trade-offs (documented in code and commit bodies)

  • Mode lookups degrade to O(n) if a stream sets thousands of distinct unknown private modes (bounded by the u16 mode space).
  • Synchronized output: the first externally-triggered redraw of a sync block can still tear once; holding a snapshot continuously would charge every pane a permanent full-grid copy.

Test plan

  • cargo test --workspace on real hardware (openpty, loopback sockets, Metal GPU pipeline tests): green. Two noa-pty tests are flaky under full-workspace parallel load but pass in isolation; noa-pty is untouched by this diff (depends only on noa-core + portable-pty).
  • ~30 new tests: differential/mutation-verified erase and print-overwrite tests, SWAR oracle fuzz, chunked-locking equivalence (incl. OSC/DCS split across chunk boundaries), sync-decision quadrant tests.
  • Manual (pending): visual tear check in neovim/btop (focus switch during sync), resize during sync, multi-pane independence.

simota added 8 commits July 12, 2026 22:56
Route Screen::print's narrow/wide/spacer writes and
promote_cluster_to_wide's spacer write through Cell::set_from instead of
struct-literal assignment, so a repeatedly overwritten cell reuses its
combining String allocation. Interleaved same-harness A/B on a
combining-heavy overwrite workload: +45% (author) / +69% (independent
rerun) median; end-to-end bench_throughput unchanged (<1%).
rebuild_row_instances now fills caller-owned in/out Vecs
(RowInstanceBuffers) instead of returning fresh ones, so dirty-row
rebuilds in rebuild_pane_cached reuse the PaneRenderCache row slots'
capacity from the previous frame — same recycle pattern as
FrameSnapshot::from_terminal_recycled.
focused_cursor_wants_blink ran on every blink-interval wake and took the
focused pane's terminal lock just to read cursor visibility/style/
viewport. Capture that state in redraw() under the lock it already
holds per visible pane (same no-lock-on-poll pattern as
kitty_animation_flag) and let the timer gate read the cache; at worst
one redraw stale, and occlusion/focus gates veto independently.
feed_terminal_batch held one Terminal lock across an entire drained
batch (up to 1MiB, a few ms of parsing), starving the main thread's
snapshot lock during bulk output. Feed each reader chunk (<=64KiB)
under its own lock and release it with MutexGuard::unlock_fair so a
waiting main thread actually gets the handoff — a plain drop lets the
io thread barge straight back in, and parking_lot's fair unlock costs
the same as a normal unlock when nobody is waiting. Batch-tail work
(overview/sidebar/ipc publish, pending-writes drain) still runs once
per batch. Throughput unchanged (interleaved A/B within noise:
author -0.5%, verifier +1.3%).
Re-profiling bench_throughput showed mode lookups as the largest
untouched cost: ModeState backed its (value, ansi) pairs with a
HashSet, and Terminal::print_str queries autowrap/grapheme_clustering
once per bulk-print run while execute_c0 queries linefeed_newline on
every CR/LF — millions of SipHash keyings to look up a set that holds
about a dozen entries in a real session (~8% of bench self-time).
Compare the pairs directly in a Vec instead: insert stays
dedup-guarded, reset uses retain, and the mouse-format exclusivity
retain matches the old per-mode removes. Interleaved same-path A/B
medians: +6.0% (author) / +11.9% (independent verifier) end-to-end;
both runs clear the gate, absolute baseline varies with machine load.

Known trade-off (tracked): a stream that sets thousands of distinct
unknown private modes degrades lookups to O(n) where the HashSet
stayed O(1); bounded by the u16 mode space, and a candidate follow-up
is dedicated flags for known modes with a side table for the rest.
Ground-state printable runs were scanned twice: a per-byte closure
walk to find the control/DEL boundary, then core::str::from_utf8 over
the same range. Re-profiling put that loop at 22% of bench_throughput
self-time. Replace it with scan_run: an 8-byte u64 SWAR scan
(underflow trick for <0x20, xor trick for 0x7f) that finds the
boundary and tracks whether the run is pure ASCII in one pass; an
all-ASCII run then skips validation via from_utf8_unchecked (SAFETY:
scan_run only reports ascii for runs it proved all <0x80). Non-ASCII
runs keep the existing from_utf8 + valid_up_to recovery path
unchanged.

Verified +7.7% end-to-end (177.5 -> 191.2 MiB/s best-median; hub
arbitration and independent verifier agree, author's initial +15%
reading did not reproduce). Soundness of the unsafe: exhaustive
256-value-at-every-position sweep, adjacent-pair cross-lane sweep
(256x256x7), and a 2M-case seeded fuzz against a naive oracle — zero
mismatches — plus chunk-boundary sweeps for split UTF-8, overlong
sequences, and lone C1 bytes.
The erase family (erase_line, erase_display, erase_chars,
insert_blank_chars, delete_chars) blanked cells with
*c = blank.clone(), dropping and reallocating each destination cell's
combining String where Row::clear and the print paths already reuse it
through Cell::set_from. Route all five through set_from — same seven
fields written, blank's combining is always empty, so output bytes are
identical; only the allocation behavior changes. Interleaved same-path
A/B on bench_throughput (its TUI-style lines hit EL on ~20% of rows):
+4.0% (author, 207.1 -> 215.4 MiB/s) / +3.5% (independent verifier,
191.5 -> 198.2) — relative gains agree, absolute baselines vary with
machine load.
Externally-triggered redraws (focus change, cursor blink, a sibling
pane's redraw in the same window) never checked DECSET 2026, so they
could snapshot a terminal mid-update and present a torn frame — and
the io thread's chunked locking (33621c3) made that window easy to
hit. Each pane now holds the last snapshot captured while synchronized
output is active and redraws from it instead of re-reading the
terminal, gated by sync_output_snapshot_decision: a resize always
forces a fresh read (an old-sized snapshot is worse than a tear), the
hold expires after the same SYNCHRONIZED_OUTPUT_MAX_SUPPRESSION cap
the io thread applies to redraw pacing (a runaway mode 2026 cannot
freeze a pane), and the held snapshot is released as soon as sync ends
so panes that used mode 2026 once do not retain a full-grid snapshot
for life. Dirty bits are not consumed while reusing, so the first
redraw after sync releases renders the complete final state.

Known residual (documented at the decision fn): the first
externally-triggered redraw of a sync block has no held snapshot yet
and can still tear once; holding one continuously would charge every
pane that never uses mode 2026 a permanent full-grid snapshot, so the
fix narrows per-block tearing to at most one at its start instead.
@simota simota merged commit c67e5dd into main Jul 13, 2026
1 check passed
@simota simota deleted the perf/hot-path-optimizations branch July 13, 2026 01:58
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