Files
orca/tests/tools/benchmarks/workspace-switch-paint-latency.mjs
OrcaWinandOrcaWin 7d6603aa17 [comment audit] Complete repository-wide comment cleanup (#33249)
* [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>
2026-10-25 04:05:46 -07:00

369 lines
12 KiB
JavaScript

#!/usr/bin/env node
// Measure click→first animation frame plus frame gaps: the synchronous active-card attribute can flip in ~1ms while activation blocks later frames.
// This frame proxy is distinct from pixel proof. Attach to an isolated dev app via CDP:
// ORCA_BACKGROUND_LAUNCH=1 ORCA_DEV_USER_DATA_PATH=... REMOTE_DEBUGGING_PORT=9455 pn dev
// node tests/tools/benchmarks/workspace-switch-paint-latency.mjs --port 9455
// Exceeding median --budget (default 200ms) exits nonzero for git bisect run.
import { writeFileSync, mkdirSync } from 'node:fs'
import path from 'node:path'
import { chromium } from '@stablyai/playwright-test'
function parseArgs(argv) {
const args = {
port: 9455,
switches: 6,
budgetMs: 200,
settleMs: 2500,
out: null,
json: false
}
for (let i = 0; i < argv.length; i += 1) {
const [flag, inlineValue] = argv[i].split('=')
const value = inlineValue ?? argv[i + 1]
const consume = () => {
if (inlineValue === undefined) {
i += 1
}
}
switch (flag) {
case '--port':
args.port = Number(value)
consume()
break
case '--switches':
args.switches = Number(value)
consume()
break
case '--budget':
args.budgetMs = Number(value)
consume()
break
case '--settle':
args.settleMs = Number(value)
consume()
break
case '--out':
args.out = value
consume()
break
case '--json':
args.json = true
break
case '--help':
console.log(
'Usage: workspace-switch-paint-latency.mjs [--port 9455] [--switches 6] [--budget 200] [--settle 2500] [--out file.json] [--json]'
)
process.exit(0)
break
default:
break
}
}
return args
}
const args = parseArgs(process.argv.slice(2))
function quantile(sorted, q) {
if (sorted.length === 0) {
return null
}
const pos = (sorted.length - 1) * q
const lo = Math.floor(pos)
const hi = Math.ceil(pos)
if (lo === hi) {
return sorted[lo]
}
return sorted[lo] + (sorted[hi] - sorted[lo]) * (pos - lo)
}
function summarize(values) {
const clean = values.filter((v) => typeof v === 'number' && Number.isFinite(v))
if (clean.length === 0) {
return null
}
const sorted = [...clean].sort((a, b) => a - b)
return {
n: sorted.length,
min: +sorted[0].toFixed(1),
p50: +quantile(sorted, 0.5).toFixed(1),
p90: +quantile(sorted, 0.9).toFixed(1),
max: +sorted.at(-1).toFixed(1)
}
}
// Record frames/long tasks from real pointerdown to distinguish blocked frames from slow-but-smooth work.
const PROBE_SOURCE = `(targetId) => {
const probe = {
t0: null,
targetId,
frames: [],
longTasks: [],
attrFlipMs: null,
storeCommitMs: null,
renderedCommitMs: null,
done: false
}
window.__switchProbe = probe
const surfaceOf = (id) => {
const row = [...document.querySelectorAll('[data-worktree-id]')].find(
(el) => el.dataset.worktreeId === id
)
return row ? row.querySelector('[data-worktree-card-surface]') : null
}
const isActive = (id) => {
const s = surfaceOf(id)
return Boolean(s && s.getAttribute('data-worktree-card-active'))
}
const renderedId = () =>
document
.querySelector('[data-rendered-active-worktree-id]')
?.getAttribute('data-rendered-active-worktree-id') ?? null
try {
const po = new PerformanceObserver((list) => {
for (const e of list.getEntries()) {
if (probe.t0 === null) continue
probe.longTasks.push({
startMs: +(e.startTime - probe.t0).toFixed(1),
durationMs: +e.duration.toFixed(1)
})
}
})
po.observe({ entryTypes: ['longtask'] })
probe.observer = po
} catch {}
// Why: pointerdown is the earliest thing the user's click produces, so it is
// the honest zero point for "time until I saw something happen".
const onPointerDown = () => {
if (probe.t0 !== null) return
probe.t0 = performance.now()
const tick = () => {
const now = performance.now() - probe.t0
probe.frames.push(+now.toFixed(1))
if (probe.attrFlipMs === null && isActive(probe.targetId)) probe.attrFlipMs = +now.toFixed(1)
if (probe.storeCommitMs === null) {
const st = window.__store?.getState?.()
if (st && st.activeWorktreeId === probe.targetId) probe.storeCommitMs = +now.toFixed(1)
}
if (probe.renderedCommitMs === null && renderedId() === probe.targetId) {
probe.renderedCommitMs = +now.toFixed(1)
}
if (!probe.done) requestAnimationFrame(tick)
}
requestAnimationFrame(tick)
}
window.addEventListener('pointerdown', onPointerDown, { capture: true, once: true })
probe.cleanup = () => {
probe.done = true
try { probe.observer?.disconnect() } catch {}
window.removeEventListener('pointerdown', onPointerDown, { capture: true })
}
return true
}`
/** Press Escape while any full-screen overlay is intercepting pointer events. */
async function dismissOverlays(page) {
for (let attempt = 0; attempt < 3; attempt += 1) {
const blocked = await page.evaluate(() =>
[...document.querySelectorAll('body *')].some((el) => {
const r = el.getBoundingClientRect()
if (r.width < window.innerWidth * 0.9 || r.height < window.innerHeight * 0.9) {
return false
}
const s = getComputedStyle(el)
return s.position === 'fixed' && s.pointerEvents !== 'none' && Number(s.zIndex) >= 40
})
)
if (!blocked) {
return
}
await page.keyboard.press('Escape')
await page.waitForTimeout(200)
}
}
async function main() {
const browser = await chromium.connectOverCDP(`http://127.0.0.1:${args.port}`)
const contexts = browser.contexts()
const pages = contexts.flatMap((c) => c.pages())
let page = null
for (const candidate of pages) {
const hasStore = await candidate
.evaluate(
() => Boolean(window.__store) && Boolean(document.querySelector('[data-worktree-id]'))
)
.catch(() => false)
if (hasStore) {
page = candidate
break
}
}
if (!page) {
throw new Error(
'No renderer page with window.__store + a mounted sidebar. Is the app past startup, and is the sidebar open?'
)
}
// Pin sidebar sort: Recent reorders after activation and can move the target under the cursor.
const worktreeIds = await page.evaluate(() => {
const state = window.__store.getState()
state.setActiveView('terminal')
state.setSidebarOpen(true)
state.setGroupBy?.('none')
state.setSortBy?.('name')
state.setShowActiveOnly?.(false)
state.setFilterRepoIds?.([])
const viewportH = window.innerHeight
// Use fully in-viewport virtualized cards; other rows may be unmounted or positioned outside.
return [...document.querySelectorAll('[data-worktree-id]')]
.filter((el) => {
const surface = el.querySelector('[data-worktree-card-surface]')
if (!surface) {
return false
}
const r = surface.getBoundingClientRect()
return r.height > 0 && r.top >= 0 && r.bottom <= viewportH
})
.map((el) => el.dataset.worktreeId)
.filter((id, i, all) => id && all.indexOf(id) === i)
})
if (worktreeIds.length < 2) {
throw new Error(
`Need >=2 workspace cards visible in the viewport, found ${worktreeIds.length}. Is the sidebar open?`
)
}
// Alternate two initially inactive cards; clicking active short-circuits without measuring activation.
const activeAtStart = await page.evaluate(() => window.__store.getState().activeWorktreeId)
const pair = worktreeIds.filter((id) => id !== activeAtStart).slice(0, 2)
if (pair.length < 2) {
throw new Error('Need two visible non-active cards to alternate between')
}
console.error(`alternating between:\n ${pair[0]}\n ${pair[1]}`)
const samples = []
for (let i = 0; i < args.switches; i += 1) {
const targetId = pair[i % 2]
// Inline the probe because page.evaluate treats a string as an expression and drops its argument.
const surface = page
.locator(`[data-worktree-id="${targetId}"] [data-worktree-card-surface]`)
.first()
// Dismiss transient full-screen popovers so the measured click reaches its card.
await dismissOverlays(page)
await page.evaluate(`(${PROBE_SOURCE})(${JSON.stringify(targetId)})`)
// Use locator CDP input to exercise drag suppression and re-resolve virtualized position at click time.
try {
await surface.click({ timeout: 10_000 })
} catch (error) {
console.error(`skip switch ${i + 1}: ${String(error).split('\n')[0]}`)
continue
}
await page.waitForTimeout(args.settleMs)
const sample = await page.evaluate(() => {
const p = window.__switchProbe
p.cleanup?.()
const frames = p.frames
let maxGap = 0
let prev = 0
for (const f of frames) {
maxGap = Math.max(maxGap, f - prev)
prev = f
}
return {
firstPaintMs: frames.length ? frames[0] : null,
maxFrameGapMs: +maxGap.toFixed(1),
frameCount: frames.length,
attrFlipMs: p.attrFlipMs,
storeCommitMs: p.storeCommitMs,
renderedCommitMs: p.renderedCommitMs,
longTaskCount: p.longTasks.length,
longTaskTotalMs: +p.longTasks.reduce((a, t) => a + t.durationMs, 0).toFixed(1),
worstLongTaskMs: p.longTasks.reduce((a, t) => Math.max(a, t.durationMs), 0)
}
})
sample.targetId = targetId
// Require activation so a missed click cannot falsely report a fast first frame.
if (sample.attrFlipMs === null && sample.renderedCommitMs === null) {
throw new Error(
`switch ${i + 1} clicked card ${targetId} but it never became active — the measurement is not valid`
)
}
samples.push(sample)
console.error(
`switch ${i + 1}/${args.switches} -> firstPaint=${sample.firstPaintMs}ms ` +
`maxFrameGap=${sample.maxFrameGapMs}ms attrFlip=${sample.attrFlipMs}ms ` +
`rendered=${sample.renderedCommitMs}ms longTasks=${sample.longTaskCount}/${sample.longTaskTotalMs}ms`
)
}
// Keep the first cold/lazy switch in raw samples but exclude it from warm bisect verdicts.
const scored = samples.length > 1 ? samples.slice(1) : samples
const report = {
port: args.port,
switches: samples.length,
budgetMs: args.budgetMs,
firstPaintMs: summarize(scored.map((s) => s.firstPaintMs)),
maxFrameGapMs: summarize(scored.map((s) => s.maxFrameGapMs)),
attrFlipMs: summarize(scored.map((s) => s.attrFlipMs)),
renderedCommitMs: summarize(scored.map((s) => s.renderedCommitMs)),
worstLongTaskMs: summarize(scored.map((s) => s.worstLongTaskMs)),
longTaskTotalMs: summarize(scored.map((s) => s.longTaskTotalMs)),
samples
}
const medianFirstPaint = report.firstPaintMs?.p50 ?? Infinity
const medianFrameGap = report.maxFrameGapMs?.p50 ?? Infinity
// Treat late first frame or long post-click frame gaps as the stall signal.
const verdictMs = Math.max(medianFirstPaint, medianFrameGap)
report.verdictMs = +verdictMs.toFixed(1)
report.pass = verdictMs <= args.budgetMs
if (args.out) {
mkdirSync(path.dirname(args.out), { recursive: true })
writeFileSync(args.out, JSON.stringify(report, null, 2))
}
if (args.json) {
console.log(JSON.stringify(report, null, 2))
} else {
console.log('')
console.log(
`first paint after click p50=${report.firstPaintMs?.p50}ms max=${report.firstPaintMs?.max}ms`
)
console.log(
`max frame gap p50=${report.maxFrameGapMs?.p50}ms max=${report.maxFrameGapMs?.max}ms`
)
console.log(`attribute flip (JS) p50=${report.attrFlipMs?.p50}ms`)
console.log(
`main pane rendered p50=${report.renderedCommitMs?.p50}ms max=${report.renderedCommitMs?.max}ms`
)
console.log(
`worst long task p50=${report.worstLongTaskMs?.p50}ms max=${report.worstLongTaskMs?.max}ms`
)
console.log('')
console.log(
`${report.pass ? 'PASS' : 'FAIL'} verdict=${report.verdictMs}ms budget=${args.budgetMs}ms`
)
}
await browser.close()
process.exit(report.pass ? 0 : 1)
}
main().catch((error) => {
console.error(error?.stack ?? String(error))
// 125 tells `git bisect run` to skip rather than mark the commit bad.
process.exit(125)
})