opencode/packages/app/e2e/performance/chrome-trace.ts

96 lines
3.2 KiB
TypeScript

import type { CDPSession, Page } from "@playwright/test"
import path from "node:path"
import { mkdir, open, rename } from "node:fs/promises"
import { Buffer } from "node:buffer"
import { createHash, randomUUID } from "node:crypto"
const categories = [
"-*",
"devtools.timeline",
"v8.execute",
"disabled-by-default-devtools.timeline",
"disabled-by-default-devtools.timeline.frame",
"toplevel",
"blink.console",
"blink.user_timing",
"latencyInfo",
"disabled-by-default-devtools.timeline.stack",
"disabled-by-default-v8.cpu_profiler",
]
export async function startChromeTrace(page: Page, name: string) {
const directory = process.env.OPENCODE_PERFORMANCE_TRACE_DIR
if (!directory) return
const selectors = process.env.OPENCODE_PERFORMANCE_SELECTOR_TRACE === "1"
const file = await prepareChromeTrace(directory, name, selectors)
const session = await page.context().newCDPSession(page)
try {
await session.send("Tracing.start", {
transferMode: "ReturnAsStream",
traceConfig: {
excludedCategories: categories
.filter((category) => category.startsWith("-"))
.map((category) => category.slice(1)),
includedCategories: [
...categories.filter((category) => !category.startsWith("-")),
...(selectors
? ["disabled-by-default-blink.debug", "disabled-by-default-devtools.timeline.invalidationTracking"]
: []),
],
},
})
} catch (error) {
await Promise.allSettled([session.detach()])
throw error
}
let stopping: Promise<string> | undefined
return () =>
(stopping ??= (async () => {
try {
const complete = new Promise<{ stream?: string; dataLossOccurred: boolean }>((resolve) =>
session.once("Tracing.tracingComplete", resolve),
)
await session.send("Tracing.end")
const result = await complete
if (!result.stream) throw new Error(`Chrome trace stream missing: ${file}`)
const partial = `${file}.partial`
await writeProtocolStream(session, result.stream, partial)
if (result.dataLossOccurred) throw new Error(`Chrome trace lost data; partial capture retained: ${partial}`)
await rename(partial, file)
return file
} finally {
await Promise.allSettled([session.detach()])
}
})())
}
export async function prepareChromeTrace(
directory: string,
name: string,
selectors: boolean,
nonce = randomUUID().slice(0, 8),
) {
await mkdir(directory, { recursive: true })
const run = process.env.OPENCODE_PERFORMANCE_RUN_ID ?? "manual"
const hash = createHash("sha256").update(name).digest("hex").slice(0, 8)
return path.join(
directory,
`${run}-${name.replace(/[^a-zA-Z0-9_-]/g, "-")}-${hash}-${nonce}${selectors ? "-selectors" : ""}.json`,
)
}
async function writeProtocolStream(session: CDPSession, handle: string, file: string) {
const output = await open(file, "wx")
try {
while (true) {
const chunk = await session.send("IO.read", { handle })
await output.write(chunk.base64Encoded ? Buffer.from(chunk.data, "base64") : chunk.data)
if (chunk.eof) break
}
} finally {
await Promise.allSettled([output.close(), session.send("IO.close", { handle })])
}
}