File size: 3,285 Bytes
eb1202c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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 })])
  }
}