mirror of
https://github.com/stablyai/orca.git
synced 2026-11-06 03:57:35 +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>
612 lines
22 KiB
JavaScript
612 lines
22 KiB
JavaScript
#!/usr/bin/env node
|
|
// Measure real dev-app tab creation/switch and workspace switch over CDP; report median/p95 phases for store, PTY spawn, shell and renderer attribution.
|
|
|
|
import { execFileSync } from 'node:child_process'
|
|
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
import process from 'node:process'
|
|
import { fileURLToPath } from 'node:url'
|
|
import {
|
|
collectRendererDiagnostics,
|
|
connectToApp,
|
|
createGpuUserDataDirectory,
|
|
installRendererProbe,
|
|
launchDevApp,
|
|
pickFreePort,
|
|
stopDevApp,
|
|
waitForStoreReady
|
|
} from '../../../config/scripts/windows-apphang-repro/electron-dev-session.mjs'
|
|
import {
|
|
pollUntil,
|
|
rendererActionTimeoutMs,
|
|
runWithTimeout,
|
|
setupTimeoutMs
|
|
} from '../../../config/scripts/windows-apphang-repro/repro-timing.mjs'
|
|
import { safeRemoveLocalDirectory } from '../../../config/scripts/windows-apphang-repro/wsl-workspace-fixture.mjs'
|
|
|
|
const rootDir = path.resolve(fileURLToPath(new URL('../../..', import.meta.url)))
|
|
const scenarioTimeoutMs = 300_000
|
|
const defaultIterations = 8
|
|
const defaultSwitches = 24
|
|
const defaultCycles = 12
|
|
|
|
function parseArgs() {
|
|
const args = {
|
|
label: 'run',
|
|
iterations: defaultIterations,
|
|
switches: defaultSwitches,
|
|
cycles: defaultCycles,
|
|
shell: null,
|
|
scenarios: ['tab-create', 'tab-switch', 'workspace-switch'],
|
|
reportPath: null,
|
|
keep: false
|
|
}
|
|
for (const arg of process.argv.slice(2)) {
|
|
if (arg === '--help' || arg === '-h') {
|
|
printHelp()
|
|
process.exit(0)
|
|
}
|
|
if (arg === '--keep') {
|
|
args.keep = true
|
|
continue
|
|
}
|
|
const [name, value] = arg.split('=', 2)
|
|
if (name === '--label') {
|
|
args.label = value?.trim() || 'run'
|
|
continue
|
|
}
|
|
if (name === '--iterations') {
|
|
args.iterations = parsePositiveInt(name, value)
|
|
continue
|
|
}
|
|
if (name === '--switches') {
|
|
args.switches = parsePositiveInt(name, value)
|
|
continue
|
|
}
|
|
if (name === '--cycles') {
|
|
args.cycles = parsePositiveInt(name, value)
|
|
continue
|
|
}
|
|
if (name === '--shell') {
|
|
args.shell = value?.trim() || null
|
|
continue
|
|
}
|
|
if (name === '--scenarios') {
|
|
const scenarios = (value ?? '')
|
|
.split(',')
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean)
|
|
const known = new Set(['tab-create', 'tab-switch', 'workspace-switch'])
|
|
if (scenarios.length === 0 || scenarios.some((scenario) => !known.has(scenario))) {
|
|
throw new Error(
|
|
`Unsupported --scenarios=${value}. Use tab-create,tab-switch,workspace-switch.`
|
|
)
|
|
}
|
|
args.scenarios = scenarios
|
|
continue
|
|
}
|
|
if (name === '--report') {
|
|
args.reportPath = value?.trim() || null
|
|
continue
|
|
}
|
|
throw new Error(`Unknown argument: ${arg}`)
|
|
}
|
|
return args
|
|
}
|
|
|
|
function printHelp() {
|
|
console.log(`Usage:
|
|
node tests/tools/benchmarks/terminal-perf-bench.mjs [options]
|
|
|
|
Options:
|
|
--label=NAME Label recorded in the report filename/JSON. Default: run.
|
|
--iterations=N Terminal tab create/close iterations. Default: ${defaultIterations}.
|
|
--switches=N Tab switch alternations. Default: ${defaultSwitches}.
|
|
--cycles=N Workspace switch cycles. Default: ${defaultCycles}.
|
|
--shell=PATH Shell override for created tabs (e.g. cmd.exe). Default: app default.
|
|
--scenarios=a,b Subset of tab-create,tab-switch,workspace-switch.
|
|
--report=PATH Report JSON path. Default: tests/tools/benchmarks/results/terminal-perf-<label>-<ts>.json.
|
|
--keep Keep the temp fixture and user data dir.`)
|
|
}
|
|
|
|
function parsePositiveInt(name, value) {
|
|
const parsed = Number.parseInt(value ?? '', 10)
|
|
if (!Number.isInteger(parsed) || parsed <= 0 || String(parsed) !== value) {
|
|
throw new Error(`${name} requires a positive integer.`)
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
function git(cwd, ...cmd) {
|
|
execFileSync('git', cmd, { cwd, stdio: 'pipe' })
|
|
}
|
|
|
|
// Use native local repo plus two external worktrees for portable default-workflow coverage rather than the WSL apphang fixture.
|
|
function createLocalRepoFixture() {
|
|
const baseDir = mkdtempSync(path.join(os.tmpdir(), 'orca-termperf-'))
|
|
const repoPath = path.join(baseDir, 'repo')
|
|
mkdirSync(repoPath, { recursive: true })
|
|
git(repoPath, 'init', '--initial-branch=main')
|
|
git(repoPath, 'config', 'user.email', 'bench@orca.local')
|
|
git(repoPath, 'config', 'user.name', 'Orca Bench')
|
|
writeFileSync(path.join(repoPath, 'README.md'), '# terminal perf fixture\n')
|
|
git(repoPath, 'add', '.')
|
|
git(repoPath, 'commit', '-m', 'init', '--no-gpg-sign')
|
|
const worktreePaths = []
|
|
for (const name of ['wt-one', 'wt-two']) {
|
|
const worktreePath = path.join(baseDir, name)
|
|
git(repoPath, 'worktree', 'add', worktreePath, '-b', name)
|
|
worktreePaths.push(worktreePath)
|
|
}
|
|
return { baseDir, repoPath, worktreePaths }
|
|
}
|
|
|
|
async function setupWorkspaces(page, fixture) {
|
|
return await runWithTimeout(
|
|
'fixture registration in Orca',
|
|
() =>
|
|
page.evaluate(
|
|
async ({ repoPath, importedWorktreePaths }) => {
|
|
const store = window.__store
|
|
if (!store) {
|
|
throw new Error('window.__store is unavailable.')
|
|
}
|
|
await store.getState().fetchSettings?.()
|
|
const addResult = await window.api.repos.add({ path: repoPath, kind: 'git' })
|
|
if ('error' in addResult) {
|
|
throw new Error(addResult.error)
|
|
}
|
|
const owner = store.getState().settings?.activeRuntimeEnvironmentId ?? null
|
|
await store.getState().fetchRepos({ runtimeEnvironmentId: owner })
|
|
const state = store.getState()
|
|
const repo =
|
|
state.repos.find((candidate) => candidate.path === repoPath) ?? addResult.repo
|
|
await state.updateRepo(repo.id, {
|
|
externalWorktreeVisibility: 'show',
|
|
externalWorktreeVisibilityPromptDismissedAt: Date.now(),
|
|
importedExternalWorktreePaths: importedWorktreePaths,
|
|
externalWorktreeInboxBaselinePaths: importedWorktreePaths
|
|
})
|
|
await store.getState().fetchWorktrees(repo.id, { requireAuthoritative: true })
|
|
const nextState = store.getState()
|
|
nextState.setSidebarOpen(true)
|
|
nextState.setGroupBy('none')
|
|
nextState.setSortBy('recent')
|
|
nextState.setShowActiveOnly(false)
|
|
nextState.setActiveView('terminal')
|
|
const worktrees = nextState.worktreesByRepo[repo.id] ?? []
|
|
return {
|
|
repoId: repo.id,
|
|
worktrees: worktrees.map((worktree) => ({
|
|
id: worktree.id,
|
|
path: worktree.path,
|
|
displayName: worktree.displayName,
|
|
isMainWorktree: worktree.isMainWorktree
|
|
}))
|
|
}
|
|
},
|
|
{ repoPath: fixture.repoPath, importedWorktreePaths: fixture.worktreePaths }
|
|
),
|
|
setupTimeoutMs
|
|
)
|
|
}
|
|
|
|
async function clickWorktreeCard(page, worktreeId) {
|
|
const rect = await runWithTimeout(
|
|
`locate worktree card ${worktreeId}`,
|
|
() =>
|
|
page.evaluate((id) => {
|
|
const rows = Array.from(document.querySelectorAll('[data-worktree-id]'))
|
|
const row = rows.find((candidate) => candidate.getAttribute('data-worktree-id') === id)
|
|
if (!row) {
|
|
return null
|
|
}
|
|
row.scrollIntoView({ block: 'center', inline: 'nearest' })
|
|
const surface = row.querySelector('[data-worktree-card-surface="true"]') ?? row
|
|
const bounds = surface.getBoundingClientRect()
|
|
if (bounds.width <= 0 || bounds.height <= 0) {
|
|
return null
|
|
}
|
|
return { x: bounds.left + bounds.width / 2, y: bounds.top + bounds.height / 2 }
|
|
}, worktreeId),
|
|
rendererActionTimeoutMs
|
|
)
|
|
if (!rect) {
|
|
throw new Error(`Could not find rendered worktree card for ${worktreeId}`)
|
|
}
|
|
await runWithTimeout(
|
|
`click worktree card ${worktreeId}`,
|
|
() => page.mouse.click(rect.x, rect.y),
|
|
rendererActionTimeoutMs
|
|
)
|
|
}
|
|
|
|
async function activateWorktree(page, worktreeId) {
|
|
await clickWorktreeCard(page, worktreeId)
|
|
await pollUntil(
|
|
`active worktree ${worktreeId}`,
|
|
() =>
|
|
page.evaluate((id) => {
|
|
const state = window.__store?.getState?.()
|
|
return state?.activeWorktreeId === id && state.activeTabType === 'terminal'
|
|
}, worktreeId),
|
|
Boolean,
|
|
30_000
|
|
)
|
|
}
|
|
|
|
// Measure phases in-renderer on one performance.now clock, avoiding CDP round-trip skew.
|
|
async function runTabCreateScenario(page, worktreeId, iterations, shell) {
|
|
return await runWithTimeout(
|
|
'tab-create scenario',
|
|
() =>
|
|
page.evaluate(
|
|
async ({ worktreeId, iterations, shell }) => {
|
|
const store = window.__store
|
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
const raf2 = () =>
|
|
new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
|
const longtasks = []
|
|
const observer = new PerformanceObserver((list) => {
|
|
for (const entry of list.getEntries()) {
|
|
longtasks.push(entry.duration)
|
|
}
|
|
})
|
|
observer.observe({ entryTypes: ['longtask'] })
|
|
const samples = []
|
|
for (let index = 0; index < iterations; index++) {
|
|
const t0 = performance.now()
|
|
const tab = store.getState().createTab(worktreeId, undefined, shell ?? undefined)
|
|
const tCreated = performance.now()
|
|
const readPane = () => {
|
|
const manager = window.__paneManagers?.get(tab.id)
|
|
return manager?.getActivePane?.() ?? manager?.getPanes?.()?.[0] ?? null
|
|
}
|
|
let ptyId = null
|
|
while (!ptyId && performance.now() - t0 < 60_000) {
|
|
ptyId = readPane()?.container?.dataset?.ptyId ?? null
|
|
if (!ptyId) {
|
|
await sleep(5)
|
|
}
|
|
}
|
|
const tPty = performance.now()
|
|
let sawOutput = false
|
|
while (!sawOutput && performance.now() - t0 < 60_000) {
|
|
const content = readPane()?.serializeAddon?.serialize?.() ?? ''
|
|
sawOutput = content.trim().length > 0
|
|
if (!sawOutput) {
|
|
await sleep(10)
|
|
}
|
|
}
|
|
const tOutput = performance.now()
|
|
await raf2()
|
|
const tPainted = performance.now()
|
|
samples.push({
|
|
index,
|
|
storeCreateMs: tCreated - t0,
|
|
ptyBindMs: tPty - t0,
|
|
firstOutputMs: tOutput - t0,
|
|
paintSettleMs: tPainted - t0,
|
|
timedOut: !ptyId || !sawOutput
|
|
})
|
|
store.getState().closeTab(tab.id)
|
|
await sleep(400)
|
|
}
|
|
observer.disconnect()
|
|
return { samples, longtasks }
|
|
},
|
|
{ worktreeId, iterations, shell }
|
|
),
|
|
scenarioTimeoutMs
|
|
)
|
|
}
|
|
|
|
// Pre-bind K tabs, alternate activation, and measure active pane availability plus two-rAF settle (not pixel proof).
|
|
async function runTabSwitchScenario(page, worktreeId, switches, shell) {
|
|
return await runWithTimeout(
|
|
'tab-switch scenario',
|
|
() =>
|
|
page.evaluate(
|
|
async ({ worktreeId, switches, shell }) => {
|
|
const store = window.__store
|
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
const raf2 = () =>
|
|
new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
|
const tabCount = 4
|
|
const tabs = []
|
|
for (let index = 0; index < tabCount; index++) {
|
|
const tab = store.getState().createTab(worktreeId, undefined, shell ?? undefined)
|
|
tabs.push(tab.id)
|
|
const started = performance.now()
|
|
let bound = false
|
|
while (!bound && performance.now() - started < 60_000) {
|
|
const manager = window.__paneManagers?.get(tab.id)
|
|
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()?.[0] ?? null
|
|
bound = Boolean(pane?.container?.dataset?.ptyId)
|
|
if (!bound) {
|
|
await sleep(10)
|
|
}
|
|
}
|
|
}
|
|
await sleep(500)
|
|
const longtasks = []
|
|
const observer = new PerformanceObserver((list) => {
|
|
for (const entry of list.getEntries()) {
|
|
longtasks.push(entry.duration)
|
|
}
|
|
})
|
|
observer.observe({ entryTypes: ['longtask'] })
|
|
const samples = []
|
|
for (let index = 0; index < switches; index++) {
|
|
const targetTabId = tabs[index % tabs.length]
|
|
if (store.getState().activeTabId === targetTabId) {
|
|
continue
|
|
}
|
|
const t0 = performance.now()
|
|
store.getState().setActiveTab(targetTabId)
|
|
const tStore = performance.now()
|
|
let visible = false
|
|
while (!visible && performance.now() - t0 < 10_000) {
|
|
const manager = window.__paneManagers?.get(targetTabId)
|
|
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()?.[0] ?? null
|
|
visible = Boolean(pane?.container?.isConnected)
|
|
if (!visible) {
|
|
await sleep(2)
|
|
}
|
|
}
|
|
const tVisible = performance.now()
|
|
await raf2()
|
|
const tPainted = performance.now()
|
|
samples.push({
|
|
index,
|
|
targetTabId,
|
|
storeMs: tStore - t0,
|
|
paneVisibleMs: tVisible - t0,
|
|
paintSettleMs: tPainted - t0,
|
|
timedOut: !visible
|
|
})
|
|
await sleep(120)
|
|
}
|
|
observer.disconnect()
|
|
for (const tabId of tabs) {
|
|
store.getState().closeTab(tabId)
|
|
}
|
|
return { samples, longtasks }
|
|
},
|
|
{ worktreeId, switches, shell }
|
|
),
|
|
scenarioTimeoutMs
|
|
)
|
|
}
|
|
|
|
async function runWorkspaceSwitchScenario(page, worktreeIds, cycles) {
|
|
const samples = []
|
|
for (let index = 0; index < cycles; index++) {
|
|
const worktreeId = worktreeIds[index % worktreeIds.length]
|
|
const t0 = Date.now()
|
|
await clickWorktreeCard(page, worktreeId)
|
|
await pollUntil(
|
|
`workspace activation ${worktreeId}`,
|
|
() =>
|
|
page.evaluate((id) => {
|
|
const state = window.__store?.getState?.()
|
|
return state?.activeWorktreeId === id && state.activeTabType === 'terminal'
|
|
}, worktreeId),
|
|
Boolean,
|
|
30_000,
|
|
10
|
|
)
|
|
const activationMs = Date.now() - t0
|
|
const pty = await pollUntil(
|
|
`workspace pty ${worktreeId}`,
|
|
() =>
|
|
page.evaluate((id) => {
|
|
const state = window.__store?.getState?.()
|
|
const tabId =
|
|
state?.activeWorktreeId === id && state.activeTabType === 'terminal'
|
|
? state.activeTabId
|
|
: (state?.activeTabIdByWorktree?.[id] ?? null)
|
|
const manager = tabId ? window.__paneManagers?.get(tabId) : null
|
|
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()?.[0] ?? null
|
|
return pane?.container?.dataset?.ptyId ?? null
|
|
}, worktreeId),
|
|
Boolean,
|
|
30_000,
|
|
10
|
|
)
|
|
const ptyBindMs = Date.now() - t0
|
|
await runWithTimeout(
|
|
'paint settle',
|
|
() =>
|
|
page.evaluate(
|
|
() =>
|
|
new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
|
),
|
|
rendererActionTimeoutMs
|
|
)
|
|
const paintSettleMs = Date.now() - t0
|
|
samples.push({ index, worktreeId, ptyId: pty, activationMs, ptyBindMs, paintSettleMs })
|
|
}
|
|
return { samples }
|
|
}
|
|
|
|
function summarize(values) {
|
|
const sorted = values.filter((value) => Number.isFinite(value)).sort((a, b) => a - b)
|
|
if (sorted.length === 0) {
|
|
return null
|
|
}
|
|
const at = (fraction) =>
|
|
sorted[Math.min(sorted.length - 1, Math.round(fraction * (sorted.length - 1)))]
|
|
return {
|
|
count: sorted.length,
|
|
median: Math.round(at(0.5)),
|
|
p95: Math.round(at(0.95)),
|
|
max: Math.round(sorted.at(-1))
|
|
}
|
|
}
|
|
|
|
function summarizeScenario(name, result) {
|
|
if (!result) {
|
|
return null
|
|
}
|
|
const fields = {}
|
|
const keys = new Set()
|
|
// Exclude timeout ceilings from latency percentiles; report timedOut separately.
|
|
const completedSamples = result.samples.filter((sample) => !sample.timedOut)
|
|
for (const sample of completedSamples) {
|
|
for (const [key, value] of Object.entries(sample)) {
|
|
if (typeof value === 'number' && key !== 'index') {
|
|
keys.add(key)
|
|
}
|
|
}
|
|
}
|
|
for (const key of keys) {
|
|
fields[key] = summarize(completedSamples.map((sample) => sample[key]))
|
|
}
|
|
const longtasks = result.longtasks ?? []
|
|
return {
|
|
name,
|
|
samples: result.samples.length,
|
|
timedOut: result.samples.filter((sample) => sample.timedOut).length,
|
|
fields,
|
|
longtaskCount: longtasks.length,
|
|
longtaskMaxMs: longtasks.length ? Math.round(Math.max(...longtasks)) : 0
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs()
|
|
const startedAt = Date.now()
|
|
// Create report/clocks before setup so launch failures still report and clean up.
|
|
const report = {
|
|
label: args.label,
|
|
startedAt: new Date(startedAt).toISOString(),
|
|
platform: `${process.platform} ${os.release()}`,
|
|
shell: args.shell ?? 'default',
|
|
args: { iterations: args.iterations, switches: args.switches, cycles: args.cycles },
|
|
scenarios: {},
|
|
summaries: [],
|
|
finalDiagnostics: null,
|
|
cleanupErrors: []
|
|
}
|
|
let fixture = null
|
|
let userDataDir = null
|
|
let launched = null
|
|
let browser = null
|
|
let page = null
|
|
try {
|
|
fixture = createLocalRepoFixture()
|
|
const cdpPort = await pickFreePort()
|
|
userDataDir = createGpuUserDataDirectory('bench')
|
|
console.log(`[terminal-perf] fixture=${fixture.baseDir} userData=${userDataDir} cdp=${cdpPort}`)
|
|
launched = launchDevApp({ cdpPort, userDataDir })
|
|
const connected = await connectToApp(cdpPort)
|
|
browser = connected.browser
|
|
page = connected.page
|
|
await waitForStoreReady(page)
|
|
await installRendererProbe(page)
|
|
const setup = await setupWorkspaces(page, fixture)
|
|
const worktrees = setup.worktrees
|
|
if (worktrees.length < 2) {
|
|
throw new Error(`Expected >=2 worktrees, got ${worktrees.length}`)
|
|
}
|
|
const primary = worktrees.find((worktree) => worktree.isMainWorktree) ?? worktrees[0]
|
|
await activateWorktree(page, primary.id)
|
|
|
|
if (args.scenarios.includes('tab-create')) {
|
|
console.log(`[terminal-perf] scenario=tab-create iterations=${args.iterations}`)
|
|
report.scenarios['tab-create'] = await runTabCreateScenario(
|
|
page,
|
|
primary.id,
|
|
args.iterations,
|
|
args.shell
|
|
)
|
|
}
|
|
if (args.scenarios.includes('tab-switch')) {
|
|
console.log(`[terminal-perf] scenario=tab-switch switches=${args.switches}`)
|
|
report.scenarios['tab-switch'] = await runTabSwitchScenario(
|
|
page,
|
|
primary.id,
|
|
args.switches,
|
|
args.shell
|
|
)
|
|
}
|
|
if (args.scenarios.includes('workspace-switch')) {
|
|
console.log(`[terminal-perf] scenario=workspace-switch cycles=${args.cycles}`)
|
|
const targets = worktrees.slice(0, 3).map((worktree) => worktree.id)
|
|
report.scenarios['workspace-switch'] = await runWorkspaceSwitchScenario(
|
|
page,
|
|
targets,
|
|
args.cycles
|
|
)
|
|
}
|
|
report.finalDiagnostics = await collectRendererDiagnostics(page)
|
|
} finally {
|
|
report.elapsedMs = Date.now() - startedAt
|
|
for (const [name, result] of Object.entries(report.scenarios)) {
|
|
const summary = summarizeScenario(name, result)
|
|
if (summary) {
|
|
report.summaries.push(summary)
|
|
}
|
|
}
|
|
if (browser) {
|
|
await browser.close().catch(() => undefined)
|
|
}
|
|
if (launched) {
|
|
// Continue teardown after shutdown failure so reports and fixture cleanup still run.
|
|
try {
|
|
await stopDevApp(launched.child)
|
|
} catch (error) {
|
|
report.cleanupErrors.push(error instanceof Error ? error.message : String(error))
|
|
}
|
|
report.appLogsTail = launched.logs.slice(-80)
|
|
// Main-process phase attribution (requires ORCA_PTY_SPAWN_TIMING=1 in the
|
|
// benchmark's environment; launchDevApp inherits it into the app).
|
|
report.ptySpawnTimings = launched.logs
|
|
.filter((entry) => entry.line.includes('[pty-spawn-timing]'))
|
|
.map((entry) => entry.line.slice(entry.line.indexOf('[pty-spawn-timing]')))
|
|
}
|
|
if (!args.keep) {
|
|
if (fixture) {
|
|
safeRemoveLocalDirectory(fixture.baseDir, report.cleanupErrors)
|
|
}
|
|
if (userDataDir) {
|
|
safeRemoveLocalDirectory(userDataDir, report.cleanupErrors)
|
|
}
|
|
}
|
|
const stamp = new Date(startedAt).toISOString().replace(/[:.]/g, '-')
|
|
const reportPath = path.resolve(
|
|
args.reportPath ??
|
|
path.join(
|
|
rootDir,
|
|
'tests',
|
|
'tools',
|
|
'benchmarks',
|
|
'results',
|
|
`terminal-perf-${args.label}-${stamp}.json`
|
|
)
|
|
)
|
|
mkdirSync(path.dirname(reportPath), { recursive: true })
|
|
writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`)
|
|
console.log(`[terminal-perf] report=${reportPath}`)
|
|
for (const summary of report.summaries) {
|
|
console.log(
|
|
`[terminal-perf] ${summary.name}: samples=${summary.samples} timedOut=${summary.timedOut} longtasks=${summary.longtaskCount} (max ${summary.longtaskMaxMs}ms)`
|
|
)
|
|
for (const [field, stats] of Object.entries(summary.fields)) {
|
|
if (stats) {
|
|
console.log(
|
|
`[terminal-perf] ${field}: median=${stats.median}ms p95=${stats.p95}ms max=${stats.max}ms`
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error instanceof Error ? error.stack || error.message : String(error))
|
|
process.exit(1)
|
|
})
|