perf(runtime): remove timer clamps from cooperative yields

Renderer paste and input loops can schedule more than a thousand zero-delay timer yields for a maximum-size payload. Chromium clamps nested timers to 4ms, adding seconds of idle wall time.

Use MessageChannel tasks in renderer runtimes and setImmediate in Node while retaining a timer fallback for tests and unsupported environments.
This commit is contained in:
Neil
2026-07-27 12:19:07 -07:00
parent 560f853a40
commit 05607f85a8
15 changed files with 122 additions and 55 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
import { measureClipboardTextByteLength } from '../../shared/clipboard-text'
import { yieldToEventLoop } from '../../shared/event-loop-yield'
import type { CdpCommandSender } from './snapshot-engine'
export const BROWSER_TEXT_INSERT_CHUNK_BYTES = 64 * 1024
@@ -71,7 +72,7 @@ export async function insertTextThroughCdp(
// process responsive between bounded CDP payloads.
chunk = chunks.next()
if (options?.yieldBetweenChunks !== false && !chunk.done) {
await new Promise<void>((resolve) => setTimeout(resolve, 0))
await yieldToEventLoop()
}
}
}
+1 -1
View File
@@ -145,7 +145,7 @@ export async function getProcessedDatabaseInfo(
}
async function yieldToEventLoop(): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, 0))
await new Promise((resolve) => setImmediate(resolve))
}
function getProjectJoin(db: Database.Database): string {
@@ -1,3 +1,4 @@
import { yieldToEventLoop } from '../../../../shared/event-loop-yield'
import {
TEXT_CONTROL_PASTE_CHUNK_MAX_BYTES,
TEXT_CONTROL_PASTE_DIRECT_MAX_BYTES,
@@ -180,7 +181,3 @@ function getNextDictationChunkBoundary(text: string, startIndex: number, maxByte
return index
}
function yieldToEventLoop(): Promise<void> {
return new Promise((resolve) => globalThis.setTimeout(resolve, 0))
}
@@ -1,4 +1,5 @@
import type { editor } from 'monaco-editor'
import { yieldToEventLoop } from '../../../../shared/event-loop-yield'
import {
measureTextControlPasteByteLength,
measureTextControlPasteByteLengthWithYield
@@ -158,10 +159,6 @@ function setCollapsedSelection(monacoEditor: MonacoPasteEditor, position: Positi
})
}
function yieldToEventLoop(): Promise<void> {
return new Promise((resolve) => globalThis.setTimeout(resolve, 0))
}
async function insertMonacoTextInChunks(
monacoEditor: MonacoPasteEditor,
text: string,
@@ -1,5 +1,6 @@
import type { Editor } from '@tiptap/react'
import { toast } from 'sonner'
import { yieldToEventLoop } from '../../../../shared/event-loop-yield'
import {
measureTextControlPasteByteLength,
measureTextControlPasteByteLengthWithYield
@@ -87,10 +88,6 @@ function getNextChunkBoundary(text: string, startIndex: number, maxBytes: number
return index
}
function yieldToEventLoop(): Promise<void> {
return new Promise((resolve) => globalThis.setTimeout(resolve, 0))
}
function isEditorAvailable(
editor: Editor,
canContinue: RichMarkdownLargeTextPasteOptions['canContinue']
@@ -1,3 +1,4 @@
import { yieldToEventLoop } from '../../../../shared/event-loop-yield'
import {
isTerminalInputTooLargeWithDeferredMeasurement,
iterateTerminalInputChunks
@@ -28,16 +29,12 @@ export type PtyInputWriteQueueDeps = {
yieldBetweenWrites?: () => Promise<void>
}
function defaultYieldBetweenWrites(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 0))
}
function isCoalescibleText(text: string): boolean {
return text.length <= TERMINAL_INPUT_COALESCE_MAX_CODE_UNITS
}
export function createPtyInputWriteQueue(deps: PtyInputWriteQueueDeps): PtyInputWriteQueue {
const yieldBetweenWrites = deps.yieldBetweenWrites ?? defaultYieldBetweenWrites
const yieldBetweenWrites = deps.yieldBetweenWrites ?? yieldToEventLoop
let pending: PendingPtyInputWrite[] = []
let drainPromise: Promise<void> | null = null
@@ -10,6 +10,7 @@ import {
isTerminalInputTooLargeWithDeferredMeasurement,
iterateTerminalInputChunks
} from '../../../../shared/terminal-input'
import { yieldToEventLoop } from '../../../../shared/event-loop-yield'
import { isRuntimeOwnedSshTargetId } from '../../../../shared/execution-host'
import {
ptyDataHandlers,
@@ -622,10 +623,6 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra
commit: clearAccumulatedState
}
function yieldToInputWriteDrain(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 0))
}
async function writeAcceptedPtyInput(id: string, data: string): Promise<boolean> {
try {
const tooLarge = isTerminalInputTooLargeWithDeferredMeasurement(data)
@@ -644,7 +641,7 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra
}
chunk = chunks.next()
if (!chunk.done) {
await yieldToInputWriteDrain()
await yieldToEventLoop()
}
}
return true
@@ -1,3 +1,4 @@
import { yieldToEventLoop } from '../../../../shared/event-loop-yield'
import { BRACKETED_PASTE_END, BRACKETED_PASTE_START } from './terminal-bracketed-paste'
import { iterateTerminalPastePlanChunks } from './terminal-paste-chunks'
import { createRedactedPasteExecutionDiagnostic } from './terminal-paste-diagnostics'
@@ -30,7 +31,7 @@ export async function executeTerminalPastePlan(
writePty,
isTargetCurrent,
canContinue,
yieldToEventLoop = defaultYieldToEventLoop,
yieldToEventLoop: yieldBetweenChunks = yieldToEventLoop,
operationTimeoutMs = getTerminalPasteOperationTimeoutMs(plan),
now = defaultNow
}: ExecuteTerminalPastePlanArgs
@@ -101,7 +102,7 @@ export async function executeTerminalPastePlan(
} else if (chunk === BRACKETED_PASTE_END) {
bracketedPasteOpen = false
}
await yieldToEventLoop()
await yieldBetweenChunks()
}
return finish('pasted', chunksWritten)
}
@@ -140,7 +141,3 @@ function result(
function defaultNow(): number {
return globalThis.performance?.now?.() ?? Date.now()
}
function defaultYieldToEventLoop(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 0))
}
@@ -1,3 +1,4 @@
import { yieldToEventLoop } from '../../../shared/event-loop-yield'
import type { GlobalSettings } from '../../../shared/types'
import {
BRACKETED_PASTE_END,
@@ -159,7 +160,7 @@ async function isSanitizedDraftPasteOverLimit(content: string, maxBytes: number)
index += 1
}
if (index >= nextYieldAt) {
await yieldToAgentDraftPastePreflight()
await yieldToEventLoop()
nextYieldAt = index + AGENT_DRAFT_PASTE_PREFLIGHT_YIELD_CODE_UNITS
}
}
@@ -187,10 +188,6 @@ function getUtf8ByteLengthForCodePoint(codePoint: number): number {
return 4
}
function yieldToAgentDraftPastePreflight(): Promise<void> {
return new Promise((resolve) => globalThis.setTimeout(resolve, 0))
}
async function writeAgentDraftPtyInput(
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined,
ptyId: string,
@@ -1,3 +1,5 @@
import { yieldToEventLoop } from '../../../shared/event-loop-yield'
export type PastePayloadMetadata = {
byteLength: number
exceededLimit: boolean
@@ -62,7 +64,7 @@ export async function measurePastePayloadMetadataWithYield(
1,
options.yieldAfterCodeUnits ?? PASTE_PAYLOAD_METADATA_YIELD_CODE_UNITS
)
const yieldToEventLoop = options.yieldToEventLoop ?? defaultPastePayloadMetadataYield
const yieldBetweenBatches = options.yieldToEventLoop ?? yieldToEventLoop
let nextYieldAt = yieldAfterCodeUnits
let byteLength = 0
let hasControlSequences = false
@@ -89,7 +91,7 @@ export async function measurePastePayloadMetadataWithYield(
index += 1
}
if (index >= nextYieldAt) {
await yieldToEventLoop()
await yieldBetweenBatches()
nextYieldAt = index + yieldAfterCodeUnits
}
}
@@ -149,7 +151,3 @@ function getUtf8ByteLengthForCodePoint(codePoint: number): number {
}
return 4
}
function defaultPastePayloadMetadataYield(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 0))
}
@@ -1,3 +1,4 @@
import { yieldToEventLoop } from '../../../shared/event-loop-yield'
import { isPrimarySelectionTextControl } from './primary-selection-capture'
import {
TEXT_CONTROL_PASTE_CHUNK_MAX_BYTES,
@@ -157,10 +158,6 @@ function insertContentEditableChunk(target: HTMLElement, range: Range, text: str
return range
}
function yieldToEventLoop(): Promise<void> {
return new Promise((resolve) => globalThis.setTimeout(resolve, 0))
}
async function pasteLargeTextIntoContentEditable(
target: HTMLElement,
text: string,
+1 -4
View File
@@ -1,3 +1,4 @@
import { yieldToEventLoop } from '../../../shared/event-loop-yield'
import {
createTextControlCancelledResult,
createTextControlPastedResult,
@@ -186,10 +187,6 @@ function getSelectionRange(target: HTMLInputElement | HTMLTextAreaElement): {
}
}
function yieldToEventLoop(): Promise<void> {
return new Promise((resolve) => globalThis.setTimeout(resolve, 0))
}
function defaultNow(): number {
return globalThis.performance?.now?.() ?? Date.now()
}
+4 -6
View File
@@ -1,3 +1,5 @@
import { yieldToEventLoop } from './event-loop-yield'
export const CLIPBOARD_TEXT_READ_MAX_BYTES = 16 * 1024 * 1024
export const CLIPBOARD_TEXT_WRITE_MAX_BYTES = 16 * 1024 * 1024
export const CLIPBOARD_TEXT_TOO_LARGE_ERROR = 'Clipboard text is too large for this paste target.'
@@ -53,7 +55,7 @@ export async function measureClipboardTextByteLengthWithYield(
1,
options.yieldAfterCodeUnits ?? CLIPBOARD_TEXT_MEASURE_YIELD_CODE_UNITS
)
const yieldToEventLoop = options.yieldToEventLoop ?? defaultClipboardTextMeasureYield
const yieldBetweenBatches = options.yieldToEventLoop ?? yieldToEventLoop
let nextYieldAt = yieldAfterCodeUnits
let byteLength = 0
@@ -67,7 +69,7 @@ export async function measureClipboardTextByteLengthWithYield(
index += 1
}
if (index >= nextYieldAt) {
await yieldToEventLoop()
await yieldBetweenBatches()
nextYieldAt = index + yieldAfterCodeUnits
}
}
@@ -183,7 +185,3 @@ function getUtf8ByteLengthForCodePoint(codePoint: number): number {
}
return 4
}
function defaultClipboardTextMeasureYield(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 0))
}
+42
View File
@@ -0,0 +1,42 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { yieldToEventLoop } from './event-loop-yield'
afterEach(() => {
vi.unstubAllEnvs()
vi.unstubAllGlobals()
})
describe('yieldToEventLoop', () => {
it('uses setImmediate in Node runtimes', async () => {
const scheduleImmediate = vi.fn((callback: () => void) => queueMicrotask(callback))
vi.stubEnv('VITEST', 'false')
vi.stubGlobal('window', undefined)
vi.stubGlobal('setImmediate', scheduleImmediate)
await yieldToEventLoop()
expect(scheduleImmediate).toHaveBeenCalledOnce()
})
it('posts one macrotask per renderer yield', async () => {
const postMessage = vi.fn()
vi.stubEnv('VITEST', 'false')
vi.stubGlobal('window', {})
vi.stubGlobal(
'MessageChannel',
class {
port1: { onmessage: ((event: MessageEvent) => void) | null } = { onmessage: null }
port2 = {
postMessage: (data: unknown): void => {
postMessage(data)
queueMicrotask(() => this.port1.onmessage?.({ data } as MessageEvent))
}
}
}
)
await Promise.all([yieldToEventLoop(), yieldToEventLoop()])
expect(postMessage).toHaveBeenCalledTimes(2)
})
})
+55
View File
@@ -0,0 +1,55 @@
type ImmediateGlobal = typeof globalThis & {
setImmediate?: (callback: () => void) => unknown
}
const pendingRendererYields: (() => void)[] = []
let nextRendererYieldIndex = 0
let rendererYieldChannel: MessageChannel | null = null
function isVitestEnvironment(): boolean {
return typeof process !== 'undefined' && process.env?.VITEST === 'true'
}
function getRendererYieldChannel(): MessageChannel {
if (!rendererYieldChannel) {
rendererYieldChannel = new globalThis.MessageChannel()
rendererYieldChannel.port1.onmessage = () => {
const resolve = pendingRendererYields[nextRendererYieldIndex]
if (!resolve) {
return
}
nextRendererYieldIndex += 1
if (nextRendererYieldIndex === pendingRendererYields.length) {
pendingRendererYields.length = 0
nextRendererYieldIndex = 0
}
resolve()
}
}
return rendererYieldChannel
}
export function yieldToEventLoop(): Promise<void> {
return new Promise((resolve) => {
// Vitest fake timers cannot advance MessageChannel tasks.
if (isVitestEnvironment()) {
globalThis.setTimeout(resolve, 0)
return
}
const setImmediate = (globalThis as ImmediateGlobal).setImmediate
if (typeof window === 'undefined' && setImmediate) {
setImmediate(resolve)
return
}
if (typeof globalThis.MessageChannel === 'function') {
// Posted tasks avoid Chromium's nested-timer clamp while still yielding to input and paint.
pendingRendererYields.push(resolve)
getRendererYieldChannel().port2.postMessage(undefined)
return
}
globalThis.setTimeout(resolve, 0)
})
}