mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 00:03:15 +00:00
perf(startup): stop queueing window creation behind the proxy apply and i18n
Three independent, measured startup wins, all free: 1. Park the initial Chromium proxy apply on `mainProcessState` instead of awaiting it mid-`initializeReadyFoundation`. `setProxy` still starts at the identical moment; the default-session request guard (which holds, not cancels) is what actually fences fetchers on it, so only window creation stops waiting. Runtime launch still awaits it before the desktop relay and before every headless-serve fetcher. 2. Run `initializeMainProcessI18nAndMenu` concurrently with `initializeMainProcessRuntimeLaunch`. Nothing in window creation reads a translated string or the native menu. 3. Load `emojibase-data` in main through `createRequire` on first use instead of a static import, keeping 166 KB of JSON off `out/main/index.js` and its ~2 ms parse off every launch. The renderer keeps its eager copy unchanged. out/main/index.js 7,210,071 -> 7,040,147 bytes. No renderer behaviour changes.
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import { createRequire } from 'node:module'
|
||||
import type { EmojiShortcodeDataset } from '../../shared/emoji-shortcode-catalog'
|
||||
|
||||
// Why createRequire (same reason as linear-sdk.ts): a static import inlines the 166 KB
|
||||
// shortcode dataset into out/main/index.js and JSON.parses it on every launch, while only
|
||||
// worktree-name sanitization ever reads it. `emojibase-data` is a production dependency, so
|
||||
// the packaged app.asar resolves this the same way it resolves any other bare require.
|
||||
const requireFromMain = createRequire(__filename)
|
||||
|
||||
export function requireEmojiShortcodeDataset(): EmojiShortcodeDataset {
|
||||
return requireFromMain('emojibase-data/en/shortcodes/emojibase.json') as EmojiShortcodeDataset
|
||||
}
|
||||
@@ -4,9 +4,15 @@ import type { Repo } from '../../shared/repo-types'
|
||||
import { isWindowsAbsolutePathLike, resolveRuntimePath } from '../../shared/cross-platform-path'
|
||||
import { isWslUncPath, resolveWslRepoWorktreeBasePath } from '../../shared/wsl-paths'
|
||||
import { splitWorktreeId } from '../../shared/worktree/id'
|
||||
import { replaceKnownEmojiWithShortcodes } from '../../shared/emoji-shortcode-catalog'
|
||||
import {
|
||||
replaceKnownEmojiWithShortcodes,
|
||||
setEmojiShortcodeDatasetLoader
|
||||
} from '../../shared/emoji-shortcode-catalog'
|
||||
import { requireEmojiShortcodeDataset } from './deferred-emoji-shortcode-dataset'
|
||||
import { getWslHome, getWslHomeAsync, parseWslPath } from '../wsl'
|
||||
|
||||
setEmojiShortcodeDatasetLoader(requireEmojiShortcodeDataset)
|
||||
|
||||
type WorktreePathSettings = Pick<GlobalSettings, 'nestWorkspaces' | 'workspaceDir'> & {
|
||||
/** Distro to mirror the workspace root into when the repo itself sits on a
|
||||
* Windows drive but this project's git runs in WSL. Omitted = today's
|
||||
|
||||
@@ -139,7 +139,22 @@ export async function initializeReadyFoundation(): Promise<void> {
|
||||
})
|
||||
state.store = store
|
||||
// Why: create pending readiness before the guard can observe the default session.
|
||||
const initialProxyApplication = applyElectronProxySettings(store.getSettings())
|
||||
// Why parked on state instead of awaited here: Dock/Launchpad launches don't inherit shell
|
||||
// proxy env vars, so the persisted proxy must land before any app-owned network fetcher runs —
|
||||
// but the guard below already holds every default-session request until this settles, so
|
||||
// awaiting it inline only delayed window creation. Runtime launch awaits it before the first
|
||||
// fetcher (the desktop relay / headless serve).
|
||||
state.initialProxyApplicationReady = applyElectronProxySettings(store.getSettings()).then(
|
||||
(result) => {
|
||||
if (result.source === 'invalid-settings') {
|
||||
// Why (STA-3442): a silent DIRECT fallback made a dead configured proxy undiagnosable.
|
||||
console.warn('[proxy] persisted proxy settings are invalid; using direct networking')
|
||||
}
|
||||
},
|
||||
() => {
|
||||
console.warn('[proxy] Failed to apply network proxy settings')
|
||||
}
|
||||
)
|
||||
installElectronProxyRequestGuard(session.defaultSession)
|
||||
// Why armed here and not at install time: the report remembers what it last said, and
|
||||
// that state lives beside the profile data file, which does not exist until now.
|
||||
@@ -235,16 +250,6 @@ export async function initializeReadyFoundation(): Promise<void> {
|
||||
if (shouldSuppressDevEducation({ isDev: is.dev })) {
|
||||
suppressDevEducationForStore(store)
|
||||
}
|
||||
try {
|
||||
// Why: Dock/Launchpad launches don't inherit shell proxy env vars, so apply the persisted proxy before any app-owned network fetchers run.
|
||||
const proxyApplyResult = await initialProxyApplication
|
||||
if (proxyApplyResult.source === 'invalid-settings') {
|
||||
// Why (STA-3442): a silent DIRECT fallback made a dead configured proxy undiagnosable.
|
||||
console.warn('[proxy] persisted proxy settings are invalid; using direct networking')
|
||||
}
|
||||
} catch {
|
||||
console.warn('[proxy] Failed to apply network proxy settings')
|
||||
}
|
||||
// Why: the partition installer reads the proxy through this resolver, so register it before sessions materialize.
|
||||
setBrowserNetworkProxySettingsResolver(() => state.store!.getSettings())
|
||||
// Why: the preview session is protocol-scoped, so the handler must exist before any preview webview attaches.
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const phaseEvents: string[] = []
|
||||
let releaseI18n: (() => void) | null = null
|
||||
|
||||
vi.mock('./main-process-ready-foundation', () => ({
|
||||
initializeReadyFoundation: vi.fn(async () => {
|
||||
phaseEvents.push('foundation')
|
||||
})
|
||||
}))
|
||||
vi.mock('./main-process-ready-runtime', () => ({
|
||||
initializeReadyRuntimeServices: vi.fn(async () => {
|
||||
phaseEvents.push('runtime-services')
|
||||
})
|
||||
}))
|
||||
vi.mock('./main-process-i18n-menu', () => ({
|
||||
initializeMainProcessI18nAndMenu: vi.fn(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
phaseEvents.push('i18n-start')
|
||||
releaseI18n = () => {
|
||||
phaseEvents.push('i18n-done')
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
)
|
||||
}))
|
||||
vi.mock('./main-process-runtime-launch', () => ({
|
||||
initializeMainProcessRuntimeLaunch: vi.fn(async () => {
|
||||
phaseEvents.push('launch-start')
|
||||
await Promise.resolve()
|
||||
phaseEvents.push('window-created')
|
||||
})
|
||||
}))
|
||||
|
||||
const { initializeMainProcessReady } = await import('./main-process-ready')
|
||||
|
||||
describe('ready-phase concurrency', () => {
|
||||
beforeEach(() => {
|
||||
phaseEvents.length = 0
|
||||
releaseI18n = null
|
||||
})
|
||||
|
||||
it('creates the window without waiting for i18n and the native menu', async () => {
|
||||
const options = {
|
||||
openMainWindow: vi.fn(),
|
||||
handleMacAppActivation: vi.fn()
|
||||
} as unknown as Parameters<typeof initializeMainProcessReady>[0]
|
||||
|
||||
const ready = initializeMainProcessReady(options)
|
||||
// Drain the launch phase's microtasks while i18n is still pending.
|
||||
for (let tick = 0; tick < 8; tick += 1) {
|
||||
await Promise.resolve()
|
||||
}
|
||||
|
||||
expect(phaseEvents).toEqual([
|
||||
'foundation',
|
||||
'runtime-services',
|
||||
'i18n-start',
|
||||
'launch-start',
|
||||
'window-created'
|
||||
])
|
||||
|
||||
releaseI18n?.()
|
||||
await ready
|
||||
expect(phaseEvents.at(-1)).toBe('i18n-done')
|
||||
})
|
||||
|
||||
it('still resolves only once i18n and the menu have settled', async () => {
|
||||
const options = {
|
||||
openMainWindow: vi.fn(),
|
||||
handleMacAppActivation: vi.fn()
|
||||
} as unknown as Parameters<typeof initializeMainProcessReady>[0]
|
||||
|
||||
const ready = initializeMainProcessReady(options)
|
||||
let settled = false
|
||||
void ready.then(() => {
|
||||
settled = true
|
||||
})
|
||||
for (let tick = 0; tick < 8; tick += 1) {
|
||||
await Promise.resolve()
|
||||
}
|
||||
|
||||
expect(settled).toBe(false)
|
||||
releaseI18n?.()
|
||||
await ready
|
||||
expect(settled).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('initial proxy application ordering', () => {
|
||||
const readStartupSource = (file: string): string =>
|
||||
readFileSync(join(process.cwd(), 'src/main/startup', file), 'utf8')
|
||||
|
||||
it('parks the default-session proxy apply instead of blocking window creation on it', () => {
|
||||
const foundation = readStartupSource('main-process-ready-foundation.ts')
|
||||
|
||||
expect(foundation).toContain('state.initialProxyApplicationReady = applyElectronProxySettings(')
|
||||
// The request guard, not this phase, is what fences fetchers on the proxy; awaiting it here
|
||||
// only queued openMainWindow behind a ~24 ms setProxy round trip.
|
||||
expect(foundation).not.toMatch(/await\s+(?:state\.)?initialProxyApplication/)
|
||||
})
|
||||
|
||||
it('awaits the proxy after the window opens and before the desktop relay starts', () => {
|
||||
const launch = readStartupSource('main-process-runtime-launch.ts')
|
||||
const desktopStart = launch.indexOf('async function launchDesktopMode(')
|
||||
const desktopEnd = launch.indexOf('\nexport async function initializeMainProcessRuntimeLaunch')
|
||||
expect(desktopStart).toBeGreaterThanOrEqual(0)
|
||||
expect(desktopEnd).toBeGreaterThan(desktopStart)
|
||||
const desktop = launch.slice(desktopStart, desktopEnd)
|
||||
|
||||
const windowIndex = desktop.indexOf('openMainWindow()')
|
||||
const proxyIndex = desktop.indexOf('await state.initialProxyApplicationReady')
|
||||
const relayIndex = desktop.indexOf('new DesktopRelayService(')
|
||||
|
||||
expect(windowIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(proxyIndex).toBeGreaterThan(windowIndex)
|
||||
expect(relayIndex).toBeGreaterThan(proxyIndex)
|
||||
})
|
||||
|
||||
it('keeps headless serve strictly ordered behind the proxy apply', () => {
|
||||
const launch = readStartupSource('main-process-runtime-launch.ts')
|
||||
const serveStart = launch.indexOf('async function launchServeMode(')
|
||||
const serveEnd = launch.indexOf('\nasync function launchDesktopMode(', serveStart)
|
||||
expect(serveStart).toBeGreaterThanOrEqual(0)
|
||||
expect(serveEnd).toBeGreaterThan(serveStart)
|
||||
const serve = launch.slice(serveStart, serveEnd)
|
||||
|
||||
const proxyIndex = serve.indexOf('await state.initialProxyApplicationReady')
|
||||
const rpcIndex = serve.indexOf('runtimeRpc.start()')
|
||||
|
||||
expect(proxyIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(rpcIndex).toBeGreaterThan(proxyIndex)
|
||||
})
|
||||
})
|
||||
@@ -12,6 +12,11 @@ export async function initializeMainProcessReady(
|
||||
): Promise<void> {
|
||||
await initializeReadyFoundation()
|
||||
await initializeReadyRuntimeServices()
|
||||
await initializeMainProcessI18nAndMenu()
|
||||
await initializeMainProcessRuntimeLaunch(options)
|
||||
// Why concurrent: window creation reads no translated string and no menu item, and both the
|
||||
// native menu and the tray only become reachable once the window shows — so serializing them
|
||||
// ahead of openMainWindow only delayed the renderer (8 ms in English, more for a lazy locale).
|
||||
await Promise.all([
|
||||
initializeMainProcessI18nAndMenu(),
|
||||
initializeMainProcessRuntimeLaunch(options)
|
||||
])
|
||||
}
|
||||
|
||||
@@ -120,6 +120,9 @@ async function launchServeMode(
|
||||
runtimeRpc: OrcaRuntimeRpcServer,
|
||||
serveOptions: NonNullable<ReturnType<typeof getServeOptions>>
|
||||
): Promise<void> {
|
||||
// Why here: headless serve has no window to unblock, so keep the persisted proxy strictly
|
||||
// ahead of every fetcher this phase can reach (relay, CLI install, RPC clients).
|
||||
await state.initialProxyApplicationReady
|
||||
// Why: give managed WSL launchers a brief chance to migrate before headless PTYs go live, without slow repairs withholding all RPC readiness.
|
||||
logStartupMilestone('wsl-cli-barrier-start')
|
||||
await state.managedWslCliStartupBarrierReady
|
||||
@@ -228,6 +231,10 @@ async function launchDesktopMode(
|
||||
if (!runtimeRpcStartResult.ok) {
|
||||
void showRuntimeRpcStartupFailureDialog(win, runtimeRpcStartResult.error)
|
||||
}
|
||||
// Why after the window and not before it: the default-session request guard already holds every
|
||||
// fetcher until the persisted proxy lands, so this only has to keep the launch phase itself
|
||||
// ordered ahead of the relay — it must not gate the renderer.
|
||||
await state.initialProxyApplicationReady
|
||||
const cloudAuth = getOrcaCloudAuthConfig()
|
||||
if (cloudAuth.configured) {
|
||||
try {
|
||||
|
||||
@@ -100,6 +100,9 @@ export const mainProcessState = {
|
||||
// Electron with no error. Only the renderer's own pull proves the listener is live.
|
||||
markdownFileOpenListenerReady: false,
|
||||
firstWindowStartupServicesReady: Promise.resolve(),
|
||||
// Why published: the default-session proxy must be applied before the first app-owned fetcher,
|
||||
// but window creation has no reason to queue behind it (the request guard already fences it).
|
||||
initialProxyApplicationReady: Promise.resolve(),
|
||||
managedWslCliReconciliationReady: Promise.resolve(),
|
||||
managedWslCliStartupBarrierReady: Promise.resolve(),
|
||||
// Why: the serve barrier fails open, so this state tells headless clients a WSL PTY launch may still race an un-migrated registration ('settled' = off-Windows no-op).
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import emojiShortcodes from 'emojibase-data/en/shortcodes/emojibase.json'
|
||||
import {
|
||||
getStandardEmojiShortcodeEntries,
|
||||
setEmojiShortcodeDatasetLoader,
|
||||
type StandardEmojiShortcodeEntry
|
||||
} from '../../../shared/emoji-shortcode-catalog'
|
||||
|
||||
// Why eager here and lazy in main: a dynamic import made the shortcode transform return an
|
||||
// empty catalog until it settled, so a `:wink:` submitted in that window persisted literally.
|
||||
setEmojiShortcodeDatasetLoader(() => emojiShortcodes)
|
||||
|
||||
export type WorkspaceEmojiSuggestion = StandardEmojiShortcodeEntry
|
||||
|
||||
export type ActiveWorkspaceEmojiShortcode = {
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import emojiShortcodes from 'emojibase-data/en/shortcodes/emojibase.json'
|
||||
|
||||
async function importConfiguredCatalog() {
|
||||
const catalog = await import('./emoji-shortcode-catalog.js')
|
||||
catalog.setEmojiShortcodeDatasetLoader(() => emojiShortcodes)
|
||||
return catalog
|
||||
}
|
||||
|
||||
describe('emoji shortcode catalog laziness', () => {
|
||||
beforeEach(() => {
|
||||
@@ -8,7 +15,7 @@ describe('emoji shortcode catalog laziness', () => {
|
||||
})
|
||||
|
||||
it('does not build the catalog when the shared module is imported', async () => {
|
||||
const catalog = await import('./emoji-shortcode-catalog.js')
|
||||
const catalog = await importConfiguredCatalog()
|
||||
|
||||
expect(catalog.isEmojiShortcodeCatalogBuiltForTest()).toBe(false)
|
||||
|
||||
@@ -17,20 +24,23 @@ describe('emoji shortcode catalog laziness', () => {
|
||||
})
|
||||
|
||||
it('builds on first use and keeps the main process off the eager path', async () => {
|
||||
const catalog = await import('./emoji-shortcode-catalog.js')
|
||||
const catalog = await importConfiguredCatalog()
|
||||
|
||||
expect(catalog.replaceKnownEmojiWithShortcodes('ship \u{1F389}')).toBe('ship party ')
|
||||
expect(catalog.isEmojiShortcodeCatalogBuiltForTest()).toBe(true)
|
||||
})
|
||||
|
||||
it('leaves the main-process worktree namer importing only the deferred entry point', () => {
|
||||
it('leaves the main-process worktree namer importing only the deferred entry points', () => {
|
||||
// A cross-project import would drag src/main into the shared tsconfig, so assert on source.
|
||||
const worktreeLogic = readFileSync(join(__dirname, '../main/ipc/worktree-logic.ts'), 'utf8')
|
||||
const catalogImport = worktreeLogic.match(
|
||||
/import \{([^}]*)\} from '[^']*emoji-shortcode-catalog'/
|
||||
)
|
||||
|
||||
expect(catalogImport?.[1].trim()).toBe('replaceKnownEmojiWithShortcodes')
|
||||
expect(catalogImport?.[1].split(',').map((name) => name.trim())).toEqual([
|
||||
'replaceKnownEmojiWithShortcodes',
|
||||
'setEmojiShortcodeDatasetLoader'
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps the catalog build out of module scope', () => {
|
||||
@@ -41,4 +51,33 @@ describe('emoji shortcode catalog laziness', () => {
|
||||
expect(sharedSource).not.toMatch(/^const \w+ = new (?:Map|Intl\.Segmenter)\(/m)
|
||||
expect(sharedSource).toContain('function loadCatalog()')
|
||||
})
|
||||
|
||||
it('keeps the 166 KB dataset off every module main statically imports', () => {
|
||||
const sharedSource = readFileSync(join(__dirname, 'emoji-shortcode-catalog.ts'), 'utf8')
|
||||
const worktreeLogic = readFileSync(join(__dirname, '../main/ipc/worktree-logic.ts'), 'utf8')
|
||||
|
||||
// A static `emojibase-data` import anywhere main reaches inlines the dataset into
|
||||
// out/main/index.js and JSON.parses it on every launch.
|
||||
const staticDatasetImport = /\bfrom '[^']*emojibase-data[^']*'/
|
||||
expect(sharedSource).not.toMatch(staticDatasetImport)
|
||||
expect(worktreeLogic).not.toMatch(staticDatasetImport)
|
||||
})
|
||||
|
||||
it('loads the main-side dataset synchronously into an identical catalog', async () => {
|
||||
const { requireEmojiShortcodeDataset } =
|
||||
await import('../main/ipc/deferred-emoji-shortcode-dataset.js')
|
||||
const eager = await importConfiguredCatalog()
|
||||
const eagerEntries = eager.getStandardEmojiShortcodeEntries()
|
||||
const eagerTransform = eager.replaceKnownEmojiWithShortcodes('ship \u{1F389} \u{1F44D}')
|
||||
|
||||
vi.resetModules()
|
||||
const deferred = await import('./emoji-shortcode-catalog.js')
|
||||
deferred.setEmojiShortcodeDatasetLoader(requireEmojiShortcodeDataset)
|
||||
|
||||
// No await between registration and first use: the require path keeps the sync contract.
|
||||
expect(deferred.getStandardEmojiShortcodeEntries()).toEqual(eagerEntries)
|
||||
expect(deferred.replaceKnownEmojiWithShortcodes('ship \u{1F389} \u{1F44D}')).toBe(
|
||||
eagerTransform
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
import emojiShortcodes from 'emojibase-data/en/shortcodes/emojibase.json'
|
||||
|
||||
export type StandardEmojiShortcodeEntry = {
|
||||
emoji: string
|
||||
shortcode: string
|
||||
}
|
||||
|
||||
/** Shape of `emojibase-data/en/shortcodes/emojibase.json`: hexcode -> shortcode or aliases. */
|
||||
export type EmojiShortcodeDataset = Readonly<Record<string, string | readonly string[]>>
|
||||
|
||||
let loadDataset: (() => EmojiShortcodeDataset) | null = null
|
||||
|
||||
/**
|
||||
* Why injected instead of statically imported: the renderer must keep its eager copy (a
|
||||
* dynamic import there returned an empty catalog mid-load and persisted `:wink:` literally),
|
||||
* but a static import here also inlines the same 166 KB into out/main/index.js and JSON.parses
|
||||
* it on every launch. Main supplies a lazy require instead; both stay synchronous.
|
||||
*/
|
||||
export function setEmojiShortcodeDatasetLoader(load: () => EmojiShortcodeDataset): void {
|
||||
loadDataset = load
|
||||
}
|
||||
|
||||
// Skin-tone aliases (`wave_tone3`) are ~40% of the dataset and would drown the suggestion list.
|
||||
const SKIN_TONE_SHORTCODE = /_tone\d(?:-\d)?$/
|
||||
|
||||
@@ -23,7 +36,10 @@ function loadCatalog(): EmojiShortcodeCatalog {
|
||||
if (catalog) {
|
||||
return catalog
|
||||
}
|
||||
const grouped = Object.entries(emojiShortcodes).flatMap(([hexcode, value]) => {
|
||||
if (!loadDataset) {
|
||||
throw new Error('Emoji shortcode dataset loader was never registered')
|
||||
}
|
||||
const grouped = Object.entries(loadDataset()).flatMap(([hexcode, value]) => {
|
||||
const shortcodes = (typeof value === 'string' ? [value] : value).filter(
|
||||
(shortcode) => !SKIN_TONE_SHORTCODE.test(shortcode)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user