mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 08:03:28 +00:00
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:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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()
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user