Schedule zero-delay terminal drains via MessageChannel

Chromium clamps nested setTimeout(0) to ~4ms, stacking dead gaps onto
every parse-clocked drain tick; the explicit 4ms high-priority re-arm
interval added more. A posted message is still a macrotask — input and
paint are serviced between posts — so cooperative yielding survives
without the clamp. Generation-tokened cancellation; vitest keeps the
timer path (fake timers can't advance channel posts) plus a real-timer
smoke test for the channel path. Standing-queue target: VS Code's ~7ms
class (measured us 18.6ms, them 7.18ms, same rig).

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-07-03 16:24:52 -04:00
co-authored by Orca
parent 4252a7eab4
commit 2434dfaaee
2 changed files with 91 additions and 1 deletions
@@ -94,3 +94,34 @@ describe.skipIf(!benchEnabled)('scheduler drain ceiling', () => {
)
})
})
// Real-timer smoke: the MessageChannel drain path must actually drain a
// high-priority backlog without any timer advancing (the clamp-dodge works).
import { setUseMessageChannelDrainForTesting } from './pane-terminal-output-scheduler'
describe('message-channel drain path', () => {
it('drains high-priority output with real timers and no timer advance', async () => {
vi.useRealTimers()
setUseMessageChannelDrainForTesting(true)
try {
const writes: string[] = []
const terminal = {
write: (data: string, cb?: () => void) => {
writes.push(data)
cb?.()
}
}
const { writeTerminalOutput, discardTerminalOutput } =
await import('./pane-terminal-output-scheduler')
for (let i = 0; i < 40; i++) {
writeTerminalOutput(terminal as never, `chunk-${i};`, { foreground: true })
}
await new Promise((resolve) => setTimeout(resolve, 250))
expect(writes.join('')).toContain('chunk-39;')
discardTerminalOutput(terminal as never)
} finally {
setUseMessageChannelDrainForTesting(null)
vi.useFakeTimers()
}
})
})
@@ -125,6 +125,47 @@ const backlogRecoveryByTerminal = new WeakMap<
>()
let drainTimer: ReturnType<typeof setTimeout> | null = null
let drainTimerDelayMs: number | null = null
// Why a MessageChannel for zero-delay drains: Chromium clamps nested
// setTimeout(0) to ~4ms, which stacks a dead gap onto every parse-clocked
// drain tick under flood (measured: standing queue ~18ms vs VS Code ~7ms).
// A posted message is still a macrotask — input events and paint are
// serviced between posts — so the cooperative yield survives without the
// clamp. Cancellation is by generation: posts carry the generation they
// were armed with and no-op when it has moved on.
let drainImmediatePending = false
let drainImmediateGeneration = 0
let useMessageChannelDrain = typeof MessageChannel !== 'undefined' && !isVitestEnv()
let drainChannel: MessageChannel | null = null
function isVitestEnv(): boolean {
// Why: vitest fake timers cannot advance MessageChannel macrotasks; the
// timer path keeps the existing suites' virtual clock authoritative.
return typeof process !== 'undefined' && process.env?.VITEST === 'true'
}
function getDrainChannel(): MessageChannel {
if (drainChannel === null) {
drainChannel = new MessageChannel()
drainChannel.port1.onmessage = (event: MessageEvent) => {
if (event.data !== drainImmediateGeneration || !drainImmediatePending) {
return
}
drainImmediatePending = false
drainQueuedOutput()
}
}
return drainChannel
}
function cancelImmediateDrain(): void {
drainImmediateGeneration++
drainImmediatePending = false
}
export function setUseMessageChannelDrainForTesting(value: boolean | null): void {
cancelImmediateDrain()
useMessageChannelDrain = value ?? (typeof MessageChannel !== 'undefined' && !isVitestEnv())
}
const debugEnabled = e2eConfig.exposeStore
// Why the cap is lossy: a hidden/backgrounded Chromium document can throttle
@@ -245,6 +286,10 @@ function exposeDebugApi(): void {
}
function scheduleDrain(delayMs: number): void {
if (drainImmediatePending) {
// An immediate drain is already armed — nothing can beat zero delay.
return
}
if (drainTimer !== null) {
if (drainTimerDelayMs !== null && drainTimerDelayMs <= delayMs) {
return
@@ -259,6 +304,11 @@ function scheduleDrain(delayMs: number): void {
if (debugEnabled) {
debugState.scheduledDrainCount++
}
if (delayMs === 0 && useMessageChannelDrain) {
drainImmediatePending = true
getDrainChannel().port2.postMessage(drainImmediateGeneration)
return
}
drainTimer = setTimeout(drainQueuedOutput, delayMs)
drainTimerDelayMs = delayMs
}
@@ -893,8 +943,17 @@ function drainQueuedOutput(): void {
}
recordQueueDebugPressure()
if (queuedByTerminal.size > 0 && hasDrainableBacklog()) {
// Why 0 on the channel path: the 4ms high-priority interval existed to
// yield between ticks, but a posted message already yields — Chromium
// services input and paint between macrotasks. The explicit sleep only
// deepened the standing queue (~4ms per 128KB tick). Timer path keeps
// the interval so fake-timer tests retain stepwise drain semantics.
scheduleDrain(
hasHighPriorityBacklog() ? HIGH_PRIORITY_DRAIN_INTERVAL_MS : BACKGROUND_DRAIN_INTERVAL_MS
hasHighPriorityBacklog()
? useMessageChannelDrain
? 0
: HIGH_PRIORITY_DRAIN_INTERVAL_MS
: BACKGROUND_DRAIN_INTERVAL_MS
)
}
}