fix(perf): preserve TS worker idle deadlines and boot guard diagnostics (#34055)

Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
OrcaWin
2026-10-26 13:00:29 -07:00
committed by GitHub
co-authored by m4air
parent c4f8fe5f3c
commit faaa59049f
5 changed files with 123 additions and 38 deletions
+10 -10
View File
@@ -105,16 +105,16 @@ export function verifyRendererBootGraph(root = process.cwd()) {
console.error(` ${chunk}`)
}
console.error('Escape them in the source (e.g. \\u2014 inside String.raw regex sources).')
return 1
}
if (violations.length === 0) {
return 0
if (violations.length > 0) {
console.error('Payloads that must stay off the renderer boot graph are preloaded again:')
for (const violation of violations) {
console.error(` ${violation.label} -> ${violation.chunk}`)
}
console.error('')
console.error(
'Load them after first paint (see primeTerminalWebglAddon) or from a route chunk.'
)
}
console.error('Payloads that must stay off the renderer boot graph are preloaded again:')
for (const violation of violations) {
console.error(` ${violation.label} -> ${violation.chunk}`)
}
console.error('')
console.error('Load them after first paint (see primeTerminalWebglAddon) or from a route chunk.')
return 1
return twoByteChunks.length > 0 || violations.length > 0 ? 1 : 0
}
@@ -0,0 +1,41 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { expect, it, vi } from 'vitest'
import { verifyRendererBootGraph } from './renderer-boot-graph.mjs'
it.each([
[false, false],
[true, false],
[false, true],
[true, true]
])('reports both boot guards independently (two-byte=%s, payload=%s)', (twoByte, payload) => {
const root = mkdtempSync(join(tmpdir(), 'orca-boot-guards-'))
const errors = vi.spyOn(console, 'error').mockImplementation(() => {})
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
try {
const renderer = join(root, 'out', 'renderer')
const i18n = join(root, 'src', 'renderer', 'src', 'i18n')
mkdirSync(join(renderer, 'assets'), { recursive: true })
mkdirSync(join(i18n, 'locales'), { recursive: true })
writeFileSync(join(i18n, 'locales', 'en.json'), JSON.stringify({ pruned: 'x'.repeat(50) }))
writeFileSync(join(i18n, 'en-runtime-required.json'), '{}')
writeFileSync(
join(renderer, 'index.html'),
'<script type="module" src="/assets/index.js"></script>'
)
writeFileSync(
join(renderer, 'assets', 'index.js'),
JSON.stringify(`${twoByte ? '\u2014' : ''}${payload ? 'WebGL2 not supported' : ''}`)
)
expect(verifyRendererBootGraph(root)).toBe(twoByte || payload ? 1 : 0)
const output = errors.mock.calls.flat().join('\n')
expect(output.includes('Boot chunks contain chars above U+00FF')).toBe(twoByte)
expect(output.includes('@xterm/addon-webgl -> index.js')).toBe(payload)
} finally {
errors.mockRestore()
log.mockRestore()
rmSync(root, { recursive: true, force: true })
}
})
+1 -2
View File
@@ -4,7 +4,6 @@ import { join } from 'node:path'
import type * as TimerPromises from 'node:timers/promises'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import type { DaemonEndpointIdentity } from './daemon-hello-protocol'
import { DAEMON_V8_FLAGS } from './daemon-launched-child-spawn'
import type { ProcessResult, ProcessSpec } from '@orca/process-host/process-spec'
const { state, runProcessMock, materializeMock, ensureWithinMock, disconnectMock, recoverMock } =
@@ -233,7 +232,7 @@ it('launches the copied helper on plain Node and leaves no inherited credentials
: []
expect(args.slice(0, 5)).toEqual([
helperExec(),
...DAEMON_V8_FLAGS,
'--max-semi-space-size=4',
helperEntry(),
'--socket',
options.socketPath
@@ -1,30 +1,42 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { installMonacoTsWorkerIdleStop } from './monaco-ts-worker-idle-stop'
function createModel(languageId: string) {
return { getLanguageId: () => languageId }
}
function createFakeMonaco(languages: string[]) {
const listeners: (() => void)[] = []
const models = languages.map(createModel)
const defaults = () => ({
getCompilerOptions: vi.fn(() => ({ jsx: 1 })),
setCompilerOptions: vi.fn()
})
const typescriptDefaults = defaults()
const javascriptDefaults = defaults()
const subscribe = (listener: () => void) => {
listeners.push(listener)
return { dispose: vi.fn() }
}
const onWillDisposeModel = vi.fn<
(listener: (model: ReturnType<typeof createModel>) => void) => { dispose(): void }
>(() => ({ dispose: vi.fn() }))
const onDidChangeModelLanguage = vi.fn<
(listener: (event: { model: ReturnType<typeof createModel>; oldLanguage: string }) => void) => {
dispose(): void
}
>(() => ({ dispose: vi.fn() }))
const monaco = {
editor: {
getModels: () => languages.map((id) => ({ getLanguageId: () => id })),
onWillDisposeModel: subscribe,
onDidChangeModelLanguage: subscribe
},
editor: { getModels: () => models, onWillDisposeModel, onDidChangeModelLanguage },
typescript: { typescriptDefaults, javascriptDefaults }
}
return {
monaco,
languages,
listeners,
models,
disposeModel(model: ReturnType<typeof createModel>) {
onWillDisposeModel.mock.calls[0][0](model)
models.splice(models.indexOf(model), 1)
},
changeLanguage(model: ReturnType<typeof createModel>, language: string) {
const oldLanguage = model.getLanguageId()
model.getLanguageId = () => language
onDidChangeModelLanguage.mock.calls[0][0]({ model, oldLanguage })
},
typescriptDefaults,
javascriptDefaults
}
@@ -33,27 +45,51 @@ function createFakeMonaco(languages: string[]) {
afterEach(() => vi.useRealTimers())
describe('installMonacoTsWorkerIdleStop', () => {
it('stops the TS/JS workers once no TS/JS model remains for the idle delay', () => {
it('stops on the original deadline despite unrelated model events', () => {
vi.useFakeTimers()
const fake = createFakeMonaco(['typescript'])
const fake = createFakeMonaco(['typescript', 'markdown', 'plaintext'])
installMonacoTsWorkerIdleStop(fake.monaco, 1000)
fake.languages.splice(0)
fake.listeners[0]()
vi.advanceTimersByTime(999)
fake.disposeModel(fake.models[0])
vi.advanceTimersByTime(500)
fake.disposeModel(fake.models[0])
fake.changeLanguage(fake.models[0], 'markdown')
vi.advanceTimersByTime(499)
expect(fake.typescriptDefaults.setCompilerOptions).not.toHaveBeenCalled()
vi.advanceTimersByTime(1)
expect(fake.typescriptDefaults.setCompilerOptions).toHaveBeenCalledWith({ jsx: 1 })
expect(fake.javascriptDefaults.setCompilerOptions).toHaveBeenCalledWith({ jsx: 1 })
})
it('keeps the worker while a TS/JS model is still open', () => {
it('rechecks reopened TS/JS models and waits for their departure', () => {
vi.useFakeTimers()
const fake = createFakeMonaco(['typescript', 'markdown'])
const fake = createFakeMonaco(['typescript'])
installMonacoTsWorkerIdleStop(fake.monaco, 1000)
fake.listeners[0]()
fake.disposeModel(fake.models[0])
fake.models.push(createModel('javascript'))
vi.advanceTimersByTime(1000)
expect(fake.typescriptDefaults.setCompilerOptions).not.toHaveBeenCalled()
fake.changeLanguage(fake.models[0], 'markdown')
vi.advanceTimersByTime(999)
expect(fake.javascriptDefaults.setCompilerOptions).not.toHaveBeenCalled()
vi.advanceTimersByTime(1)
expect(fake.javascriptDefaults.setCompilerOptions).toHaveBeenCalledTimes(1)
})
it('cancels the pending stop and model subscriptions on cleanup', () => {
vi.useFakeTimers()
const fake = createFakeMonaco(['javascript'])
const dispose = installMonacoTsWorkerIdleStop(fake.monaco, 1000)
fake.disposeModel(fake.models[0])
dispose()
vi.advanceTimersByTime(1000)
expect(fake.javascriptDefaults.setCompilerOptions).not.toHaveBeenCalled()
for (const event of [
fake.monaco.editor.onWillDisposeModel,
fake.monaco.editor.onDidChangeModelLanguage
]) {
expect(event.mock.results[0].value.dispose).toHaveBeenCalledOnce()
}
})
})
@@ -3,7 +3,8 @@ export const TS_WORKER_IDLE_STOP_MS = 60_000
const TS_LANGUAGE_IDS = new Set(['typescript', 'javascript'])
type ModelEvent = (listener: () => void) => { dispose(): void }
type Model = { getLanguageId(): string }
type ModelEvent<Event> = (listener: (event: Event) => void) => { dispose(): void }
type LanguageDefaults<Options> = {
getCompilerOptions(): Options
setCompilerOptions(options: Options): void
@@ -11,9 +12,9 @@ type LanguageDefaults<Options> = {
/** The slice of the monaco namespace this reads; the real namespace satisfies it structurally. */
export type MonacoTsWorkerHost<Options> = {
editor: {
getModels(): readonly { getLanguageId(): string }[]
onWillDisposeModel: ModelEvent
onDidChangeModelLanguage: ModelEvent
getModels(): readonly Model[]
onWillDisposeModel: ModelEvent<Model>
onDidChangeModelLanguage: ModelEvent<{ model: Model; oldLanguage: string }>
}
typescript: {
typescriptDefaults: LanguageDefaults<Options>
@@ -49,8 +50,16 @@ export function installMonacoTsWorkerIdleStop<Options>(
}
const subscriptions = [
monaco.editor.onWillDisposeModel(schedule),
monaco.editor.onDidChangeModelLanguage(schedule)
monaco.editor.onWillDisposeModel((model) => {
if (TS_LANGUAGE_IDS.has(model.getLanguageId())) {
schedule()
}
}),
monaco.editor.onDidChangeModelLanguage(({ model, oldLanguage }) => {
if (TS_LANGUAGE_IDS.has(oldLanguage) && !TS_LANGUAGE_IDS.has(model.getLanguageId())) {
schedule()
}
})
]
return () => {
if (timer !== null) {