perf: move terminal scrollback serialize off the workspace-switch path#152
Merged
Conversation
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
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.
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):notifyOutputis O(1) per chunk — a few field writes + one guarded timer arm, so the per-keystroke echo path pays nothing measurable) and fromterm.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).requestIdleCallbackslot (setTimeout fallback for engines without rIC; a capped defer avoids serializing mid-contention when the rIC only fired via its timeout).session_restore.enabled; a resize or ascrollback_linessettings change invalidates the cache.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 ofscrollback.rs(session-keyed in-memory cache vs(workspace,pane)-keyed disk store),[mock::scrollback]observability on every handler, tracked PTY channels, andwindow.__codemuxTerminalMock.flood()/emitSerializeBuffers()(bound toCtrl+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 verifygreen (cargo check + cargo test + tsc + 2371 vitest, incl. 13 new idle-serializer unit tests).codemux browser): after a 150k-line flood settles, the idle serialize runs (trigger=idle); switching away logstrigger=unmount reused=truewith the flood's finalMOCK-FLOOD-ENDmarker in the cached payload tail (zero serialize cost on the switch, payload provably up-to-date); switching mid-flood logsreused=falsewith a complete synchronously-serialized payload (dirty fallback intact); the cache→flush→disk→restore round-trip visibly restores the flood tail +── session restored ──.codemux-devtauri:devinstance driven over its control socket):seq 1 200000in 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.