fix(client): multi-primitive yield for write pacing (rAF + setTimeout + Worker)#85
Open
aakhter wants to merge 1 commit into
Open
fix(client): multi-primitive yield for write pacing (rAF + setTimeout + Worker)#85aakhter wants to merge 1 commit into
aakhter wants to merge 1 commit into
Conversation
When the data-pacing path (chunkedTerminalWrite + deferred path of flushPendingWrites) schedules its next chunk via requestAnimationFrame alone, terminal output stalls indefinitely if rAF is starved. Three real-world scenarios reproduce this in Chromium: 1. Window is occluded (fully covered by another window, or on a monitor that has gone to sleep). rAF drops to ~0Hz. 2. Tab is idle-throttled (no user interaction for ~5 min). Chromium intensive-throttling clamps setTimeout to 1Hz too. 3. Tab is in a background window. Both rAF and setTimeout slow to a crawl. Replace the rAF-only scheduling with a _safeYield helper that races three primitives in parallel: - requestAnimationFrame (primary, fires at compositor rate). - setTimeout(50) (fallback for visible-but-occluded windows). - Worker postMessage tick (fallback for idle-throttled and background tabs; Workers are not subject to main-thread throttling — this is the React Scheduler trick). The first one to fire wins via a `done` guard; the others become no-ops. The Worker is built lazily on first call (4 lines of inline JS via Blob URL); if Worker construction throws we silently fall back to the other two primitives. Replaces 6 requestAnimationFrame callsites that participate in data pacing: - 3 flushPendingWrites scheduling sites (live + deferred paths). - 3 chunkedTerminalWrite sites (initial chunk, next-chunk loop, final finish-callback). True animation use cases (scroll loop in scrollToBottom, fit-addon reflow) stay on plain requestAnimationFrame — they are correctly throttled when the user is not looking, by design. File: src/web/public/terminal-ui.js (+70/-8). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.
Summary
When the data-pacing path (
chunkedTerminalWrite+ the deferred path offlushPendingWrites) schedules its next chunk viarequestAnimationFramealone, terminal output stalls indefinitely if rAF is starved. Three real-world Chromium scenarios reproduce this:setTimeoutto 1 Hz too — so the existing rAF-then-fall-back-to-setTimeout pattern doesn't help.setTimeoutslow to a crawl.Symptom in all three: a long-running command's output appears to "freeze" mid-write. When the user refocuses the tab the queue drains in a burst.
Fix
Replace the rAF-only scheduling with a
_safeYield(cb)helper that races three primitives in parallel:requestAnimationFrame— primary, fires at compositor rate.setTimeout(50)— fallback for visible-but-occluded windows.postMessagetick — fallback for idle-throttled and background tabs. Workers are not subject to main-thread throttling. (This is the React Scheduler trick.)The first one to fire wins via a
doneguard; the others become no-ops.The Worker is built lazily on first call (4 lines of inline JS via Blob URL). If
Workerconstruction throws we silently fall back to the other two primitives.Scope
Replaces 6
requestAnimationFramecallsites that participate in data pacing:flushPendingWritesscheduling — live path (3 sites across the live-write and deferred-write helpers).chunkedTerminalWrite— initial chunk, next-chunk loop, and the final finish-callback.True animation use cases (the scroll loop in
scrollToBottom, the fit-addon reflow) stay on plainrequestAnimationFrame— they are correctly throttled when the user isn't looking, by design.Test plan
setTimeout(50)wins).doneguard prevents redundant work).Workersupport (or where construction throws),_workerYieldis silently disabled and the rest still works.Files
src/web/public/terminal-ui.js(+70/-8)🤖 Generated with Claude Code