perf: end-to-end terminal output compression (deflate-raw-v1) - #301
Conversation
Dedups the lockfile (axum 0.8 already pulled 0.28; cli/machine legs were on 0.26.2). Prerequisite for the deflate-raw-v1 compression work; no source changes were needed.
Terminal output now travels compressed from machine to browser: raw DEFLATE, one long-lived compressor per attach (context takeover), sync flush per WS message. Measured 20-30x on realistic ANSI streams at ~365 MB/s/core (research report in docs/plans). Real permessage-deflate is unavailable in our stack — tungstenite has no codec and axum exposes no negotiation — so this is in-protocol, which also lets the hub relay compressed bytes zero-copy without per-connection deflate state. Negotiation is version-skew safe in both directions (desktop bundles its own frontend): the machine advertises the capability in Register, the browser opts in via a `compress` query param (escape hatch: localStorage webmux:compress=off), and the hub acks with a CompressionEnabled text frame strictly before any binary frame, only when both sides opted in. All new protocol fields are serde(default) with old-peer compat tests. Input, CLI, and preview attaches stay uncompressed — keystroke latency direction is never touched. Browser side inflates with fflate's synchronous streaming Inflate feeding the existing output path (immediate-first-write preserved); inflate errors fail closed (frames swallowed, socket closed, reconnect renegotiates). Verification: cargo workspace tests (codec round-trip incl. arbitrary split/merge, context takeover, serde compat), vitest, full e2e suite 83/83 with compression on by default, plus a dedicated terminal-compression spec. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R2ewppMpeyasebTgcwDCNL
…watermark Under a starved main thread (slow phones, 2-core CI) CompositionHelper's setTimeout(0) finalize timers queue behind input tasks. A stale timer then computes its send region from LIVE state — the newest composition's position, the live textarea value, a _dataAlreadySent token the next compositionstart destroys — while cancellation is one shared boolean every compositionend re-arms. Result: the same committed CJK text reaches onData twice (upstream xterm.js#5023, unfixed in 6.1.0-beta.303; event traces in docs/plans/2026-08-30-ime-dup-diagnosis-REPORT.md pin the arithmetic, and the sequence is bit-identical to real mobile IME events). Instance-patch the helper next to patchScaledMouseCoordinates: track how much of the textarea prefix has already been emitted and clamp every deferred send region to start at or after it. Purely positional — content is never compared, legitimate repeated text still flows. No-ops gracefully if xterm's internals change. Repro at 10x CPU throttle went 14/20 failing to 0/20; the IME e2e spec passes 50/50 un-throttled; full suite 83/83 with compression on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R2ewppMpeyasebTgcwDCNL
|
Added Fix: positional emitted-prefix watermark clamped onto the helper's send paths (instance patch beside |
What
Terminal output now travels compressed machine→browser: raw DEFLATE per attach stream (context takeover, sync flush per WS message). Measured 20-30x on realistic ANSI output at ~365 MB/s/core — the dominant win for remote/mobile links where output bursts (builds, tmux redraws) saturate the pipe.
Why in-protocol instead of permessage-deflate: tungstenite has never shipped a deflate codec (issue #2 open since 2017, PR #426 unmerged) and axum exposes no negotiation — full evidence and measurements in
docs/plans/2026-08-30-permessage-deflate-REPORT.md. In-protocol also lets the hub relay compressed bytes zero-copy with no per-connection deflate state.deflate-raw-v1in Register → browser opts in via query param (escape hatchlocalStorage webmux:compress=off) → hub acks with acompression_enabledtext frame strictly before any binary frame, only when both ends opted in. All protocol fieldsserde(default)+ old-peer compat tests.Inflatefeeding the existing output path (immediate-first-write preserved); inflate errors fail closed (frames swallowed, socket closes, reconnect renegotiates fresh).Verification
attachCompression.test.ts(ack gating, unknown-algo refusal, fail-closed); typecheck; build.terminal-compression.spec.ts(ack +seq 1 2000stream integrity). One mobile-IME flake A/B-tested at 30 runs each with compression on/off: ~8-9% both ways → pre-existing perf: end-to-end input latency, scroll feel, and mobile IME fixes #295 spec race, compression-independent (details indocs/plans/2026-08-30-ws-compression-e2e-REPORT.md).🤖 Generated with Claude Code
https://claude.ai/code/session_01R2ewppMpeyasebTgcwDCNL