perf(runtime): remove timer clamps from cooperative yields (#10908)

* 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.

* fix(runtime): preserve pacing and release yield callbacks

Adversarial review found that concurrent producers could retain resolved callbacks until global quiescence. Route renderer yields by token and delete each resolver before resuming its producer.

Keep timer pacing in terminal paste and accepted-write loops where SSH and local PTYs do not provide drain acknowledgement. Use the shared scheduler for the OpenCode scanner.
This commit is contained in:
Neil 2026-07-27 12:21:14 -07:00 committed by GitHub
parent c140a51118
commit 8b154d686c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 134 additions and 47 deletions

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()
}
}
}

View File

@ -3,6 +3,7 @@ import { existsSync } from 'node:fs'
import { readdir, realpath, stat } from 'node:fs/promises'
import { homedir } from 'node:os'
import { basename, isAbsolute, join, posix, win32 } from 'node:path'
import { yieldToEventLoop } from '../../shared/event-loop-yield'
import type { Repo } from '../../shared/types'
import { areWorktreePathsEqual } from '../ipc/worktree-logic'
import Database from '../sqlite/sync-database'
@ -144,10 +145,6 @@ export async function getProcessedDatabaseInfo(
}
}
async function yieldToEventLoop(): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, 0))
}
function getProjectJoin(db: Database.Database): string {
return tableExists(db, 'project') && columnExists(db, 'session', 'project_id')
? 'LEFT JOIN project p ON p.id = s.project_id'

View File

@ -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))
}

View File

@ -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,

View File

@ -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']

View File

@ -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

View File

@ -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,

View File

@ -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))
}

View File

@ -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,

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()
}

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))
}

View File

@ -0,0 +1,54 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { getPendingRendererYieldCountForTesting, 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('releases callbacks during sustained concurrent renderer yields', async () => {
const postMessage = vi.fn()
let peakPendingAfterResolution = 0
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)
setTimeout(() => this.port1.onmessage?.({ data } as MessageEvent), 0)
}
}
}
)
const runProducer = async (): Promise<void> => {
for (let index = 0; index < 20; index += 1) {
await yieldToEventLoop()
peakPendingAfterResolution = Math.max(
peakPendingAfterResolution,
getPendingRendererYieldCountForTesting()
)
}
}
await Promise.all([runProducer(), runProducer()])
expect(postMessage).toHaveBeenCalledTimes(40)
expect(peakPendingAfterResolution).toBeLessThanOrEqual(1)
expect(getPendingRendererYieldCountForTesting()).toBe(0)
})
})

View File

@ -0,0 +1,60 @@
type ImmediateGlobal = typeof globalThis & {
setImmediate?: (callback: () => void) => unknown
}
const pendingRendererYields = new Map<number, () => void>()
let nextRendererYieldId = 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 = (event) => {
const yieldId = event.data
const resolve = typeof yieldId === 'number' ? pendingRendererYields.get(yieldId) : undefined
if (!resolve) {
return
}
pendingRendererYields.delete(yieldId)
resolve()
}
}
return rendererYieldChannel
}
/** @internal */
export function getPendingRendererYieldCountForTesting(): number {
return pendingRendererYields.size
}
/** Yields to another runnable task without a timer clamp when supported. */
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.
const yieldId = nextRendererYieldId
nextRendererYieldId += 1
pendingRendererYields.set(yieldId, resolve)
getRendererYieldChannel().port2.postMessage(yieldId)
return
}
globalThis.setTimeout(resolve, 0)
})
}