mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(perf): preserve TS worker idle deadlines and boot guard diagnostics (#34055)
Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
@@ -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 })
|
||||
}
|
||||
})
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user