1
0

chrome-trace.ts 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import type { CDPSession, Page } from "@playwright/test"
  2. import path from "node:path"
  3. import { mkdir, open, rename } from "node:fs/promises"
  4. import { Buffer } from "node:buffer"
  5. import { createHash, randomUUID } from "node:crypto"
  6. const categories = [
  7. "-*",
  8. "devtools.timeline",
  9. "v8.execute",
  10. "disabled-by-default-devtools.timeline",
  11. "disabled-by-default-devtools.timeline.frame",
  12. "toplevel",
  13. "blink.console",
  14. "blink.user_timing",
  15. "latencyInfo",
  16. "disabled-by-default-devtools.timeline.stack",
  17. "disabled-by-default-v8.cpu_profiler",
  18. ]
  19. export async function startChromeTrace(page: Page, name: string) {
  20. const directory = process.env.OPENCODE_PERFORMANCE_TRACE_DIR
  21. if (!directory) return
  22. const selectors = process.env.OPENCODE_PERFORMANCE_SELECTOR_TRACE === "1"
  23. const file = await prepareChromeTrace(directory, name, selectors)
  24. const session = await page.context().newCDPSession(page)
  25. try {
  26. await session.send("Tracing.start", {
  27. transferMode: "ReturnAsStream",
  28. traceConfig: {
  29. excludedCategories: categories
  30. .filter((category) => category.startsWith("-"))
  31. .map((category) => category.slice(1)),
  32. includedCategories: [
  33. ...categories.filter((category) => !category.startsWith("-")),
  34. ...(selectors
  35. ? ["disabled-by-default-blink.debug", "disabled-by-default-devtools.timeline.invalidationTracking"]
  36. : []),
  37. ],
  38. },
  39. })
  40. } catch (error) {
  41. await Promise.allSettled([session.detach()])
  42. throw error
  43. }
  44. let stopping: Promise<string> | undefined
  45. return () =>
  46. (stopping ??= (async () => {
  47. try {
  48. const complete = new Promise<{ stream?: string; dataLossOccurred: boolean }>((resolve) =>
  49. session.once("Tracing.tracingComplete", resolve),
  50. )
  51. await session.send("Tracing.end")
  52. const result = await complete
  53. if (!result.stream) throw new Error(`Chrome trace stream missing: ${file}`)
  54. const partial = `${file}.partial`
  55. await writeProtocolStream(session, result.stream, partial)
  56. if (result.dataLossOccurred) throw new Error(`Chrome trace lost data; partial capture retained: ${partial}`)
  57. await rename(partial, file)
  58. return file
  59. } finally {
  60. await Promise.allSettled([session.detach()])
  61. }
  62. })())
  63. }
  64. export async function prepareChromeTrace(
  65. directory: string,
  66. name: string,
  67. selectors: boolean,
  68. nonce = randomUUID().slice(0, 8),
  69. ) {
  70. await mkdir(directory, { recursive: true })
  71. const run = process.env.OPENCODE_PERFORMANCE_RUN_ID ?? "manual"
  72. const hash = createHash("sha256").update(name).digest("hex").slice(0, 8)
  73. return path.join(
  74. directory,
  75. `${run}-${name.replace(/[^a-zA-Z0-9_-]/g, "-")}-${hash}-${nonce}${selectors ? "-selectors" : ""}.json`,
  76. )
  77. }
  78. async function writeProtocolStream(session: CDPSession, handle: string, file: string) {
  79. const output = await open(file, "wx")
  80. try {
  81. while (true) {
  82. const chunk = await session.send("IO.read", { handle })
  83. await output.write(chunk.base64Encoded ? Buffer.from(chunk.data, "base64") : chunk.data)
  84. if (chunk.eof) break
  85. }
  86. } finally {
  87. await Promise.allSettled([output.close(), session.send("IO.close", { handle })])
  88. }
  89. }