Skip to content

perf(server): batch terminal output and bound replay buffer eviction - #1729

Merged
chuks-qua merged 2 commits into
mainfrom
perf/terminal-output-pipeline
Sep 21, 2026
Merged

chuks-qua merged 2 commits into
mainfrom
perf/terminal-output-pipeline

Conversation

@chuks-qua

@chuks-qua chuks-qua commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

What

The terminal output path paid a fixed serialization gauntlet per PTY chunk and had no flow control. This PR batches output at the host, adds real backpressure, and removes quadratic decay in the server-side replay buffer.

  • Coalescing: publishOutput now buffers per session and flushes on a 2ms window (or immediately at a full 64 KiB chunk), collapsing output bursts into far fewer IPC events, Zod parses, WS frames, and xterm.write calls. outputSeq stays contiguous; no contract change.
  • Backpressure: when the IPC queue (inbound pending + outbound in-flight) crosses 768 KiB the host pauses that session's PTY reads and resumes below 512 KiB. Previously a fast producer could push outboundBytes past the 1 MiB cap, making publish throw and failHost kill every session on every thread.
  • Exit ordering under pressure: an exited session flushes pending output before its exit event publishes (bounded by a 2s deadline that drops unsequenced bytes), so close/shutdown cannot wedge the serial inbound queue. Inbound commands are dropped for exited-but-mapped sessions instead of throwing into failHost.
  • Replay buffer: eviction advances a head index (O(1) amortized) instead of Array.shift (O(n) per append at capacity), and per-chunk cumulative endBytes makes bytesAfter O(1) instead of a full-array reduce on every append while a checkpoint exists.
  • Removed waste: the PTY host no longer Zod-parses its own outbound events (the supervisor validates every inbound event at the trust boundary), and handleOutput no longer copies each decoded buffer twice more for headless/attached delivery.

Verified: bun run --cwd apps/server test -- on the five touched suites, 64/64 pass including new tests for coalescing, immediate full-chunk flush, pressure pause/resume, hysteresis band, exit-after-flush ordering, pressured-exit deadline, and the 4 MiB kill path. tsc --noEmit clean, oxlint clean. Live proof: verify-mcode runtime health on a rebuilt bundle; pty-host-supervisor.real.test.ts and terminal-session-runtime.real.test.ts (real ConPTY + real shell detach/reattach) pass. Two independent reviewers audited the diff; two major findings (dead-scope command window, close-under-pressure deadlock) were fixed and rechecked.

Limits: known environment flakes unrelated to this diff (EBUSY temp-dir cleanup in composition/diagnostics tests, pty-host-runtime.real ConPTY timeout) reproduce identically on the pre-change baseline.

Why

Under heavy output (build logs, floods) each PTY chunk paid base64 encode, two Zod parses with canonical re-encode, two JSON stringifies, base64 decode, and multiple buffer copies, then one WS frame per chunk. A sustained flood could exceed the shared 1 MiB outbound cap and crash the entire PTY host, taking every terminal on every thread down. The replay buffer also degraded quadratically once full or while a checkpoint was retained.

UI Changes

None — server-side pipeline only.

Config Changes

None.

Review Notes

  • Flood-killed sessions report reason: "natural" because the protocol's exitReason enum has no fitting value; noted in a code comment. Dropped bytes were never assigned seqs, so contiguity holds.
  • queueBytes intentionally sums inbound + outbound queue pressure; the 2s exit deadline bounds the worst-case close delay to a delay, not a deadlock.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Coalesce PTY output bursts into <=64 KiB host events on a 2ms window so
each onData callback no longer pays a full IPC, Zod, and WebSocket frame
per chunk. Apply PTY-level backpressure (pause/resume) when the IPC queue
crosses 768/512 KiB watermarks instead of letting a flood trip the 1 MiB
outbound cap and kill the host; a per-session 4 MiB pending bound kills a
session that outruns the pipe, and a 2s deadline keeps an exited
session's exit event publishable so closes cannot wedge the serial queue.

In the server runtime, evict replay chunks by advancing a head index
with cumulative byte offsets instead of Array.shift plus a full-array
scan on every append while a checkpoint exists, and drop two redundant
Uint8Array copies per output event. The PTY host also stops Zod-parsing
its own outbound events; the supervisor already validates every inbound
event at the trust boundary.
A 4 MiB bound is reachable within the pause-latency window when the
server stalls during heavy output, which would kill a developer's
terminal for a transient stall. The pause/resume backpressure is the
primary defense; the bound now only trips when pause fails to stem a
sustained flood, at which point the session was already undeliverable.
@chuks-qua
chuks-qua merged commit 8384f30 into main Sep 21, 2026
9 checks passed
@chuks-qua
chuks-qua deleted the perf/terminal-output-pipeline branch September 21, 2026 18:44
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