From 380db44d77f92052db404d3c84b7866a4f4e5a13 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 29 Jun 2026 20:37:26 -0700 Subject: [PATCH] Measure and batch restored terminal startup (#6836) --- src/main/index.ts | 32 ++- src/main/persistence.ts | 27 ++- src/preload/api-types.ts | 2 + src/preload/index.ts | 6 + src/renderer/src/App.tsx | 131 ++++++++---- .../src/startup/startup-diagnostics.ts | 69 ++++++ .../store/slices/terminals-hydration.test.ts | 52 +++++ src/renderer/src/store/slices/terminals.ts | 53 +++-- src/renderer/src/web/web-preload-api.ts | 1 + tools/benchmarks/startup-time-bench.mjs | 202 ++++++++++++++++-- 10 files changed, 485 insertions(+), 90 deletions(-) create mode 100644 src/renderer/src/startup/startup-diagnostics.ts diff --git a/src/main/index.ts b/src/main/index.ts index 8f5ecc58411..00886001c16 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -523,23 +523,41 @@ ipcMain.handle('app:awaitFirstWindowStartupServices', async () => { await firstWindowStartupServicesReady }) +ipcMain.handle( + 'app:startupDiagnostic', + (_event, event: string, details?: Record) => { + if (!startupDiagnosticsEnabled || !event.startsWith('renderer-')) { + return + } + logStartupMilestone(event, details && typeof details === 'object' ? details : {}) + } +) + function startDesktopFirstWindowStartupServices(): Promise { + logStartupMilestone('first-window-startup-services-start') const startupServices = startFirstWindowStartupServices({ // Why: the persistent-terminal daemon is desktop-only. Headless `orca serve` // registers its PTY runtime separately and must not spawn the desktop daemon // or hook loopback listener. - startDaemonPtyProvider: (signal) => initDaemonPtyProvider(signal), + startDaemonPtyProvider: async (signal) => { + logStartupMilestone('startup-service-start', { service: 'daemon-pty-provider' }) + await initDaemonPtyProvider(signal) + logStartupMilestone('startup-service-done', { service: 'daemon-pty-provider' }) + }, // Why: PTY spawn env reads ORCA_AGENT_HOOK_* from the live server state, so // the renderer awaits this barrier before restored terminals reconnect. - startAgentHookServer: () => - agentHookServer.start({ + startAgentHookServer: async () => { + logStartupMilestone('startup-service-start', { service: 'agent-hook-server' }) + await agentHookServer.start({ env: app.isPackaged ? 'production' : 'development', // Why: hooks source this endpoint file at invocation time, so old PTY // env still reaches the current Orca process after an app restart. // Dev uses a namespace because all worktrees share `orca-dev`. userDataPath: app.getPath('userData'), endpointNamespace: devAgentHookEndpointNamespace - }), + }) + logStartupMilestone('startup-service-done', { service: 'agent-hook-server' }) + }, onDaemonError: (error) => { console.error('[daemon] Failed to start daemon PTY provider, falling back to local:', error) }, @@ -551,6 +569,12 @@ function startDesktopFirstWindowStartupServices(): Promise { }) firstWindowStartupServicesReady = startupServices.firstWindowReady localPtyStartupReady = startupServices.localPtyReady + void firstWindowStartupServicesReady.then(() => { + logStartupMilestone('first-window-startup-services-ready') + }) + void localPtyStartupReady.then(() => { + logStartupMilestone('local-pty-startup-ready') + }) return firstWindowStartupServicesReady } diff --git a/src/main/persistence.ts b/src/main/persistence.ts index c2651a3c05f..9c9c19cf7f2 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -209,6 +209,7 @@ import { } from './terminal-scrollback-snapshots' import { track } from './telemetry/client' import { getCohortAtEmit } from './telemetry/cohort-classifier' +import { isStartupDiagnosticsEnabled, logStartupDiagnostic } from './startup/startup-diagnostics' function encrypt(plaintext: string): string { if (!plaintext || !safeStorage.isEncryptionAvailable()) { @@ -382,6 +383,15 @@ const WORKSPACE_SESSION_PATCH_FULL_NORMALIZATION_KEYS = new Set = {} +): void { + if (isStartupDiagnosticsEnabled()) { + logStartupDiagnostic(event, { t: Math.round(performance.now()), ...details }) + } +} + function workspaceSessionPatchNeedsFullNormalization(patch: WorkspaceSessionPatch): boolean { return Object.keys(patch).some((key) => WORKSPACE_SESSION_PATCH_FULL_NORMALIZATION_KEYS.has(key as keyof WorkspaceSessionState) @@ -2617,12 +2627,22 @@ export class Store { // social contract we installed them under. const dataFile = getDataFile() const fileExistedOnLoad = existsSync(dataFile) + logPersistenceStartupMilestone('persistence-load-start', { + fileExists: fileExistedOnLoad + }) let result: PersistedState | null = null try { if (fileExistedOnLoad) { + const readStartedAt = performance.now() const raw = readFileSync(dataFile, 'utf-8') + logPersistenceStartupMilestone('persistence-read-done', { + bytes: Buffer.byteLength(raw), + durationMs: Math.round(performance.now() - readStartedAt) + }) + logPersistenceStartupMilestone('persistence-json-parse-start') const parsed = JSON.parse(raw) as PersistedState + logPersistenceStartupMilestone('persistence-json-parse-done') // Why: secret settings are stored encrypted on disk via safeStorage. // Decrypt at the load boundary so the rest of the app sees plaintext. @@ -3215,7 +3235,12 @@ export class Store { } result = folderScopeConnectionMigration.state - return this.migrateTelemetry(result, fileExistedOnLoad) + const migrated = this.migrateTelemetry(result, fileExistedOnLoad) + logPersistenceStartupMilestone('persistence-load-done', { + repos: migrated.repos.length, + workspaceSessionBytes: Buffer.byteLength(JSON.stringify(migrated.workspaceSession)) + }) + return migrated } // One-shot telemetry cohort migration. Runs on every `load()` but is a diff --git a/src/preload/api-types.ts b/src/preload/api-types.ts index 36c4db8e173..288789c852d 100644 --- a/src/preload/api-types.ts +++ b/src/preload/api-types.ts @@ -804,6 +804,8 @@ export type AppApi = { /** Resolves when the daemon PTY provider and hook receiver have either * started or failed open for the first BrowserWindow. */ awaitFirstWindowStartupServices: () => Promise + /** Emits a startup benchmark marker when ORCA_STARTUP_DIAGNOSTICS is enabled. */ + startupDiagnostic: (event: string, details?: Record) => Promise /** Returns the macOS active input mode, or layout ID when no IME mode is * selected (e.g. `com.apple.keylayout.PolishPro`). Used by the * keyboard-layout probe to distinguish CJK IMEs and layouts whose base diff --git a/src/preload/index.ts b/src/preload/index.ts index 13e9a6ccf97..44a7a223ef7 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -434,6 +434,8 @@ document.addEventListener( true ) +const startupDiagnosticsEnabled = process.env.ORCA_STARTUP_DIAGNOSTICS === '1' + // Custom APIs for renderer const api = { app: { @@ -456,6 +458,10 @@ const api = { reload: (): Promise => ipcRenderer.invoke('app:reload'), awaitFirstWindowStartupServices: (): Promise => ipcRenderer.invoke('app:awaitFirstWindowStartupServices'), + startupDiagnostic: (event: string, details?: Record): Promise => + startupDiagnosticsEnabled + ? ipcRenderer.invoke('app:startupDiagnostic', event, details) + : Promise.resolve(), // Why: on macOS this returns the active input mode, or the layout ID when // no IME mode is selected, so renderer keyboard workarounds can distinguish // CJK IMEs and compose layouts from plain US QWERTY (see issue #1205). diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index d8aeb9b7dd4..697d0dd45f5 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -120,6 +120,11 @@ import { getStartupErrorFallbackUI, hydratePersistedUIAfterStartupRead } from './lib/startup-ui-hydration' +import { + logRendererStartupDiagnostic, + timeRendererStartupStep, + timeRendererStartupSyncStep +} from './startup/startup-diagnostics' import { shouldRenderPetOverlay } from './components/pet/pet-overlay-visibility' import { applyDocumentTheme } from './lib/document-theme' import { isEditableTarget } from './lib/editable-target' @@ -843,39 +848,50 @@ function App(): React.JSX.Element { // UI mounts. let reconnectStarted = false void (async () => { + const startupStartedAt = performance.now() + logRendererStartupDiagnostic('startup-chain-start') try { // Why: repo/worktree hydration routes through settings.activeRuntimeEnvironmentId. // Load settings first so a persisted remote runtime does not boot against // the local filesystem and then hydrate stale local workspace state. - await actions.fetchSettings() + await timeRendererStartupStep('fetch-settings', () => actions.fetchSettings()) // Why: load local + every configured runtime environment (not just the // active one) so a cold start that restored a remote workspace doesn't // hide local repos. The sidebar "All hosts" scope then shows them all. - await actions.fetchReposForAllHosts() - await actions.fetchProjectGroupsForAllHosts() - await actions.fetchFolderWorkspacesForAllHosts() - await actions.fetchAllWorktrees() - await actions.fetchWorktreeLineage() - const persistedUI = await window.api.ui.get() - uiHydrated = hydratePersistedUIAfterStartupRead({ - persistedUI, - cancelled, - hydratePersistedUI: actions.hydratePersistedUI - }) + await timeRendererStartupStep('fetch-repos', () => actions.fetchReposForAllHosts()) + await timeRendererStartupStep('fetch-project-groups', () => + actions.fetchProjectGroupsForAllHosts() + ) + await timeRendererStartupStep('fetch-folder-workspaces', () => + actions.fetchFolderWorkspacesForAllHosts() + ) + await timeRendererStartupStep('fetch-worktrees', () => actions.fetchAllWorktrees()) + await timeRendererStartupStep('fetch-worktree-lineage', () => + actions.fetchWorktreeLineage() + ) + const persistedUI = await timeRendererStartupStep('ui-get', () => window.api.ui.get()) + uiHydrated = timeRendererStartupSyncStep('hydrate-persisted-ui', () => + hydratePersistedUIAfterStartupRead({ + persistedUI, + cancelled, + hydratePersistedUI: actions.hydratePersistedUI + }) + ) // Why: runtime-owned worktree slices live in per-host partitions. // Repos were fetched above, so the known runtime hosts are derivable // here; merge their slices into the unified session the hydrators // expect. An unreadable host partition is skipped (fail-soft). - const session = await fetchWorkspaceSessionFromHosts( - window.api.session, - useAppStore.getState().repos + const session = await timeRendererStartupStep('session-get', () => + fetchWorkspaceSessionFromHosts(window.api.session, useAppStore.getState().repos) ) - await actions.fetchKeybindings() + await timeRendererStartupStep('fetch-keybindings', () => actions.fetchKeybindings()) if (!cancelled) { - actions.hydrateWorkspaceSession(session) - actions.hydrateTabsSession(session) - actions.hydrateEditorSession(session) - actions.hydrateBrowserSession(session) + timeRendererStartupSyncStep('hydrate-session-stores', () => { + actions.hydrateWorkspaceSession(session) + actions.hydrateTabsSession(session) + actions.hydrateEditorSession(session) + actions.hydrateBrowserSession(session) + }) // Why: prune lastVisitedAtByWorktreeId entries whose worktrees // no longer exist. Must run AFTER hydration — before this point, // async repo loads may not have populated worktreesByRepo yet and @@ -884,10 +900,16 @@ function App(): React.JSX.Element { // so users upgrading from a pre-feature build don't see the active // worktree sink in the empty-query list. // See docs/cmd-j-empty-query-ordering.md. - actions.pruneLastVisitedTimestamps() - actions.seedActiveWorktreeLastVisitedIfMissing() - await actions.fetchBrowserSessionProfiles() - const onboardingState = await window.api.onboarding.get() + timeRendererStartupSyncStep('visit-timestamp-prune', () => { + actions.pruneLastVisitedTimestamps() + actions.seedActiveWorktreeLastVisitedIfMissing() + }) + await timeRendererStartupStep('fetch-browser-session-profiles', () => + actions.fetchBrowserSessionProfiles() + ) + const onboardingState = await timeRendererStartupStep('onboarding-get', () => + window.api.onboarding.get() + ) if (!cancelled) { setOnboarding(onboardingState) setOnboardingLoaded(true) @@ -902,7 +924,9 @@ function App(): React.JSX.Element { if (connectionIds.length > 0) { try { const SSH_RECONNECT_TIMEOUT_MS = 15_000 - const allTargets = await window.api.ssh.listTargets() + const allTargets = await timeRendererStartupStep('ssh-list-targets', () => + window.api.ssh.listTargets() + ) const targetMap = new Map(allTargets.map((t) => [t.id, t])) const targets = connectionIds.map((targetId) => ({ targetId, @@ -923,25 +947,33 @@ function App(): React.JSX.Element { // reattached when the user focuses the tab (by which time the // slow connect will likely have succeeded). const timedOutTargets: string[] = [] - await Promise.allSettled( - eagerTargets.map(({ targetId }) => - Promise.race([ - window.api.ssh.connect({ targetId }), - new Promise((_, reject) => - setTimeout( - () => reject(new Error('SSH reconnect timeout')), - SSH_RECONNECT_TIMEOUT_MS - ) + await timeRendererStartupStep( + 'ssh-reconnect', + () => + Promise.allSettled( + eagerTargets.map(({ targetId }) => + Promise.race([ + window.api.ssh.connect({ targetId }), + new Promise((_, reject) => + setTimeout( + () => reject(new Error('SSH reconnect timeout')), + SSH_RECONNECT_TIMEOUT_MS + ) + ) + ]).catch((err) => { + const isTimeout = + err instanceof Error && err.message === 'SSH reconnect timeout' + if (isTimeout) { + timedOutTargets.push(targetId) + } + console.warn(`SSH auto-reconnect failed for ${targetId}:`, err) + }) ) - ]).catch((err) => { - const isTimeout = - err instanceof Error && err.message === 'SSH reconnect timeout' - if (isTimeout) { - timedOutTargets.push(targetId) - } - console.warn(`SSH auto-reconnect failed for ${targetId}:`, err) - }) - ) + ), + { + eagerTargets: eagerTargets.length, + deferredTargets: deferredTargets.length + } ) if (timedOutTargets.length > 0) { actions.setDeferredSshReconnectTargets([ @@ -974,14 +1006,20 @@ function App(): React.JSX.Element { } catch (err) { console.warn('SSH startup reconnect failed:', err) } + } else { + logRendererStartupDiagnostic('ssh-reconnect-skipped', { connectionIds: 0 }) } // Why: main overlaps daemon/hook startup with renderer hydration for // first paint, but restored terminals still need those services ready // before they mount and spawn/reconnect PTYs. - await window.api.app.awaitFirstWindowStartupServices() + await timeRendererStartupStep('first-window-services-await', () => + window.api.app.awaitFirstWindowStartupServices() + ) reconnectStarted = true - await actions.reconnectPersistedTerminals(abortController.signal) + await timeRendererStartupStep('reconnect-terminals', () => + actions.reconnectPersistedTerminals(abortController.signal) + ) syncZoomCSSVar() // Why (issue #1158): unlock the debounced session writer only after // hydration AND all dependent startup steps (SSH reconnect, terminal @@ -991,6 +1029,9 @@ function App(): React.JSX.Element { // and the writer would serialize a partially-mutated store back to // disk — the exact data-loss mode this PR fixes. actions.setHydrationSucceeded(true) + logRendererStartupDiagnostic('startup-hydration-done', { + durationMs: Math.round(performance.now() - startupStartedAt) + }) } } catch (error) { // Why (issue #1158): previously this catch called hydrateWorkspaceSession diff --git a/src/renderer/src/startup/startup-diagnostics.ts b/src/renderer/src/startup/startup-diagnostics.ts new file mode 100644 index 00000000000..b3586c967cb --- /dev/null +++ b/src/renderer/src/startup/startup-diagnostics.ts @@ -0,0 +1,69 @@ +type StartupDiagnosticDetails = Record + +function nowMs(): number { + return Math.round(performance.now()) +} + +export function logRendererStartupDiagnostic( + event: string, + details: StartupDiagnosticDetails = {} +): void { + const api = window.api?.app + if (!api?.startupDiagnostic) { + return + } + void api + .startupDiagnostic(`renderer-${event}`, { + rendererT: nowMs(), + ...details + }) + .catch(() => { + // Diagnostics are best-effort and must never perturb startup behavior. + }) +} + +export async function timeRendererStartupStep( + event: string, + operation: () => Promise, + details: StartupDiagnosticDetails = {} +): Promise { + const startedAt = performance.now() + try { + const result = await operation() + logRendererStartupDiagnostic(`${event}-done`, { + durationMs: Math.round(performance.now() - startedAt), + ...details + }) + return result + } catch (error) { + logRendererStartupDiagnostic(`${event}-failed`, { + durationMs: Math.round(performance.now() - startedAt), + message: error instanceof Error ? error.message : String(error), + ...details + }) + throw error + } +} + +export function timeRendererStartupSyncStep( + event: string, + operation: () => T, + details: StartupDiagnosticDetails = {} +): T { + const startedAt = performance.now() + try { + const result = operation() + logRendererStartupDiagnostic(`${event}-done`, { + durationMs: Math.round(performance.now() - startedAt), + ...details + }) + return result + } catch (error) { + logRendererStartupDiagnostic(`${event}-failed`, { + durationMs: Math.round(performance.now() - startedAt), + message: error instanceof Error ? error.message : String(error), + ...details + }) + throw error + } +} diff --git a/src/renderer/src/store/slices/terminals-hydration.test.ts b/src/renderer/src/store/slices/terminals-hydration.test.ts index 082184dfdaf..e4b37423137 100644 --- a/src/renderer/src/store/slices/terminals-hydration.test.ts +++ b/src/renderer/src/store/slices/terminals-hydration.test.ts @@ -224,6 +224,58 @@ describe('hydrateWorkspaceSession', () => { expect(store.getState().pendingReconnectWorktreeIds).toEqual([FLOATING_TERMINAL_WORKTREE_ID]) }) + it('batches restored terminal reconnect wake hints into one store update', async () => { + const store = createTestStore() + const worktreeId = 'repo1::/wt-1' + seedStore(store, { + worktreesByRepo: { + repo1: [makeWorktree({ id: worktreeId, repoId: 'repo1', path: '/wt-1' })] + } + }) + const session: WorkspaceSessionState = { + activeRepoId: 'repo1', + activeWorktreeId: worktreeId, + activeTabId: 'tab-1', + tabsByWorktree: { + [worktreeId]: [ + makeTab({ id: 'tab-1', worktreeId, ptyId: 'pty-1' }), + makeTab({ id: 'tab-2', worktreeId, ptyId: 'pty-2' }), + makeTab({ id: 'tab-3', worktreeId, ptyId: 'pty-3' }) + ] + }, + terminalLayoutsByTabId: { + 'tab-1': makeLayout(), + 'tab-2': makeLayout(), + 'tab-3': makeLayout() + }, + activeWorktreeIdsOnShutdown: [worktreeId] + } + + store.getState().hydrateWorkspaceSession(session) + + let updateCount = 0 + const unsubscribe = store.subscribe(() => { + updateCount += 1 + }) + await store.getState().reconnectPersistedTerminals() + unsubscribe() + + // Why: startup restores every daemon wake hint, but subscribers should see + // one ready-state transition instead of one update per restored tab. + expect(updateCount).toBe(1) + expect(store.getState().workspaceSessionReady).toBe(true) + expect(store.getState().ptyIdsByTabId).toMatchObject({ + 'tab-1': ['pty-1'], + 'tab-2': ['pty-2'], + 'tab-3': ['pty-3'] + }) + expect(store.getState().tabsByWorktree[worktreeId]).toEqual([ + expect.objectContaining({ id: 'tab-1', ptyId: 'pty-1' }), + expect.objectContaining({ id: 'tab-2', ptyId: 'pty-2' }), + expect.objectContaining({ id: 'tab-3', ptyId: 'pty-3' }) + ]) + }) + it('resets persisted agent titles to the fallback label on hydration', () => { const store = createTestStore() const worktreeId = 'repo1::/wt-1' diff --git a/src/renderer/src/store/slices/terminals.ts b/src/renderer/src/store/slices/terminals.ts index c09301ae5cc..0271c8aeee7 100644 --- a/src/renderer/src/store/slices/terminals.ts +++ b/src/renderer/src/store/slices/terminals.ts @@ -2887,7 +2887,8 @@ export const createTerminalSlice: StateCreator pendingReconnectTabByWorktree, pendingReconnectPtyIdByTabId, terminalLayoutsByTabId, - tabsByWorktree + tabsByWorktree, + ptyIdsByTabId } = get() const ids = pendingReconnectWorktreeIds ?? [] @@ -2911,6 +2912,8 @@ export const createTerminalSlice: StateCreator // The layout's ptyIdsByLeafId (preserved from shutdown) already has per-leaf // mappings. For single-pane tabs without leaf mappings, store the tab-level // ptyId as a sentinel so connectPanePty knows to reattach. + let reconnectedTabsByWorktree: Record | null = null + let reconnectedPtyIdsByTabId: Record | null = null for (const worktreeId of ids) { const tabs = tabsByWorktree[worktreeId] ?? [] const worktree = Object.values(get().worktreesByRepo) @@ -2961,33 +2964,27 @@ export const createTerminalSlice: StateCreator `[reconnect-terminals] tab=${tabId} tabLevelPtyId=${tabLevelPtyId} supportsDeferredReattach=${supportsDeferredReattach} hasLeafMappings=${hasLeafMappings}` ) if (tabLevelPtyId) { - set((s) => { - const next = { ...s.tabsByWorktree } - if (!next[worktreeId]) { - return {} - } + reconnectedTabsByWorktree ??= { ...tabsByWorktree } + const nextTabs = reconnectedTabsByWorktree[worktreeId] + if (!nextTabs) { + continue + } - // Why: populate ptyIdsByTabId so the sessions status segment - // can map daemon session IDs back to tabs (for bound/orphan - // detection and click-to-navigate). Without this, all sessions - // appear as orphans until the terminal pane mounts. - const allPtyIds = hasLeafMappings - ? (Object.values(leafPtyMap).filter(Boolean) as string[]) - : [tabLevelPtyId!] - next[worktreeId] = next[worktreeId].map((t) => - t.id === tabId ? { ...t, ptyId: tabLevelPtyId } : t - ) - return { - tabsByWorktree: next, - // Why: hide-sleeping uses ptyIdsByTabId as the liveness source. - // Restored daemon sessions are still running even before their - // pane remounts, so background workspaces must advertise them. - ptyIdsByTabId: { - ...s.ptyIdsByTabId, - [tabId]: allPtyIds - } - } - }) + // Why: populate ptyIdsByTabId so the sessions status segment + // can map daemon session IDs back to tabs (for bound/orphan + // detection and click-to-navigate). Without this, all sessions + // appear as orphans until the terminal pane mounts. + const allPtyIds = hasLeafMappings + ? (Object.values(leafPtyMap).filter(Boolean) as string[]) + : [tabLevelPtyId] + reconnectedTabsByWorktree[worktreeId] = nextTabs.map((t) => + t.id === tabId ? { ...t, ptyId: tabLevelPtyId } : t + ) + // Why: hide-sleeping uses ptyIdsByTabId as the liveness source. + // Restored daemon sessions are still running even before their + // pane remounts, so background workspaces must advertise them. + reconnectedPtyIdsByTabId ??= { ...ptyIdsByTabId } + reconnectedPtyIdsByTabId[tabId] = allPtyIds } } } @@ -3019,6 +3016,8 @@ export const createTerminalSlice: StateCreator } set({ + ...(reconnectedTabsByWorktree ? { tabsByWorktree: reconnectedTabsByWorktree } : {}), + ...(reconnectedPtyIdsByTabId ? { ptyIdsByTabId: reconnectedPtyIdsByTabId } : {}), workspaceSessionReady: true, pendingReconnectWorktreeIds: [], pendingReconnectTabByWorktree: {}, diff --git a/src/renderer/src/web/web-preload-api.ts b/src/renderer/src/web/web-preload-api.ts index 812e7607aae..89dafc97116 100644 --- a/src/renderer/src/web/web-preload-api.ts +++ b/src/renderer/src/web/web-preload-api.ts @@ -464,6 +464,7 @@ function createWebPreloadApi(): Partial { restart: () => Promise.resolve(window.location.reload()), reload: () => Promise.resolve(window.location.reload()), awaitFirstWindowStartupServices: () => Promise.resolve(), + startupDiagnostic: () => Promise.resolve(), getKeyboardInputSourceId: () => Promise.resolve(null), setUnreadDockBadgeCount: () => Promise.resolve(), getFloatingTerminalCwd: () => Promise.resolve(''), diff --git a/tools/benchmarks/startup-time-bench.mjs b/tools/benchmarks/startup-time-bench.mjs index 5e2f390bb79..573894ae974 100644 --- a/tools/benchmarks/startup-time-bench.mjs +++ b/tools/benchmarks/startup-time-bench.mjs @@ -11,19 +11,30 @@ * Usage: * node tools/benchmarks/startup-time-bench.mjs --label baseline * [--iterations 5] [--files 28000] [--fixture-dir ] - * [--exe ] [--timeout-ms 240000] + * [--state-profile none|restored-local-tabs] [--session-tabs 200] + * [--wait-for-event renderer-startup-hydration-done] + * [--exe ] [--timeout-ms 240000] * * Prereq (when not using --exe): `pnpm build:electron-vite` so out/ exists. * Results: tools/benchmarks/results/startup-