Skip to content

perf: move terminal scrollback serialize off the workspace-switch path#152

Merged
Zeus-Deus merged 2 commits into
mainfrom
perf/idle-scrollback-serialize
Jul 10, 2026
Merged

perf: move terminal scrollback serialize off the workspace-switch path#152
Zeus-Deus merged 2 commits into
mainfrom
perf/idle-scrollback-serialize

Conversation

@Zeus-Deus

Copy link
Copy Markdown
Owner

Closes #128

Problem

Switching away from a workspace ran serializeAddon.serialize() synchronously on the main thread for every unmounting terminal pane (TerminalPane.tsx, unmount cleanup → buildScrollbackPayload) — the known >30ms hotspot the code itself warns about. Alt-screen panes were already skipped; the exposure was plain shells with deep scrollback, paid on every switch, per pane.

Fix

Idle pre-serialization with fresh-if-clean reuse (src/components/terminal/scrollback-idle-serializer.ts):

  • Dirty-tracking is fed from the write pump (notifyOutput is O(1) per chunk — a few field writes + one guarded timer arm, so the per-keystroke echo path pays nothing measurable) and from term.onWriteParsed (xterm parses writes asynchronously; the parse tail of a multi-MB reattach replay can outlive the settle window, so freshness is anchored to the parse boundary, not the write call).
  • Once output settles (1s quiet, ≥5s between serializes) the buffer is serialized in a requestIdleCallback slot (setTimeout fallback for engines without rIC; a capped defer avoids serializing mid-contention when the rIC only fired via its timeout).
  • Never on the alt screen (existing skip retained); gated on session_restore.enabled; a resize or a scrollback_lines settings change invalidates the cache.
  • Unmount and app-close reuse the cached serialization only when provably clean — payload metadata is always recomputed fresh — and fall back to today's synchronous serialize when dirty, so scrollback persistence semantics are unchanged in the fallback.
  • The >30ms warning is now tagged trigger=unmount|close|idle; DEV builds log every serialize/flush decision (reused=true|false).

Untouched, per the issue's constraints: the disabled terminal cache (terminal-cache.ts), the write pump / flow-control wiring, the alt-screen skip, and the O(1) session→workspace index.

Dev-mock e2e enablement (src/dev/tauri-mock.ts): faithful two-store twin of scrollback.rs (session-keyed in-memory cache vs (workspace,pane)-keyed disk store), [mock::scrollback] observability on every handler, tracked PTY channels, and window.__codemuxTerminalMock.flood() / emitSerializeBuffers() (bound to Ctrl+Alt+Shift+F / Ctrl+Alt+Shift+S) so the whole serialize/restore path is drivable and assertable in a plain browser.

Docs updated: docs/features/terminal.md, docs/features/session-persistence.md, docs/features/dev-mock-runtime.md.

Verification

  • npm run verify green (cargo check + cargo test + tsc + 2371 vitest, incl. 13 new idle-serializer unit tests).
  • Browser-mock e2e (real UI + real xterm + mock IPC, driven via codemux browser): after a 150k-line flood settles, the idle serialize runs (trigger=idle); switching away logs trigger=unmount reused=true with the flood's final MOCK-FLOOD-END marker in the cached payload tail (zero serialize cost on the switch, payload provably up-to-date); switching mid-flood logs reused=false with a complete synchronously-serialized payload (dirty fallback intact); the cache→flush→disk→restore round-trip visibly restores the flood tail + ── session restored ──.
  • Real-app e2e (isolated codemux-dev tauri:dev instance driven over its control socket): seq 1 200000 in a plain shell → workspace switch away/back restores the full tail; a graceful window close persists the complete scrollback to disk; relaunching the app restores it into a live shell.
  • Multi-agent adversarial review pass (8 finder angles + verify wave); all confirmed findings fixed and re-verified.

Switching away from a workspace ran serializeAddon.serialize()
synchronously on the main thread for every unmounting terminal pane —
the known >30ms hotspot for plain shells with deep scrollback (the
alt-screen skip already covered TUI panes).

A per-pane idle serializer (scrollback-idle-serializer.ts) now
pre-serializes the buffer while the pane is quiet: dirty-tracking is
fed from the write pump (O(1) per chunk — no added keystroke-echo
cost) plus term.onWriteParsed (xterm parses writes asynchronously, so
the parse tail of a large replay can outlive the settle window), and a
serialize runs in a requestIdleCallback slot (setTimeout fallback)
once output has settled for 1s, at most every 5s, never on the alt
screen, gated on session_restore.enabled, with a capped defer when the
rIC fires via timeout on a busy main thread.

Unmount and app-close reuse the cached serialization when it is still
clean (metadata is always recomputed fresh; a scrollback_lines settings
change invalidates the cache) and fall back to today's synchronous
serialize when dirty — persistence semantics are unchanged in the
fallback. The >30ms warning is now tagged trigger=unmount|close|idle;
DEV builds log every serialize/flush decision (reused=true|false).

The dev mock gains a faithful two-store twin of scrollback.rs
(session-keyed cache + (workspace,pane)-keyed disk store), per-handler
[mock::scrollback] observability, tracked PTY channels, and
flood()/emitSerializeBuffers() helpers on window.__codemuxTerminalMock
(bound to Ctrl+Alt+Shift+F/S) so the serialize/restore path is
drivable end-to-end in a plain browser.

Verified: npm run verify (2371 tests, incl. 13 idle-serializer unit
tests); browser-mock e2e (idle serialize after a 150k-line flood,
reused=true on switch with the flood marker in the cached tail, dirty
mid-flood switch falls back to a fresh serialize, cache→flush→disk→
restore round-trip); real tauri:dev e2e over the codemux-dev control
socket (seq 1 200000 → switch/back restore, scrollback persisted
across a graceful app restart).

Closes #128
@Zeus-Deus Zeus-Deus merged commit c39ab0d into main Jul 10, 2026
4 checks passed
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.

perf: move terminal scrollback serialize() off the workspace-switch path

1 participant