Skip to content

Commit 52902b8

Browse files
--wip-- [skip ci]
1 parent 3745693 commit 52902b8

11 files changed

Lines changed: 207 additions & 140 deletions

File tree

.github/workflows/codspeed.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ jobs:
103103
- name: Set up pnpm
104104
uses: pnpm/action-setup@v4
105105

106-
# The example links @codspeed/playwright to packages/playwright via the
106+
# The example links @codspeed/playwright-plugin to packages/playwright-plugin via the
107107
# `link:` protocol, so the in-repo plugin must be built (under the repo's
108108
# own Node version) before the example installs and resolves the symlink.
109109
- name: Set up Node.js (repo)
@@ -118,8 +118,8 @@ jobs:
118118
- name: Install repo dependencies
119119
run: pnpm install --frozen-lockfile --prefer-offline
120120

121-
- name: Build @codspeed/playwright
122-
run: pnpm turbo run build --filter=@codspeed/playwright
121+
- name: Build @codspeed/playwright-plugin
122+
run: pnpm turbo run build --filter=@codspeed/playwright-plugin
123123

124124
- name: Set up Node.js (example)
125125
uses: actions/setup-node@v6

examples/with-electron-and-walltime/apps/electron/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"@mail-client-demo/model": "workspace:*"
1717
},
1818
"devDependencies": {
19-
"@codspeed/playwright": "link:../../../../packages/playwright",
19+
"@codspeed/playwright-plugin": "link:../../../../packages/playwright-plugin",
2020
"@types/node": "^25.9.1",
2121
"electron": "^42.2.0",
2222
"electron-vite": "^5.0.0",

examples/with-electron-and-walltime/apps/electron/test/inbox.e2e.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { bench, type Page } from "@codspeed/playwright";
1+
import { bench, type Page } from "@codspeed/playwright-plugin";
22
import electronExecutable from "electron";
33
import path from "node:path";
44
import { fileURLToPath } from "node:url";
@@ -27,9 +27,12 @@ await bench(
2727
await waitUntilSettled(page);
2828
},
2929
{
30-
appPath: path.join(appRoot, "out/main/index.js"),
31-
cwd: appRoot,
32-
setup: async ({ page }) => {
30+
target: {
31+
kind: "electron",
32+
appPath: path.join(appRoot, "out/main/index.js"),
33+
cwd: appRoot,
34+
},
35+
beforeRound: async ({ page }) => {
3336
page.setDefaultTimeout(180_000);
3437
await page.waitForSelector("#main");
3538
await waitUntilSettled(page);

examples/with-electron-and-walltime/pnpm-lock.yaml

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<div align="center">
2+
<h1><code>@codspeed/playwright-plugin</code></h1>
3+
4+
[Playwright](https://playwright.dev) integration for [CodSpeed](https://codspeed.io), to drive an app through Playwright and report measured user flows.
5+
6+
[![CI](https://github.com/CodSpeedHQ/codspeed-node/actions/workflows/ci.yml/badge.svg)](https://github.com/CodSpeedHQ/codspeed-node/actions/workflows/ci.yml)
7+
[![npm (scoped)](https://img.shields.io/npm/v/@codspeed/playwright-plugin)](https://www.npmjs.com/package/@codspeed/playwright-plugin)
8+
[![Discord](https://img.shields.io/badge/chat%20on-discord-7289da.svg)](https://discord.com/invite/MxpaCfKSqF)
9+
[![CodSpeed Badge](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/CodSpeedHQ/codspeed-node)
10+
11+
</div>
12+
13+
> [!NOTE]
14+
> The `@codspeed/playwright-plugin` integration currently supports only the
15+
> [walltime instrument](https://docs.codspeed.io/instruments/walltime). CPU
16+
> Simulation is not available.
17+
18+
`@codspeed/playwright-plugin` is the CodSpeed integration for
19+
[Playwright](https://playwright.dev). It runs a user-defined flow against a
20+
target application, measures the time spent inside that flow, and reports it to
21+
CodSpeed. The flow itself is plain Playwright code, so anything Playwright can
22+
drive can be benchmarked.
23+
24+
> [!IMPORTANT]
25+
> Today the plugin supports [Electron](https://www.electronjs.org) apps as a
26+
> target. Browser-based targets (existing dev servers, static builds, hosted
27+
> URLs) are on the roadmap and will be added under the same `bench` API.
28+
29+
## Documentation
30+
31+
Check out the [documentation](https://docs.codspeed.io/benchmarks/nodejs/playwright) for complete integration instructions.
32+
33+
## Installation
34+
35+
Install the plugin alongside `playwright`:
36+
37+
```sh
38+
npm install --save-dev @codspeed/playwright-plugin playwright
39+
```
40+
41+
or with `yarn`:
42+
43+
```sh
44+
yarn add --dev @codspeed/playwright-plugin playwright
45+
```
46+
47+
or with `pnpm`:
48+
49+
```sh
50+
pnpm add --save-dev @codspeed/playwright-plugin playwright
51+
```
52+
53+
## Example usage with Electron
54+
55+
Build your Electron app first so the main entrypoint exists (e.g.,
56+
`out/main/index.js`), then declare a benchmark with `target.kind` set to
57+
`"electron"`:
58+
59+
```ts title="bench/inbox.bench.ts"
60+
import { bench } from "@codspeed/playwright-plugin";
61+
import path from "node:path";
62+
63+
bench(
64+
"inbox-search",
65+
async ({ page }) => {
66+
await page.fill("#search", "quarterly report");
67+
await page.waitForSelector("#results");
68+
},
69+
{
70+
target: {
71+
kind: "electron",
72+
appPath: path.resolve("out/main/index.js"),
73+
},
74+
beforeRound: async ({ page }) => {
75+
await page.waitForSelector("#main:not(.loading)");
76+
},
77+
rounds: 5,
78+
},
79+
);
80+
```
81+
82+
For each round, the plugin launches Electron with the provided main entrypoint,
83+
waits for the first window, runs `beforeRound`, measures `fn`, runs
84+
`afterRound`, then closes the app.
85+
86+
## API
87+
88+
The plugin exposes a single `bench` function. Its shape is target-agnostic:
89+
90+
```ts
91+
import { bench } from "@codspeed/playwright-plugin";
92+
93+
bench(name, fn, options);
94+
```
95+
96+
| Argument | Type | Required | Description |
97+
| --------- | ------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
98+
| `name` | `string` | yes | Identifier of the benchmark, used by CodSpeed to track it across runs. |
99+
| `fn` | `({ page }) => void \| Promise<void>` | yes | The function whose execution time is measured. Receives a Playwright [`Page`](https://playwright.dev/docs/api/class-page) bound to the target. Everything inside `fn` counts toward the reported timing. |
100+
| `options` | `BenchOptions` | yes | Target configuration and benchmark settings, detailed below. |
101+
102+
### Options
103+
104+
| Option | Type | Default | Description |
105+
| ------------- | ------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
106+
| `target` | `Target` | _(required)_ | Discriminated union describing what to drive. The `kind` field selects the target; the remaining fields are specific to that kind. Current variants: [`{ kind: "electron", ... }`](#electron-target-options). |
107+
| `rounds` | `number` | `1` | Number of measurement rounds. Can be overridden at runtime via the `CODSPEED_PLAYWRIGHT_ROUNDS` environment variable. |
108+
| `beforeRound` | `({ page }) => void \| Promise<void>` || Runs before each round, after the target is ready. Use it to bring the app to a ready state. Not measured. |
109+
| `afterRound` | `({ page }) => void \| Promise<void>` || Runs after each round, before the target is torn down. Not measured. |
110+
111+
### Electron target options
112+
113+
| Option | Type | Default | Description |
114+
| ------------------------------- | ------------ | ------------------- | ------------------------------------------------------------------------------------------------------------ |
115+
| `target.kind` | `"electron"` | _(required)_ | Selects the Electron target. |
116+
| `target.appPath` | `string` | _(required)_ | Absolute path to the Electron main entrypoint, e.g., `out/main/index.js`. |
117+
| `target.electronArgs` | `string[]` | `[]` | Extra CLI flags forwarded to the Electron process. |
118+
| `target.cwd` | `string` | `process.cwd()` | Working directory for the Electron process. Also the directory `electron` is resolved from when `target.electronExecutablePath` is not set. |
119+
| `target.electronExecutablePath` | `string` | resolved `electron` | Absolute path to the Electron binary. Only set this to override the default resolution. |
120+
121+
## Running the benchmarks locally
122+
123+
With node 24+, you can run typescript files directly:
124+
125+
```bash
126+
$ node bench/inbox.bench.ts
127+
[CodSpeed] [round 1/5] 42.13 ms
128+
[CodSpeed] [round 2/5] 41.78 ms
129+
[CodSpeed] [round 3/5] 42.05 ms
130+
[CodSpeed] [round 4/5] 41.92 ms
131+
[CodSpeed] [round 5/5] 42.21 ms
132+
```
133+
134+
Locally, `bench` runs the app and prints per-round timings to the terminal.
135+
Results are uploaded to CodSpeed only when running in the
136+
[CI environment](https://docs.codspeed.io/benchmarks/nodejs/playwright#running-the-benchmarks-in-your-ci)
137+
or when using the
138+
[CodSpeed CLI](https://docs.codspeed.io/cli#running-benchmarks).
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "@codspeed/playwright",
2+
"name": "@codspeed/playwright-plugin",
33
"version": "5.4.0",
44
"description": "Playwright benchmarking integration for CodSpeed",
55
"keywords": [
File renamed without changes.
Lines changed: 53 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const DEFAULT_PROFILING_JS_FLAGS =
2222

2323
/**
2424
* The fixtures passed to a benchmark function, mirroring Playwright's `test`
25-
* API. Currently exposes the Electron window as `page`.
25+
* API. Currently exposes the target's main window as `page`.
2626
*/
2727
export interface BenchFixtures {
2828
page: Page;
@@ -37,15 +37,10 @@ export type BenchFunction = (fixtures: BenchFixtures) => void | Promise<void>;
3737
export type BenchHook = (fixtures: BenchFixtures) => void | Promise<void>;
3838

3939
/**
40-
* Minimal options for a benchmark. Inspired by Vitest's `bench`, but kept
41-
* deliberately small.
40+
* Electron-specific target options.
4241
*/
43-
export interface BenchOptions {
44-
/**
45-
* Number of measurement rounds to perform. Defaults to 1, can be overridden
46-
* via the `CODSPEED_ROUNDS` environment variable.
47-
*/
48-
rounds?: number;
42+
export interface ElectronTarget {
43+
kind: "electron";
4944
/**
5045
* Absolute path to the Electron main entrypoint (e.g. `out/main/index.js`).
5146
*/
@@ -63,16 +58,36 @@ export interface BenchOptions {
6358
* the `electron` package in `cwd`. Set this only to override that default.
6459
*/
6560
electronExecutablePath?: string;
61+
}
62+
63+
/**
64+
* Determines what kind of host playwright is going to run the tests on
65+
*/
66+
export type Target = ElectronTarget;
67+
68+
/**
69+
* Minimal options for a benchmark. Inspired by Vitest's `bench`, but kept
70+
* deliberately small.
71+
*/
72+
export interface BenchOptions {
6673
/**
67-
* Run before each round, after the window opens. Use it to bring the app to
68-
* a steady state (initial render done, data loaded, …). Not measured.
74+
* Target configuration. The `kind` field selects the target; the remaining
75+
* fields are specific to that kind.
6976
*/
70-
setup?: BenchHook;
77+
target: Target;
7178
/**
72-
* Run after each round, before the app is closed. Use it for teardown that
73-
* should not be measured.
79+
* Number of measurement rounds to perform. Defaults to 3, or the value of CODSPEED_PLAYWRIGHT_ROUNDS.
7480
*/
75-
teardown?: BenchHook;
81+
rounds?: number;
82+
/**
83+
* Runs before each round, after the target is ready. Use it to bring the app to
84+
* a ready state. Not measured.
85+
*/
86+
beforeRound?: BenchHook;
87+
/**
88+
* Runs after each round, before the target is torn down. Not measured.
89+
*/
90+
afterRound?: BenchHook;
7691
}
7792

7893
let integrationInitialized = false;
@@ -93,7 +108,7 @@ function ensureIntegrationSetup(): void {
93108
}
94109

95110
function resolveRounds(optionRounds: number | undefined): number {
96-
const envValue = process.env.CODSPEED_ROUNDS;
111+
const envValue = process.env.CODSPEED_PLAYWRIGHT_ROUNDS;
97112
const raw = envValue ?? optionRounds;
98113
if (raw === undefined) return DEFAULT_ROUNDS;
99114
const n = Number(raw);
@@ -118,30 +133,32 @@ function resolveElectronExecutable(cwd: string): string {
118133
return require("electron") as string;
119134
}
120135

121-
async function launchApp(options: BenchOptions): Promise<ElectronApplication> {
122-
const cwd = options.cwd ?? process.cwd();
136+
async function launchElectronApp(
137+
target: ElectronTarget,
138+
): Promise<ElectronApplication> {
139+
const cwd = target.cwd ?? process.cwd();
123140
return electron.launch({
124141
args: [
125-
options.appPath,
126-
...(options.electronArgs ?? []),
142+
target.appPath,
143+
...(target.electronArgs ?? []),
127144
`--js-flags=${DEFAULT_PROFILING_JS_FLAGS}`,
128145
],
129146
cwd,
130147
executablePath:
131-
options.electronExecutablePath ?? resolveElectronExecutable(cwd),
148+
target.electronExecutablePath ?? resolveElectronExecutable(cwd),
132149
});
133150
}
134151

135152
async function runOneSample(
136153
fn: BenchFunction,
137154
options: BenchOptions,
138155
): Promise<bigint> {
139-
const app = await launchApp(options);
156+
const app = await launchElectronApp(options.target);
140157
const page = await app.firstWindow();
141158

142159
try {
143-
if (options.setup) {
144-
await options.setup({ page });
160+
if (options.beforeRound) {
161+
await options.beforeRound({ page });
145162
}
146163

147164
const startTs = InstrumentHooks.currentTimestamp();
@@ -155,8 +172,8 @@ async function runOneSample(
155172
);
156173
InstrumentHooks.addMarker(process.pid, MARKER_TYPE_BENCHMARK_END, endTs);
157174

158-
if (options.teardown) {
159-
await options.teardown({ page });
175+
if (options.afterRound) {
176+
await options.afterRound({ page });
160177
}
161178

162179
return endTs - startTs;
@@ -202,24 +219,27 @@ function buildStats(sampleTimesNs: bigint[]): BenchmarkStats {
202219
}
203220

204221
/**
205-
* Define and run a CodSpeed-instrumented Electron benchmark, mirroring
206-
* Playwright's `test` API.
222+
* Define and run a CodSpeed-instrumented benchmark, mirroring Playwright's
223+
* `test` API.
207224
*
208-
* Launches the Electron app once per round, runs the user-provided function
209-
* around a measured region, and writes walltime results to disk so that the
210-
* CodSpeed runner can pick them up.
225+
* Launches the target once per round, runs the user-provided function around a
226+
* measured region, and writes walltime results to disk so that the CodSpeed
227+
* runner can pick them up.
211228
*
212229
* @example
213230
* ```ts
214-
* import { bench } from "@codspeed/playwright";
231+
* import { bench } from "@codspeed/playwright-plugin";
215232
*
216233
* bench(
217234
* "renders the dashboard",
218235
* async ({ page }) => {
219236
* await page.getByRole("link", { name: "Dashboard" }).click();
220237
* await page.getByRole("heading", { name: "Overview" }).waitFor();
221238
* },
222-
* { appPath: "out/main/index.js", rounds: 5 },
239+
* {
240+
* target: { kind: "electron", appPath: "out/main/index.js" },
241+
* rounds: 5,
242+
* },
223243
* );
224244
* ```
225245
*/

0 commit comments

Comments
 (0)