benchmark.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import { expect, test as base, type Browser, type Page, type TestInfo } from "@playwright/test"
  2. import { startChromeTrace } from "./chrome-trace"
  3. type BenchmarkFixtures = {
  4. report: (metrics: Record<string, unknown>, context?: Record<string, unknown>) => void
  5. reportState: { payload?: { metrics: Record<string, unknown>; context: Record<string, unknown> } }
  6. benchmarkResult: void
  7. }
  8. export type PerformancePageDiagnostics = {
  9. navigations: string[]
  10. stop: () => Promise<string | undefined>
  11. }
  12. const pages = new WeakMap<Page, PerformancePageDiagnostics>()
  13. export const benchmark = base.extend<BenchmarkFixtures>({
  14. reportState: async ({}, use) => use({}),
  15. report: async ({ reportState }, use) => {
  16. await use((metrics, context = {}) => {
  17. if (reportState.payload) throw new Error("Benchmark reported metrics more than once")
  18. reportState.payload = { metrics, context }
  19. })
  20. },
  21. benchmarkResult: [
  22. async ({ reportState }, use, testInfo) => {
  23. await use()
  24. const missing = !reportState.payload
  25. console.log(
  26. `BENCHMARK ${JSON.stringify({
  27. schemaVersion: 2,
  28. runID: process.env.OPENCODE_PERFORMANCE_RUN_ID,
  29. name: benchmarkName(testInfo),
  30. status: missing ? "failed" : testInfo.status,
  31. expectedStatus: testInfo.expectedStatus,
  32. retry: testInfo.retry,
  33. repeatEachIndex: testInfo.repeatEachIndex,
  34. context: {
  35. project: testInfo.project.name,
  36. platform: process.platform,
  37. ...reportState.payload?.context,
  38. },
  39. metrics: reportState.payload?.metrics ?? null,
  40. error: missing ? "Benchmark did not report metrics" : undefined,
  41. })}`,
  42. )
  43. if (missing && testInfo.status === testInfo.expectedStatus)
  44. throw new Error(`Benchmark did not report metrics: ${benchmarkName(testInfo)}`)
  45. },
  46. { auto: true },
  47. ],
  48. page: async ({ page }, use, testInfo) => {
  49. const name = benchmarkName(testInfo)
  50. const diagnostics = await observePerformancePage(page, name)
  51. try {
  52. await use(page)
  53. } finally {
  54. try {
  55. await reportPerformancePage(name, diagnostics, testInfo)
  56. } finally {
  57. if (testInfo.status !== testInfo.expectedStatus) {
  58. await testInfo.attach("performance-navigations", {
  59. body: JSON.stringify(diagnostics.navigations, null, 2),
  60. contentType: "application/json",
  61. })
  62. }
  63. }
  64. }
  65. },
  66. })
  67. function benchmarkName(testInfo: TestInfo) {
  68. return testInfo.titlePath.slice(1).join(" > ")
  69. }
  70. export { expect }
  71. async function observePerformancePage(page: Page, name: string) {
  72. const navigations: string[] = []
  73. const onNavigation = (frame: ReturnType<Page["mainFrame"]>) => {
  74. if (frame === page.mainFrame()) navigations.push(frame.url())
  75. }
  76. page.on("framenavigated", onNavigation)
  77. const stopTrace = await startChromeTrace(page, name).catch((error) => {
  78. page.off("framenavigated", onNavigation)
  79. throw error
  80. })
  81. let stopping: Promise<string | undefined> | undefined
  82. const diagnostics: PerformancePageDiagnostics = {
  83. navigations,
  84. stop() {
  85. page.off("framenavigated", onNavigation)
  86. return (stopping ??= stopTrace?.() ?? Promise.resolve(undefined))
  87. },
  88. }
  89. pages.set(page, diagnostics)
  90. return diagnostics
  91. }
  92. export async function withBenchmarkPage<T>(
  93. browser: Browser,
  94. name: string,
  95. run: (page: Page) => Promise<T>,
  96. testInfo?: TestInfo,
  97. ) {
  98. const context = await browser.newContext()
  99. try {
  100. const page = await context.newPage()
  101. const diagnostics = await observePerformancePage(page, name)
  102. try {
  103. return await run(page)
  104. } finally {
  105. await reportPerformancePage(name, diagnostics, testInfo)
  106. }
  107. } finally {
  108. await context.close()
  109. }
  110. }
  111. async function reportPerformancePage(name: string, diagnostics: PerformancePageDiagnostics, testInfo?: TestInfo) {
  112. const trace = await diagnostics.stop()
  113. console.log(
  114. `BENCHMARK_PAGE ${JSON.stringify({
  115. schemaVersion: 2,
  116. runID: process.env.OPENCODE_PERFORMANCE_RUN_ID,
  117. name,
  118. test: testInfo ? benchmarkName(testInfo) : undefined,
  119. retry: testInfo?.retry,
  120. repeatEachIndex: testInfo?.repeatEachIndex,
  121. context: {
  122. platform: process.platform,
  123. trace,
  124. selectorTrace: process.env.OPENCODE_PERFORMANCE_SELECTOR_TRACE === "1",
  125. },
  126. navigations: diagnostics.navigations,
  127. })}`,
  128. )
  129. }
  130. export function benchmarkDiagnostics(page: Page) {
  131. const diagnostics = pages.get(page)
  132. if (!diagnostics) throw new Error("Performance diagnostics are not installed for this page")
  133. return diagnostics
  134. }