packages/app/perf/navigation.bench.ts
Rama dev · réplica de lectura del repositorio original.
// Moving around the app at 1440×900: Home, new sessions, child sessions, restored tabs and the command palette, on the
// stress fixture (a 12-turn and a 72-turn session with every tool and diffs up to 300 lines, and a subagent's child
// session), paginated as the server does. Every case starts from a fresh document, so each run pays what the first
// visit pays: lazy chunks, the first history page and the first render.
import { bench, web, type Case } from "@hona/strobe"
import { draftHref, sessionHref } from "../e2e/utils/app"
import { messages } from "../e2e/utils/markdown-sessions"
import { mockOpenCodeServer } from "../e2e/utils/mock-server"
import { fixture, installTimelineSettings, mockStressTimeline } from "../e2e/utils/session-fixture"
import { installProjects } from "./fixtures/home"
import { cached } from "./fixtures/mock"
import { installTabs } from "./fixtures/tabs"
const draftID = "draft_perf_navigation"
const last = (ids: string[]) => `[data-message-id="${ids.at(-1)}"]`
const source = last(fixture.expected.sourceMessageIDs)
const target = last(fixture.expected.targetMessageIDs)
const child = last(fixture.expected.childMessageIDs)
const tab = (href: string) => `[data-slot="titlebar-tabs"] a[href="${href}"]`
const tabItem = (href: string) => `[data-slot="titlebar-tabs"] [data-slot="titlebar-tab-item"]:has(a[href="${href}"])`
const home = '[data-component="home-session-search"]'
const homeRow = (sessionID: string) =>
`[data-component="home-session-row-container"][data-session-id="${sessionID}"] [data-component="home-session-row"]`
const newSession = '[data-component="new-session"] [data-component="composer-editor"][contenteditable="true"]'
const model = '[data-component="new-session"] [data-action="composer-model"]'
const palette = '[role="dialog"] [role="combobox"]'
const selected = '[role="dialog"] [role="option"][aria-selected="true"]'
const paletteList = '[role="dialog"] [data-component="menu-scroll"] > [data-scrollable]'
// Eight restored tabs of a normal 20-message first page each (44 parts, 139 KB).
const restored = Array.from({ length: 8 }, (_, index) => ({
...fixture.sessions[0],
id: `ses_perf_restored_${index}`,
title: `Renderer review ${index}`,
}))
const pages = Object.fromEntries(
restored.map((session) => [
session.id,
messages[fixture.targetID].slice(-20).map((message) => ({ ...message, id: `${message.id}_${session.id}` })),
]),
)
const answer = (sessionID: string) =>
`[data-timeline-part-id="${pages[sessionID].at(-1)?.id}:text:0"] [data-component="markdown"][data-markdown-ready]`
type Scene = { tabs: string[]; draft?: boolean; first: (server: string) => string; ready: string }
const sourcePage = (server: string) => sessionHref(fixture.sourceID, server)
const inSource: Scene = { tabs: [fixture.sourceID], first: sourcePage, ready: source }
const onHome: Scene = { tabs: [], first: () => "/", ready: homeRow(fixture.targetID) }
const restoring: Scene = {
tabs: restored.map((session) => session.id),
first: (server) => sessionHref(restored[0].id, server),
ready: answer(restored[0].id),
}
/** The places cases start from: open tabs, a first page and what that page shows once ready. */
const scenes = {
home: onHome,
"home, new session": { ...onHome, ready: '[data-action="home-new-session"]' },
source: inSource,
"source, new session tab": { ...inSource, draft: true },
"new session page": {
tabs: [fixture.sourceID, fixture.targetID],
draft: true,
first: () => draftHref(draftID),
ready: newSession,
},
restoring,
} satisfies Record<string, Scene>
/** Where each case starts. */
/** What a launch sets up: the scene, and whether setup opens the command palette. */
type Setup = { scene: keyof typeof scenes; palette: boolean }
const sceneOf = new Map<string, keyof typeof scenes>([
["open a session from home", "home"],
["close the last tab", "source"],
["new session from home", "home, new session"],
["new session from a session", "source"],
["open the new session page", "source, new session tab"],
["leave the new session page", "new session page"],
["open a child session", "source"],
["restore 8 tabs", "restoring"],
["activate a restored tab", "restoring"],
["search commands in a session", "source"],
["search commands on home", "home"],
["scroll the command palette", "source"],
])
const fresh: Pick<Case<{}>, "reset"> = {
async reset({ page, prepared }) {
await page.goto(prepared.first, { until: prepared.ready, timeout: 60_000 })
await page.idle()
},
}
const metrics = ["run.duration", "interaction.p50"]
export default bench({
// A case's scene, and whether setup opens the command palette: cases with the same ones share a launch.
setupKey: ({ case: name }): Setup => {
const scene = sceneOf.get(name)
if (!scene) throw new Error(`navigation.bench.ts: no scene for "${name}"`)
return { scene, palette: name === "scroll the command palette" }
},
targets: {
// A target is made before the bench, so its hooks see the setup key as typed here.
web: web<{}, Setup>({
expect: { title: "OpenCode" },
viewport: { width: 1440, height: 900 },
async prepare(ctx) {
const { url } = ctx
const server = new URL(url).origin
const scene: Scene = scenes[ctx.setupKey.scene]
const page = cached(ctx)
if (scene === restoring)
await mockOpenCodeServer(page, {
server,
sessions: restored,
provider: fixture.provider,
directory: fixture.directory,
project: fixture.project,
pageMessages: (sessionID) => ({ items: pages[sessionID] ?? [] }),
})
else await mockStressTimeline(page, { server })
await installTimelineSettings(page)
await installProjects(page, [fixture.directory])
await installTabs(page, {
server,
directory: fixture.directory,
sessions: scene.tabs,
draft: scene.draft ? draftID : undefined,
})
return { server, first: scene.first(server), ready: scene.ready }
},
url: ({ prepared }) => prepared.first,
}),
},
async setup({ setupKey, page, prepared }) {
await page.waitFor(prepared.ready, { timeout: 60_000 })
if (setupKey.palette) await page.press("ControlOrMeta+Shift+P", { until: paletteList })
await page.idle()
},
cases: {
"open a session from home": {
primary: "main.work",
metrics,
async run({ page, prepared }) {
await page.click(homeRow(fixture.targetID), { until: target })
await page.waitFor(tab(sessionHref(fixture.targetID, prepared.server)))
},
...fresh,
},
// As a person closes it: the close button shows on hover.
"close the last tab": {
primary: "main.work",
metrics,
async run({ page, prepared }) {
const item = tabItem(sessionHref(fixture.sourceID, prepared.server))
const close = `${item} [data-slot="tab-close"] button`
await page.hover(item, { until: close })
await page.click(close, { until: home })
await page.waitFor(homeRow(fixture.targetID))
},
...fresh,
},
// Ready to type: the composer is editable with the default model.
"new session from home": {
primary: "main.work",
metrics,
async run({ page }) {
await page.click('[data-action="home-new-session"]', { until: newSession })
await page.waitFor(model, { text: "Claude Opus 4.6" })
},
...fresh,
},
"new session from a session": {
primary: "main.work",
metrics,
async run({ page }) {
await page.click('button[aria-label="New session"]', { until: newSession })
await page.waitFor(model, { text: "Claude Opus 4.6" })
},
...fresh,
},
// The new session page's module loads on its first use.
"open the new session page": {
primary: "main.work",
metrics,
run: ({ page }) => page.click(tab(draftHref(draftID)), { until: newSession }),
...fresh,
},
"leave the new session page": {
primary: "main.work",
metrics,
run: ({ page, prepared }) => page.click(tab(sessionHref(fixture.targetID, prepared.server)), { until: target }),
...fresh,
},
// From the subagent card in the source's last turn.
"open a child session": {
primary: "main.work",
metrics,
run: ({ page, prepared }) =>
page.click(
`a[href="${sessionHref(fixture.childID, prepared.server)}"]:has([data-component="task-tool-card"])`,
{
until: child,
},
),
...fresh,
},
// Loading the app with eight tabs to restore, to the active one's last answer on screen. Transcripts fetched
// (`net.requests` on the history endpoint): the active tab's, not one per restored tab.
"restore 8 tabs": {
primary: "main.work",
metrics: ["run.duration", "net.requests{/api/session/*/message}"],
run: ({ page, prepared }) => page.goto(prepared.first, { until: prepared.ready, timeout: 60_000 }),
},
"activate a restored tab": {
primary: "main.work",
metrics: [...metrics, "net.requests{/api/session/*/message}"],
run: ({ page, prepared }) =>
page.click(tab(sessionHref(restored[1].id, prepared.server)), { until: answer(restored[1].id) }),
async reset({ page, prepared }) {
await page.goto(prepared.first, { until: prepared.ready, timeout: 60_000 })
// Off the tab strip: a pointer resting on a tab for 100 ms prefetches its transcript.
await page.hover('[data-component="composer-editor"]')
await page.idle()
},
},
"search commands in a session": {
primary: "main.work",
metrics: ["run.duration", "interaction.p95"],
async run({ page }) {
await page.press("ControlOrMeta+Shift+P", { until: palette })
await page.type(palette, "copy session", { delay: 50 })
await page.waitFor(selected, { text: "Copy Session ID" })
},
async reset({ page }) {
await page.press("Escape")
await page.idle()
},
},
"search commands on home": {
primary: "main.work",
metrics: ["run.duration", "interaction.p95"],
async run({ page }) {
await page.press("ControlOrMeta+Shift+P", { until: palette })
await page.type(palette, "open settings", { delay: 50 })
await page.waitFor(selected, { text: "Open settings" })
},
async reset({ page }) {
await page.press("Escape")
await page.idle()
},
},
// The palette's result list scrolls in a MenuScroll, which moves its thumb on every scroll frame.
"scroll the command palette": {
primary: "main.busy",
metrics: ["main.work", "frames.dropped.pct"],
async run({ page }) {
await page.scroll(paletteList, { by: 3000, speed: 3000 })
await page.scroll(paletteList, { by: -3000, speed: 3000 })
},
// Opens the palette again each run, so a run starts at the top even if something closed it.
async reset({ page }) {
await page.press("Escape")
await page.press("ControlOrMeta+Shift+P", { until: paletteList })
await page.idle()
},
},
},
})