-
Notifications
You must be signed in to change notification settings - Fork 7
New browser instance per benchmark task #808
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
869a629
5b15db0
d07858f
7a7e5f9
efc2483
4a857ce
9e1c2a5
e28acc1
4576877
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { QueryResult } from "./types"; | ||
|
|
||
| export const DEFAULT_ITERATIONS = 5; | ||
| export const DEFAULT_WARMUP_ROUNDS = 1; | ||
|
|
||
| export function percentile(sorted: number[], p: number): number { | ||
| const idx = Math.ceil((p / 100) * sorted.length) - 1; | ||
| return sorted[Math.max(0, idx)]; | ||
| } | ||
|
|
||
| export function buildQueryResult(name: string, rawTimings: number[]): QueryResult { | ||
| const timings = [...rawTimings].sort((a, b) => a - b); | ||
| return { | ||
| name, | ||
| timings, | ||
| p50: percentile(timings, 50), | ||
| p95: percentile(timings, 95), | ||
| p99: percentile(timings, 99), | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,11 +22,14 @@ | |
| } | ||
|
|
||
| import { chromium } from "playwright"; | ||
| import type { Page } from "playwright"; | ||
| import http from "http"; | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import { execSync } from "child_process"; | ||
| import { BenchmarkResults, TestCase } from "../../benchmark/src/types"; | ||
| import { BenchmarkResults, SourceResult, TestCase } from "../../benchmark/src/types"; | ||
| import { BENCHMARK_TASKS } from "../../benchmark/src/tasks"; | ||
| import { DEFAULT_ITERATIONS, buildQueryResult } from "../../benchmark/src/stats"; | ||
|
|
||
| const DIST_DIR = path.join(__dirname, "..", "..", "benchmark", "dist"); | ||
| const FIXTURES_DIR = path.join(__dirname, "..", "..", "fixtures"); | ||
|
|
@@ -167,6 +170,108 @@ | |
| } | ||
|
|
||
| const server = await startServer(); | ||
|
|
||
| try { | ||
| if (warmupRounds === 0) { | ||
| return await runColdStartBenchmarks({ testCases, iterations, channel }); | ||
| } else { | ||
| // Warmups > 0: all test cases share a single browser instance. | ||
| return await runInBrowser({ testCases, iterations, warmupRounds, channel }); | ||
| } | ||
| } finally { | ||
| await new Promise((res) => server.close(res)); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * warmupRounds=0 path: each (testCase, task, iteration) triple gets a fresh | ||
| * browser so every measurement is a cold start. | ||
| */ | ||
| async function runColdStartBenchmarks({ | ||
| testCases, | ||
| iterations, | ||
| channel, | ||
| }: { | ||
| testCases: TestCase[]; | ||
| iterations?: number; | ||
| channel?: string; | ||
| }): Promise<BenchmarkResults> { | ||
| const allTaskNames = BENCHMARK_TASKS.map((task) => task.name); | ||
| const iterationCount = iterations ?? DEFAULT_ITERATIONS; | ||
|
|
||
| let initTimeMs = 0; | ||
| const sourceResults: SourceResult[] = []; | ||
|
|
||
| for (const testCase of testCases) { | ||
| let registrationMs = 0; | ||
|
|
||
| console.log( | ||
| `[playwright] warmupRounds=0: running ${allTaskNames.length} task(s) × ` + | ||
| `${iterationCount} iteration(s) in separate browser instances` | ||
| ); | ||
|
|
||
| const timingsMap = new Map<string, number[]>(allTaskNames.map((name) => [name, []])); | ||
|
|
||
| for (const taskName of allTaskNames) { | ||
| for (let i = 0; i < iterationCount; i++) { | ||
| console.log( | ||
| `[playwright] Launching browser for "${taskName}" ` + | ||
| `iteration ${i + 1}/${iterationCount} ` + | ||
| `(${testCase.map((source) => source.label).join(", ")})` | ||
| ); | ||
| const run = await runInBrowser({ | ||
| testCases: [testCase], | ||
| iterations: 1, | ||
| warmupRounds: 0, | ||
| channel, | ||
| taskFilter: [taskName], | ||
| }); | ||
|
|
||
| if (initTimeMs === 0) initTimeMs = run.initTimeMs; | ||
| const runResult = run.results[0]; | ||
| if (registrationMs === 0) registrationMs = runResult.registrationMs; | ||
|
|
||
| const timing = runResult.queries[0]?.timings[0]; | ||
| if (timing !== undefined) { | ||
| timingsMap.get(taskName)!.push(timing); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| sourceResults.push({ | ||
| labels: testCase.map((source) => source.label), | ||
| registrationMs, | ||
| queries: allTaskNames.map((name) => buildQueryResult(name, timingsMap.get(name) ?? [])), | ||
| }); | ||
| } | ||
|
|
||
| return { | ||
| timestamp: new Date().toISOString(), | ||
| commit: "unknown", | ||
| branch: "unknown", | ||
| initTimeMs, | ||
| results: sourceResults, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Launch a single browser, run the benchmark with the given config, and return | ||
| * the raw BenchmarkResults. Both the warmup>0 path (all test cases) and the | ||
| * warmup=0 path (one test case + task filter) funnel through here. | ||
| */ | ||
| async function runInBrowser({ | ||
| testCases, | ||
| iterations, | ||
| warmupRounds, | ||
| channel, | ||
| taskFilter, | ||
| }: { | ||
| testCases: TestCase[]; | ||
| iterations?: number; | ||
| warmupRounds?: number; | ||
| channel?: string; | ||
| taskFilter?: string[]; | ||
| }): Promise<BenchmarkResults> { | ||
| const browser = await chromium.launch({ | ||
| channel, | ||
| headless: true, | ||
|
|
@@ -188,6 +293,7 @@ | |
| testCases, | ||
| iterations, | ||
| warmupRounds, | ||
| taskFilter, | ||
| })};`, | ||
| }); | ||
|
|
||
|
|
@@ -203,38 +309,7 @@ | |
| // The browser reads the file lazily via FileReader (BROWSER_FILEREADER protocol), | ||
| // which is identical to how the real app loads files via the file picker — | ||
| // no HTTP range-request overhead, so DuckDB sort performance matches real-user timing. | ||
| const loaded = new Set(); | ||
| for (const source of testCases.flat()) { | ||
| if (source.label in loaded) continue; // Don't add duplicate sources | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This whole section was moved verbatim to |
||
| const localMatch = source.url.match( | ||
| new RegExp(`^http://localhost:${PORT}/fixtures/(.+)$`) | ||
| ); | ||
| if (!localMatch) continue; | ||
| const fixturePath = path.join(FIXTURES_DIR, localMatch[1]); | ||
| if (!fs.existsSync(fixturePath)) continue; | ||
|
|
||
| console.log(`[playwright] Injecting ${source.label} via setInputFiles...`); | ||
| const inputHandle = await page.evaluateHandle(() => { | ||
| const inp: HTMLInputElement = document.createElement("input"); | ||
| inp.type = "file"; | ||
| document.body.appendChild(inp); | ||
| return inp; | ||
| }); | ||
| await inputHandle.setInputFiles(fixturePath); | ||
| await page.evaluate((label) => { | ||
| const inputs: NodeListOf<HTMLInputElement> = document.querySelectorAll( | ||
| "input[type=file]" | ||
| ); | ||
| const inp = inputs[inputs.length - 1]; | ||
| window.__pendingLocalFiles = window.__pendingLocalFiles || {}; | ||
| if (!inp.files) { | ||
| throw new Error(`Injected file not found for ${label}.`); | ||
| } | ||
| window.__pendingLocalFiles[label] = inp.files[0]; | ||
| inp.remove(); | ||
| }, source.label); | ||
| loaded.add(source.label); | ||
| } | ||
| await injectFixtures(page, testCases); | ||
|
|
||
| // Signal the benchmark to proceed with injected File objects | ||
| await page.evaluate(() => { | ||
|
|
@@ -257,7 +332,39 @@ | |
| return await page.evaluate(() => window.__benchmarkResults); | ||
| } finally { | ||
| await browser.close(); | ||
| await new Promise((res) => server.close(res)); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was moved to line 182 |
||
| } | ||
| } | ||
|
|
||
| async function injectFixtures(page: Page, testCases: TestCase[]) { | ||
| const loaded = new Set<string>(); | ||
| for (const source of testCases.flat()) { | ||
| if (loaded.has(source.label)) continue; | ||
| const localMatch = source.url.match(new RegExp(`^http://localhost:${PORT}/fixtures/(.+)$`)); | ||
| if (!localMatch) continue; | ||
| const fixturePath = path.join(FIXTURES_DIR, localMatch[1]); | ||
| if (!fs.existsSync(fixturePath)) continue; | ||
|
|
||
| console.log(`[playwright] Injecting ${source.label} via setInputFiles...`); | ||
| const inputHandle = await page.evaluateHandle(() => { | ||
| const inp: HTMLInputElement = document.createElement("input"); | ||
| inp.type = "file"; | ||
| document.body.appendChild(inp); | ||
| return inp; | ||
| }); | ||
| await inputHandle.setInputFiles(fixturePath); | ||
| await page.evaluate((label) => { | ||
| const inputs: NodeListOf<HTMLInputElement> = document.querySelectorAll( | ||
| "input[type=file]" | ||
| ); | ||
| const inp = inputs[inputs.length - 1]; | ||
| window.__pendingLocalFiles = window.__pendingLocalFiles || {}; | ||
| if (!inp.files) { | ||
| throw new Error(`Injected file not found for ${label}.`); | ||
| } | ||
| window.__pendingLocalFiles[label] = inp.files[0]; | ||
| inp.remove(); | ||
| }, source.label); | ||
| loaded.add(source.label); | ||
| } | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.