mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 09:37:33 +00:00
* [comment audit] Align legacy failure-read counts with cached display * [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci] * [comment audit] Condense shared generation, path and preview comments [skip ci] * [comment audit] Condense host, file and Git contract rationale [skip ci] * [comment audit] Tighten Git lifecycle and provider contract comments [skip ci] * [comment audit] Clarify shared host authority and launch isolation [skip ci] * [comment audit] simplify local access mobile and native chat contracts [skip ci] * [comment audit] simplify native chat presentation and state contracts [skip ci] * [comment audit] simplify native runtime and provider contracts [skip ci] * [comment audit] simplify migration and orchestration contracts [skip ci] * [comment audit] simplify plugin trust and process capture rationale [skip ci] * [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci] * [comment audit] clarify remote recovery and launch wire rationale [skip ci] * [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci] * [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci] * [comment audit] clarify sidebar, skill and SSH invariants [skip ci] * [comment audit] condense structured chat delivery and timing rationale [skip ci] * [comment audit] condense terminal replay and telemetry invariants [skip ci] * [comment audit] clarify launch permission and terminal delivery rationale [skip ci] * [comment audit] clarify workspace ownership and layout contracts [skip ci] * [comment audit] wait for fitted and painted SSH terminal links * [comment audit] clarify layout persistence and session ownership [skip ci] * [comment audit] clarify host routing and workspace lifecycle contracts [skip ci] * [comment audit] clarify startup persistence and app lifetime comments [skip ci] * [comment audit] clarify recovery and activity boundaries [skip ci] * [comment audit] condense activity and artifact invariants [skip ci] * [comment audit] condense automation authority contracts [skip ci] * [comment audit] simplify automation scheduling and history contracts [skip ci] * [comment audit] clarify browser chrome and annotation lifecycles [skip ci] * [comment audit] clarify browser routing and retained guest ownership [skip ci] * [comment audit] clarify browser recovery and document ownership comments [skip ci] * [comment audit] restore hosted runners required by native IME fixtures * [comment audit] shorten preview, palette and dashboard invariants [skip ci] * [comment audit] clarify terminal preview, dashboard and retention comments [skip ci] * [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci] * [comment audit] shorten rich-text and combined-diff ownership comments [skip ci] * [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci] * [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci] * [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci] * [comment audit] clarify rich Markdown source and selection safeguards [skip ci] * [comment audit] clarify editor loading and emulator stream lifetimes [skip ci] * [comment audit] trim emulator and feature tour narration [skip ci] * [comment audit] clarify floating workspace and GitHub cache ownership [skip ci] * [comment audit] trim provider controls and Linear filter explanations [skip ci] * [comment audit] clarify pairing and chat resume lifetimes [skip ci] * [comment audit] clarify chat delivery and transcript lifetimes [skip ci] * [comment audit] clarify session options and host-owned status [skip ci] * [comment audit] clarify draft durability and live-status precedence [skip ci] * [comment audit] Trim chat send and model ownership explanations [skip ci] * [comment audit] Shorten transcript ownership and send recovery comments [skip ci] * [comment audit] Simplify native chat composer and transcript hook comments [skip ci] * [comment audit] Shorten native chat scroll and structured session explanations [skip ci] * [comment audit] Trim workspace picker and onboarding commentary [skip ci] * [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci] * [comment audit] Trim sidebar and session history explanations [skip ci] * [comment audit] Condense session resume and review checks comments [skip ci] * [comment audit] Shorten checks state and file explorer refresh comments [skip ci] * [comment audit] Trim plugin bridge and source-control action rationale [skip ci] * [comment audit] Trim source-control listing and review rationale [skip ci] * [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci] * [comment audit] Condense Settings account and appearance rationale [skip ci] * [comment audit] Trim pairing and repository Settings explanations [skip ci] * [comment audit] Condense Settings search SSH and terminal safeguards [skip ci] * [comment audit] Shorten Settings voice and provider ownership comments [skip ci] * [comment audit] Condense Settings draft persistence and host-state rationale [skip ci] * [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci] * [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci] * [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci] * [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci] * [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci] * [comment audit] Prune focus-read baseline after comment cleanup * [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci] * [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci] * [comment audit] Clarify viewport geometry and skill update safeguards [skip ci] * [comment audit] Tighten skill inventory, usage and host status comments [skip ci] * [comment audit] Clarify usage ownership and resource kill safeguards [skip ci] * [comment audit] Tighten tab selection, rename and search contracts [skip ci] * [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci] * [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci] * [comment audit] Tighten provider loading and terminal recovery comments [skip ci] * [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci] * [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci] * [comment audit] Tighten PTY startup and replay explanations [skip ci] * [comment audit] Clarify PTY identity and delivery ownership [skip ci] * [comment audit] Shorten remote reconnect and input replay rationale [skip ci] * [comment audit] Condense terminal parking clipboard and file link comments [skip ci] * [comment audit] Clarify IME ownership and trim trace history [skip ci] * [comment audit] Shorten pane recovery and parked watcher explanations [skip ci] * [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci] * [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci] * [comment audit] Trim renderer lifecycle and menu rationale [skip ci] * [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci] * [comment audit] Clarify renderer hook evidence and source ownership [skip ci] * [comment audit] Tighten IPC ownership and reconnect explanations [skip ci] * [comment audit] Trim renderer focus, paste and attention comments [skip ci] * [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci] * [comment audit] Trim agent launch, resume and ownership commentary [skip ci] * [comment audit] Shorten account, browser and editor comments [skip ci] * [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci] * [comment audit] Condense launch recovery and editor tokenization comments [skip ci] * [comment audit] Condense workspace selection and terminal pane rationale [skip ci] * [comment audit] Condense terminal fitting and rendering recovery comments [skip ci] * [comment audit] Condense terminal input and host routing explanations [skip ci] * [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci] * [comment audit] Condense structured launch and terminal link explanations [skip ci] * [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci] * [comment audit] Condense session persistence and workspace activation rationale [skip ci] * [comment audit] Condense workspace routing and browser ownership rationale [skip ci] * [comment audit] Condense mirror recovery and layout authority rationale [skip ci] * [comment audit] Condense runtime transport and file safety rationale [skip ci] * [comment audit] Condense runtime graph and structured session rationale [skip ci] * [comment audit] Condense browser creation and remote tab recovery rationale [skip ci] * [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci] * [comment audit] Condense frame, startup and provider cache rationale [skip ci] * [comment audit] Condense host twin and agent lifecycle rationale [skip ci] * [comment audit] Condense browser authority and editor cleanup rationale [skip ci] * [comment audit] Condense editor buffer identity and review cache rationale [skip ci] * [comment audit] Condense provider lifetime and repository ownership rationale [skip ci] * [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci] * [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci] * [comment audit] Condense cleanup ownership and listing admission rationale [skip ci] * [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci] * [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci] * [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci] * [comment audit] Condense cloud rollout and assignment safety rationale [skip ci] * [comment audit] Condense relay session and database recovery rationale [skip ci] * [comment audit] Condense relay placement and protocol compatibility rationale [skip ci] * [comment audit] Condense build and packaging constraints [skip ci] * [comment audit] Condense CI gates and validation isolation rationale [skip ci] * [comment audit] Condense catalog identity and browser validation rationale [skip ci] * [comment audit] Condense mobile browser and frame-budget rationale [skip ci] * [comment audit] Shorten mobile route and browser test explanations [skip ci] * [comment audit] Condense editor and session seam test rationale [skip ci] * [comment audit] Trim session and terminal closure history [skip ci] * [comment audit] Shorten terminal browser and census explanations [skip ci] * [comment audit] Preserve root-only E2E diagnostic baseline * [comment audit] Condense mobile seam and HTML preview test rationale [skip ci] * [comment audit] Condense preview instrumentation and render harness rationale [skip ci] * [comment audit] Condense bundle contracts and native-build constraints [skip ci] * [comment audit] Shorten native packaging and CI routing rationale [skip ci] * [comment audit] Condense release tooling and benchmark constraints [skip ci] * [comment audit] Condense packaging and platform gate rationale [skip ci] * [comment audit] Shorten mobile route and history rationale [skip ci] * [comment audit] Condense mobile browser and component rationale [skip ci] * [comment audit] Shorten drawer and workspace-form rationale [skip ci] * [comment audit] Trim mobile review and editor-document explanations [skip ci] * [comment audit] Condense editor lifecycle and Markdown explanations [skip ci] * [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci] * [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci] * [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci] * [comment audit] Condense bridge stream, native request and audio rationale [skip ci] * [comment audit] Reduce bridge schema and page-client commentary [skip ci] * [comment audit] Tighten mobile page bridge contracts [skip ci] * [comment audit] Clarify mobile cache and session constraints [skip ci] * [comment audit] Condense mobile route and lifecycle rationale [skip ci] * [comment audit] Tighten mobile shell hook timing rationale [skip ci] * [comment audit] Clarify route handoff and notification compatibility [skip ci] * [comment audit] Clarify push registration and dictation cleanup constraints [skip ci] * [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci] * [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci] * [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci] * [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci] * [comment audit] Tighten stream replay and queued session ownership rationale [skip ci] * [comment audit] Condense mobile composer and reply-schema constraints [skip ci] * [comment audit] Clarify mobile transcript recovery and session ownership [skip ci] * [comment audit] Clarify mobile queue and source-control compatibility [skip ci] * [comment audit] Condense storage and task launch constraints [skip ci] * [comment audit] Clarify task schemas and terminal document ownership [skip ci] * [comment audit] Tighten terminal input and viewport rationale [skip ci] * [comment audit] Shorten terminal and RPC recorder contracts [skip ci] * [comment audit] Condense recorder engine and connection contracts [skip ci] * [comment audit] Shorten host credential and relay recovery comments [skip ci] * [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci] * [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci] * [comment audit] Condense catalog and relay entry contracts [skip ci] * [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci] * [comment audit] Condense WSL relay and E2E regression rationale [skip ci] * [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci] * [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci] * [comment audit] Shorten layout oracle and regression explanations [skip ci] * [comment audit] Condense paired recovery and ownership regression contracts [skip ci] * [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci] * [comment audit] Condense terminal rendering and IME regression rationale [skip ci] * [comment audit] Shorten terminal recovery and benchmark contracts [skip ci] * [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci] * [comment audit] Condense workflow routing and release rationale [skip ci] * [comment audit] Condense mobile security and installer contracts [skip ci] * [comment audit] Shorten native rationale and stylesheet workarounds [skip ci] * [comment audit] Shorten Windows visibility probe rationale [skip ci] * [comment audit] Condense embedded release and signing rationale [skip ci] * [comment audit] Retain current main test behavior in final reconciliation [skip ci] * [comment audit] Complete final comment review across current source inventory * [comment audit] Handle consolidated audit reports before final CI * [comment audit] Preserve credential storage census detail [skip ci] * [comment audit] Scale broad changed E2E selections with verified timing shards * [comment audit] Align E2E assertions with browser close and settled session metadata * [comment audit] Match existing E2Es to current resource and windowing contracts * [comment audit] Observe the shared server outage notice in reconnect coverage * [comment audit] Make existing renderer probes measure settled tab and PTY work * [comment audit] Align palette E2E selector with task search * [comment audit] Refresh remaining palette search selectors * [comment audit] Keep selector correction within selected audit specs * [comment audit] Await emoji fixture settings inside renderer evaluation * [comment audit] Cancel fixture fit retries between terminal tests --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
523 lines
17 KiB
JavaScript
523 lines
17 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* Orca startup-time benchmark.
|
||
*
|
||
* Launches the built app (out/) against a synthetic userData fixture that
|
||
* mimics a long-lived real profile (tens of thousands of Chromium cache
|
||
* files — the documented pathological case for the win32 startup ACL grant),
|
||
* parses `ORCA_STARTUP_DIAGNOSTICS=1` milestone lines from stderr, and
|
||
* reports per-phase timings across iterations.
|
||
*
|
||
* Usage:
|
||
* node tests/tools/benchmarks/startup-time-bench.mjs --label baseline
|
||
* [--iterations 5] [--files 28000] [--fixture-dir <path>]
|
||
* [--state-profile none|restored-local-tabs] [--session-tabs 200]
|
||
* [--ssh-unreachable-targets 1]
|
||
* [--github-repos 3] [--gh-hang-ms 30000]
|
||
* [--wait-for-event renderer-startup-hydration-done]
|
||
* [--exe <path-to-packaged-Orca>] [--timeout-ms 240000]
|
||
*
|
||
* Issue #7225 freeze reproduction: `--github-repos N` seeds N git repos with
|
||
* GitHub remotes and no configured username, so repo hydration reaches the
|
||
* `gh` login probe; `--gh-hang-ms` puts a fake `gh` on PATH that hangs like a
|
||
* blackholed api.github.com. The child it spawns survives the probe's timeout
|
||
* kill while holding the inherited stdio pipe — the exact mechanism that
|
||
* turns a 2.5s execSync timeout into a minutes-long main-thread stall.
|
||
*
|
||
* Prereq (when not using --exe): `pnpm build:electron-vite` so out/ exists.
|
||
* Results: tests/tools/benchmarks/results/startup-<label>-<timestamp>.json
|
||
*/
|
||
import { spawn, spawnSync } from 'node:child_process'
|
||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||
import { createRequire } from 'node:module'
|
||
import os from 'node:os'
|
||
import { delimiter, join, resolve } from 'node:path'
|
||
import { writePersistedStateFixture } from './startup-bench-state-fixture.mjs'
|
||
|
||
const scriptDir = import.meta.dirname
|
||
const repoRoot = resolve(scriptDir, '..', '..', '..')
|
||
const require = createRequire(import.meta.url)
|
||
|
||
function parseArgs(argv) {
|
||
const args = {
|
||
label: 'run',
|
||
iterations: 5,
|
||
files: 28000,
|
||
fixtureDir: null,
|
||
exe: null,
|
||
timeoutMs: 240000,
|
||
stateProfile: 'none',
|
||
sessionTabs: 0,
|
||
// Seed TEST-NET-3 targets connected at shutdown so startup attempts reconnect, modeling one unreachable host delaying local terminals.
|
||
sshUnreachableTargets: 0,
|
||
githubRepos: 0,
|
||
ghHangMs: 0,
|
||
waitForEvent: 'did-finish-load',
|
||
// Linger after did-finish-load to capture trailing/background milestones such as async Windows ACL work.
|
||
lingerMs: 500
|
||
}
|
||
for (let i = 2; i < argv.length; i++) {
|
||
const next = () => argv[++i]
|
||
switch (argv[i]) {
|
||
case '--label':
|
||
args.label = next()
|
||
break
|
||
case '--iterations':
|
||
args.iterations = Number(next())
|
||
break
|
||
case '--files':
|
||
args.files = Number(next())
|
||
break
|
||
case '--fixture-dir':
|
||
args.fixtureDir = next()
|
||
break
|
||
case '--exe':
|
||
args.exe = next()
|
||
break
|
||
case '--timeout-ms':
|
||
args.timeoutMs = Number(next())
|
||
break
|
||
case '--state-profile':
|
||
args.stateProfile = next()
|
||
break
|
||
case '--session-tabs':
|
||
args.sessionTabs = Number(next())
|
||
break
|
||
case '--ssh-unreachable-targets':
|
||
args.sshUnreachableTargets = Number(next())
|
||
break
|
||
case '--github-repos':
|
||
args.githubRepos = Number(next())
|
||
break
|
||
case '--gh-hang-ms':
|
||
args.ghHangMs = Number(next())
|
||
break
|
||
case '--wait-for-event':
|
||
args.waitForEvent = next()
|
||
break
|
||
case '--linger-ms':
|
||
args.lingerMs = Number(next())
|
||
break
|
||
default:
|
||
throw new Error(`Unknown argument: ${argv[i]}`)
|
||
}
|
||
}
|
||
return args
|
||
}
|
||
|
||
// Seed tiny Chromium/Orca cache files; count/tree shape drives icacls walk cost, contents do not.
|
||
function ensureFixture(fixtureDir, options) {
|
||
const { fileCount, stateProfile, sessionTabs, githubRepos, sshUnreachableTargets } = options
|
||
const manifestPath = join(fixtureDir, 'bench-fixture-manifest.json')
|
||
if (existsSync(manifestPath)) {
|
||
try {
|
||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'))
|
||
if (
|
||
manifest.files === fileCount &&
|
||
manifest.stateProfile === stateProfile &&
|
||
manifest.sessionTabs === sessionTabs &&
|
||
(manifest.githubRepos ?? 0) === githubRepos &&
|
||
(manifest.sshUnreachableTargets ?? 0) === sshUnreachableTargets
|
||
) {
|
||
console.log(`[fixture] reusing ${fixtureDir} (${fileCount} files, state=${stateProfile})`)
|
||
return
|
||
}
|
||
} catch {
|
||
// fall through and rebuild
|
||
}
|
||
}
|
||
console.log(`[fixture] creating ${fixtureDir} with ~${fileCount} synthetic files…`)
|
||
const buckets = [
|
||
['Cache', 'Cache_Data'],
|
||
['Code Cache', 'js'],
|
||
['Code Cache', 'wasm'],
|
||
['GPUCache'],
|
||
['DawnGraphiteCache'],
|
||
['blob_storage', 'blobs'],
|
||
['Service Worker', 'CacheStorage'],
|
||
['terminal-scrollback-snapshots']
|
||
]
|
||
const payload = 'x'.repeat(1024)
|
||
let written = 0
|
||
const started = Date.now()
|
||
for (let b = 0; written < fileCount; b = (b + 1) % buckets.length) {
|
||
const dir = join(fixtureDir, ...buckets[b], `g${Math.floor(written / 512)}`)
|
||
mkdirSync(dir, { recursive: true })
|
||
const batch = Math.min(512, fileCount - written)
|
||
for (let i = 0; i < batch; i++) {
|
||
writeFileSync(join(dir, `f_${String(written + i).padStart(6, '0')}`), payload)
|
||
}
|
||
written += batch
|
||
}
|
||
const persistedStateBytes = writePersistedStateFixture(fixtureDir, {
|
||
stateProfile,
|
||
sessionTabs,
|
||
githubRepos,
|
||
sshUnreachableTargets
|
||
})
|
||
writeFileSync(
|
||
manifestPath,
|
||
JSON.stringify({
|
||
files: fileCount,
|
||
stateProfile,
|
||
sessionTabs,
|
||
githubRepos,
|
||
sshUnreachableTargets,
|
||
persistedStateBytes,
|
||
createdAt: Date.now()
|
||
})
|
||
)
|
||
console.log(`[fixture] done in ${((Date.now() - started) / 1000).toFixed(1)}s`)
|
||
}
|
||
|
||
// Hang fake gh in a child holding inherited stdio, reproducing blackholed API children outliving the parent’s execSync timeout.
|
||
function writeGhShim(fixtureDir, ghHangMs) {
|
||
if (!ghHangMs) {
|
||
return null
|
||
}
|
||
const shimDir = join(fixtureDir, 'gh-shim')
|
||
mkdirSync(shimDir, { recursive: true })
|
||
const hangSeconds = Math.max(1, Math.ceil(ghHangMs / 1000))
|
||
if (process.platform === 'win32') {
|
||
// ping -n K waits K-1 seconds between K probes of localhost.
|
||
writeFileSync(
|
||
join(shimDir, 'gh.cmd'),
|
||
`@echo off\r\nping -n ${hangSeconds + 1} 127.0.0.1\r\nexit /b 1\r\n`
|
||
)
|
||
} else {
|
||
const shimPath = join(shimDir, 'gh')
|
||
writeFileSync(shimPath, `#!/bin/sh\nsleep ${hangSeconds}\nexit 1\n`)
|
||
spawnSync('chmod', ['+x', shimPath], { stdio: 'ignore' })
|
||
}
|
||
return shimDir
|
||
}
|
||
|
||
function buildLaunchEnvironment({ fixtureDir, githubRepos, ghShimDir }) {
|
||
// Isolate Codex home and rollout migration work inside the fixture to keep timings/profile effects controlled.
|
||
const isolatedHome = join(fixtureDir, 'home')
|
||
mkdirSync(isolatedHome, { recursive: true })
|
||
const env = {
|
||
...process.env,
|
||
ORCA_STARTUP_DIAGNOSTICS: '1',
|
||
ORCA_E2E_USER_DATA_DIR: fixtureDir,
|
||
HOME: isolatedHome,
|
||
USERPROFILE: isolatedHome,
|
||
ORCA_E2E_HOME_DIR: isolatedHome,
|
||
ORCA_E2E_HEADLESS: '1'
|
||
}
|
||
delete env.CODEX_HOME
|
||
delete env.ORCA_CODEX_HOME
|
||
if (ghShimDir) {
|
||
env.PATH = `${ghShimDir}${delimiter}${env.PATH ?? ''}`
|
||
}
|
||
if (githubRepos > 0) {
|
||
// Keep the developer's real github.user/user.username out of the run so
|
||
// repo hydration deterministically falls through to the gh probe.
|
||
const emptyGitConfig = join(fixtureDir, 'bench-empty-gitconfig')
|
||
if (!existsSync(emptyGitConfig)) {
|
||
writeFileSync(emptyGitConfig, '')
|
||
}
|
||
env.GIT_CONFIG_GLOBAL = emptyGitConfig
|
||
env.GIT_CONFIG_NOSYSTEM = '1'
|
||
}
|
||
return env
|
||
}
|
||
|
||
function killProcessTree(proc) {
|
||
if (process.platform === 'win32') {
|
||
if (proc.exitCode === null && proc.signalCode === null) {
|
||
spawnSync('taskkill', ['/PID', String(proc.pid), '/T', '/F'], { stdio: 'ignore' })
|
||
}
|
||
return
|
||
}
|
||
// Kill the detached launcher group: surviving Electron helpers retain stderr and prevent EOF/harness exit.
|
||
try {
|
||
process.kill(-proc.pid, 'SIGKILL')
|
||
} catch {
|
||
try {
|
||
proc.kill('SIGKILL')
|
||
} catch {
|
||
// already gone
|
||
}
|
||
}
|
||
}
|
||
|
||
function parseStartupLine(line) {
|
||
const match = /^\[startup\] (\S+)(.*)$/.exec(line)
|
||
if (!match) {
|
||
return null
|
||
}
|
||
const details = {}
|
||
const detailText = match[2].trim()
|
||
if (detailText) {
|
||
for (const pair of detailText.match(/(\S+?)=("[^"]*"|\S+)/g) ?? []) {
|
||
const eq = pair.indexOf('=')
|
||
const key = pair.slice(0, eq)
|
||
let value = pair.slice(eq + 1)
|
||
try {
|
||
value = JSON.parse(value)
|
||
} catch {
|
||
// keep raw string
|
||
}
|
||
details[key] = value
|
||
}
|
||
}
|
||
return { event: match[1], details }
|
||
}
|
||
|
||
function runIteration({ exe, timeoutMs, lingerMs, waitForEvent, launchEnv }) {
|
||
return new Promise((resolvePromise) => {
|
||
// Resolve electron package’s platform executable; hardcoded electron.exe breaks macOS/Linux.
|
||
const command = exe ?? require('electron')
|
||
const commandArgs = exe ? [] : [repoRoot]
|
||
const events = []
|
||
const startedAt = process.hrtime.bigint()
|
||
const child = spawn(command, commandArgs, {
|
||
env: launchEnv,
|
||
stdio: ['ignore', 'ignore', 'pipe'],
|
||
// Own a process group so teardown can reap all Electron helpers.
|
||
detached: process.platform !== 'win32'
|
||
})
|
||
let finished = false
|
||
let buffer = ''
|
||
const finish = (outcome) => {
|
||
if (finished) {
|
||
return
|
||
}
|
||
finished = true
|
||
clearTimeout(timer)
|
||
// Linger for trailing diagnostics/background work, including async ACL grant.
|
||
setTimeout(() => {
|
||
killProcessTree(child)
|
||
resolvePromise({ outcome, events })
|
||
}, lingerMs)
|
||
}
|
||
const timer = setTimeout(() => finish('timeout'), timeoutMs)
|
||
child.stderr.setEncoding('utf-8')
|
||
child.stderr.on('data', (chunk) => {
|
||
buffer += chunk
|
||
let newlineIndex = buffer.indexOf('\n')
|
||
while (newlineIndex !== -1) {
|
||
const line = buffer.slice(0, newlineIndex).trimEnd()
|
||
buffer = buffer.slice(newlineIndex + 1)
|
||
newlineIndex = buffer.indexOf('\n')
|
||
const parsed = parseStartupLine(line)
|
||
if (!parsed) {
|
||
continue
|
||
}
|
||
const harnessMs = Number(process.hrtime.bigint() - startedAt) / 1e6
|
||
events.push({ ...parsed, harnessMs: Math.round(harnessMs * 10) / 10 })
|
||
if (parsed.event === waitForEvent) {
|
||
finish('ok')
|
||
}
|
||
}
|
||
})
|
||
child.on('exit', () => finish('early-exit'))
|
||
child.on('error', () => finish('spawn-error'))
|
||
})
|
||
}
|
||
|
||
function eventTime(events, name, key) {
|
||
const entry = events.find((event) => event.event === name)
|
||
if (!entry) {
|
||
return null
|
||
}
|
||
return key === 't'
|
||
? typeof entry.details.t === 'number'
|
||
? entry.details.t
|
||
: null
|
||
: entry.harnessMs
|
||
}
|
||
|
||
function derivePhases(events) {
|
||
const aclStart = eventTime(events, 'acl-grant-start', 't')
|
||
const aclDone = eventTime(events, 'acl-grant-done', 't')
|
||
return {
|
||
startupJsonParseMs: delta(
|
||
events,
|
||
'persistence-json-parse-start',
|
||
'persistence-json-parse-done'
|
||
),
|
||
startupStoreLoadMs: delta(events, 'persistence-load-start', 'persistence-load-done'),
|
||
spawnToAppReady: eventTime(events, 'app-ready', 'harness'),
|
||
appReadyToServices: delta(events, 'app-ready', 'services-initialized'),
|
||
servicesToI18n: delta(events, 'services-initialized', 'i18n-ready'),
|
||
i18nToOpenWindow: delta(events, 'i18n-ready', 'open-main-window-start'),
|
||
daemonInitMs: delta(events, 'daemon-init-start', 'daemon-init-done'),
|
||
aclGrantMs: aclStart !== null && aclDone !== null ? aclDone - aclStart : null,
|
||
windowCreatedToLoadStart: delta(events, 'window-created', 'load-start'),
|
||
windowCreatedToLoaded: delta(events, 'window-created', 'did-finish-load'),
|
||
totalToWindowCreated: eventTime(events, 'window-created', 'harness'),
|
||
totalToDidFinishLoad: eventTime(events, 'did-finish-load', 'harness'),
|
||
didFinishLoadToWorkspaceReady: delta(
|
||
events,
|
||
'did-finish-load',
|
||
'renderer-startup-hydration-done'
|
||
),
|
||
totalToWorkspaceReady: eventTime(events, 'renderer-startup-hydration-done', 'harness'),
|
||
// Measure terminal restoration’s SSH reconnect and former main hydration barrier.
|
||
rendererSshReconnectMs: eventDetailsNumber(events, 'renderer-ssh-reconnect-done', 'durationMs'),
|
||
rendererStartupBarrierMs:
|
||
eventDetailsNumber(events, 'renderer-git-environment-barrier-await-done', 'durationMs') ??
|
||
eventDetailsNumber(events, 'renderer-first-window-services-await-done', 'durationMs'),
|
||
rendererReconnectTerminalsMs:
|
||
eventDetailsNumber(events, 'renderer-reconnect-terminals-done', 'durationMs') ??
|
||
delta(
|
||
events,
|
||
'renderer-first-window-services-await-done',
|
||
'renderer-reconnect-terminals-done'
|
||
),
|
||
// Record worst event-loop stall as the direct #7225 freeze measurement.
|
||
maxEventLoopStallMs: maxEventDetailsNumber(events, 'event-loop-stall', 'maxGapMs')
|
||
}
|
||
}
|
||
|
||
function maxEventDetailsNumber(events, name, key) {
|
||
let max = null
|
||
for (const event of events) {
|
||
if (event.event !== name) {
|
||
continue
|
||
}
|
||
const value = event.details[key]
|
||
if (typeof value === 'number' && (max === null || value > max)) {
|
||
max = value
|
||
}
|
||
}
|
||
return max
|
||
}
|
||
|
||
function eventDetailsNumber(events, name, key) {
|
||
const value = events.find((event) => event.event === name)?.details[key]
|
||
return typeof value === 'number' ? value : null
|
||
}
|
||
|
||
function delta(events, from, to) {
|
||
const a = eventTime(events, from, 't')
|
||
const b = eventTime(events, to, 't')
|
||
return a !== null && b !== null ? b - a : null
|
||
}
|
||
|
||
function median(values) {
|
||
const usable = values.filter((value) => typeof value === 'number').sort((a, b) => a - b)
|
||
if (usable.length === 0) {
|
||
return null
|
||
}
|
||
const mid = Math.floor(usable.length / 2)
|
||
return usable.length % 2 ? usable[mid] : (usable[mid - 1] + usable[mid]) / 2
|
||
}
|
||
|
||
// Results are committed as PR evidence — keep home-anchored paths out of them.
|
||
function sanitizeLocalPath(value) {
|
||
if (typeof value !== 'string') {
|
||
return value
|
||
}
|
||
const home = os.homedir()
|
||
return value.startsWith(home) ? `~${value.slice(home.length)}` : value
|
||
}
|
||
|
||
function formatMs(value) {
|
||
if (value === null) {
|
||
return 'n/a'
|
||
}
|
||
return value >= 1000 ? `${(value / 1000).toFixed(2)}s` : `${Math.round(value)}ms`
|
||
}
|
||
|
||
async function main() {
|
||
const args = parseArgs(process.argv)
|
||
if (!['none', 'restored-local-tabs'].includes(args.stateProfile)) {
|
||
throw new Error(`Unknown state profile: ${args.stateProfile}`)
|
||
}
|
||
const fixtureDir = resolve(
|
||
args.fixtureDir ??
|
||
join(
|
||
os.tmpdir(),
|
||
'orca-startup-bench',
|
||
`userdata-${args.files}-${args.stateProfile}-${args.sessionTabs}-gh${args.githubRepos}-ssh${args.sshUnreachableTargets}`
|
||
)
|
||
)
|
||
mkdirSync(fixtureDir, { recursive: true })
|
||
ensureFixture(fixtureDir, {
|
||
fileCount: args.files,
|
||
stateProfile: args.stateProfile,
|
||
sessionTabs: args.sessionTabs,
|
||
githubRepos: args.githubRepos,
|
||
sshUnreachableTargets: args.sshUnreachableTargets
|
||
})
|
||
const ghShimDir = writeGhShim(fixtureDir, args.ghHangMs)
|
||
const launchEnv = buildLaunchEnvironment({
|
||
fixtureDir,
|
||
githubRepos: args.githubRepos,
|
||
ghShimDir
|
||
})
|
||
|
||
if (!args.exe && !existsSync(join(repoRoot, 'out', 'main', 'index.js'))) {
|
||
throw new Error('out/main/index.js missing — run `pnpm build:electron-vite` first')
|
||
}
|
||
|
||
const iterations = []
|
||
for (let i = 0; i < args.iterations; i++) {
|
||
process.stdout.write(`[bench] iteration ${i + 1}/${args.iterations}… `)
|
||
const result = await runIteration({
|
||
exe: args.exe,
|
||
timeoutMs: args.timeoutMs,
|
||
lingerMs: args.lingerMs,
|
||
waitForEvent: args.waitForEvent,
|
||
launchEnv
|
||
})
|
||
const phases = derivePhases(result.events)
|
||
iterations.push({ ...result, phases })
|
||
console.log(
|
||
`${result.outcome} total=${formatMs(phases.totalToDidFinishLoad)} acl=${formatMs(phases.aclGrantMs)} maxStall=${formatMs(phases.maxEventLoopStallMs)}`
|
||
)
|
||
// Let the OS settle between launches (process teardown, file handles).
|
||
await new Promise((resolveSleep) => setTimeout(resolveSleep, 1500))
|
||
}
|
||
|
||
const phaseNames = Object.keys(iterations[0]?.phases ?? {})
|
||
const summary = {}
|
||
for (const name of phaseNames) {
|
||
summary[name] = median(iterations.map((iteration) => iteration.phases[name]))
|
||
}
|
||
|
||
const resultsDir = join(scriptDir, 'results')
|
||
mkdirSync(resultsDir, { recursive: true })
|
||
const stamp = new Date().toISOString().replace(/[:.]/g, '-')
|
||
const outPath = join(resultsDir, `startup-${args.label}-${stamp}.json`)
|
||
writeFileSync(
|
||
outPath,
|
||
JSON.stringify(
|
||
{
|
||
label: args.label,
|
||
platform: process.platform,
|
||
arch: process.arch,
|
||
cpus: os.cpus()[0]?.model,
|
||
fixtureDir: sanitizeLocalPath(fixtureDir),
|
||
fixtureFiles: args.files,
|
||
stateProfile: args.stateProfile,
|
||
sessionTabs: args.sessionTabs,
|
||
githubRepos: args.githubRepos,
|
||
ghHangMs: args.ghHangMs,
|
||
waitForEvent: args.waitForEvent,
|
||
exe: sanitizeLocalPath(args.exe),
|
||
iterations,
|
||
summaryMedianMs: summary
|
||
},
|
||
null,
|
||
2
|
||
)
|
||
)
|
||
|
||
console.log(`\n[bench] label=${args.label} (medians over ${iterations.length} runs)`)
|
||
console.log('| phase | median |')
|
||
console.log('|---|---|')
|
||
for (const name of phaseNames) {
|
||
console.log(`| ${name} | ${formatMs(summary[name])} |`)
|
||
}
|
||
console.log(`\n[bench] results written to ${outPath}`)
|
||
}
|
||
|
||
main().catch((error) => {
|
||
console.error(error)
|
||
process.exit(1)
|
||
})
|