// A reply streaming into a long session: 160 markdown deltas (bold text, tables, code fences) at 40 a second, from a // server that never waits for the renderer. Per-delta cost that grows with the reply or the history shows up here. import { bench, web, type Case, type Page } from "@hona/strobe" import { reviewDiffs } from "./fixtures/diffs" import { cached } from "./fixtures/mock" import { openReview } from "./fixtures/review" import { mockTimelineSession, reply, streamedReply, timeline } from "./fixtures/timeline" const { start, deltas } = streamedReply(160) const withFind = "stream with Find open" const find = '[data-component="timeline-search-bar"] input' // A phrase only the reply's last delta holds, so Find stays on the reply while every delta rescans the transcript. // One input event: typed key by key, its prefixes would match the history and seek the timeline away from the reply. async function openFind(page: Page) { await page.press("ControlOrMeta+f", { until: find }) await page.eval(() => { const input = document.querySelector('[data-component="timeline-search-bar"] input')! input.value = "benchmark-complete" input.dispatchEvent(new InputEvent("input", { bubbles: true })) }) await page.waitFor('[data-slot="timeline-search-count"]', { text: "0/0" }) } const stream: Case<{}> = { primary: "main.work", metrics: ["frames.interval.p95", "frames.dropped.pct", "main.script", "main.layout", "main.style"], async run({ page, prepared }) { prepared.source.send(start) await Promise.all([ prepared.source.stream(deltas, 25), page.waitFor(reply, { text: "benchmark-complete", timeout: 60_000 }), ]) }, // A fresh document without the streamed reply. async reset({ case: name, page, prepared }) { await page.goto(prepared.path, { until: timeline, timeout: 60_000 }) if (name === "stream with review open") await openReview(page) if (name === withFind) await openFind(page) await page.idle() }, } /** What a case's launch sets up beyond the 120-turn session: cases with the same one share a launch. */ const setups = new Map([ ["stream into a tool-heavy history", "tool-heavy"], ["stream with review open", "review"], [withFind, "find"], ["wait on a running session", "wait"], ]) export default bench({ setupKey: (ctx) => setups.get(ctx.case) ?? "plain", targets: { web: web({ expect: { title: "OpenCode" }, viewport: { width: 1366, height: 768 }, prepare: (ctx) => mockTimelineSession(cached(ctx), { server: new URL(ctx.url).origin, historyTurns: ctx.setupKey === "tool-heavy" ? 320 : 120, toolHeavy: ctx.setupKey === "tool-heavy", busy: true, stream: true, vcsDiff: ctx.setupKey === "review" ? reviewDiffs() : undefined, }), url: ({ prepared }) => prepared.path, }), }, async setup({ setupKey, page }) { await page.waitFor(timeline, { timeout: 60_000 }) if (setupKey === "review") await openReview(page) if (setupKey === "find") await openFind(page) // Its animations never stop, and with the attribution probes on a slow machine their frames can run long enough // that the page never counts as idle. if (setupKey === "wait") return page.wait(2000) await page.idle() }, teardown: ({ prepared }) => prepared.source?.stop(), cases: { // 120 turns: 242 messages, 248 KB of history. "stream a reply": stream, // 320 turns with six file reads in every answer: 2,392 parts, 2.6 MB of history. "stream into a tool-heavy history": stream, // The review pane open beside the timeline on 72 changed files. "stream with review open": stream, // Find open on a phrase that only the last delta holds: each delta rescans the 120-turn transcript for it. [withFind]: stream, // Five seconds of a running session between deltas: the tab's spinner and the Working shimmer animate, and nothing // else changes. Animation the compositor runs costs the page's main thread nothing. "wait on a running session": { primary: "main.work", metrics: ["main.style", "cpu.renderer", "cpu.gpu", "frames.presented.count"], run: ({ page }) => page.wait(5000), }, }, })