99 type Benchmark ,
1010 type BenchmarkStats ,
1111} from "@codspeed/core" ;
12+ import { createRequire } from "node:module" ;
13+ import { pathToFileURL } from "node:url" ;
1214import { _electron as electron } from "playwright" ;
1315import type { ElectronApplication , Page } from "playwright-core" ;
1416
@@ -32,6 +34,11 @@ export interface BenchFixtures {
3234 */
3335export type BenchFunction = ( fixtures : BenchFixtures ) => void | Promise < void > ;
3436
37+ /**
38+ * A hook run around the measured region. Not included in the reported timing.
39+ */
40+ export type BenchHook = ( fixtures : BenchFixtures ) => void | Promise < void > ;
41+
3542/**
3643 * Minimal options for a benchmark. Inspired by Vitest's `bench`, but kept
3744 * deliberately small.
@@ -47,20 +54,45 @@ export interface BenchOptions {
4754 */
4855 appPath : string ;
4956 /**
50- * Additional CLI flags forwarded to Electron.
57+ * CLI flags forwarded to Electron.
5158 */
5259 electronArgs ?: string [ ] ;
5360 /**
5461 * Working directory for the Electron process. Defaults to `process.cwd()`.
5562 */
5663 cwd ?: string ;
5764 /**
58- * Absolute path to the Electron executable. When omitted, Playwright resolves
59- * it via `require("electron")`. Set this when running under package managers
60- * (e.g. pnpm) where Playwright cannot resolve `electron` from its own
61- * directory.
65+ * Absolute path to the Electron executable. When omitted, it is resolved from
66+ * the `electron` package in `cwd`. Set this only to override that default.
67+ */
68+ electronExecutablePath ?: string ;
69+ /**
70+ * Run before each round, after the window opens. Use it to bring the app to
71+ * a steady state (initial render done, data loaded, …). Not measured.
72+ */
73+ setup ?: BenchHook ;
74+ /**
75+ * Run after each round, before the app is closed. Use it for teardown that
76+ * should not be measured.
6277 */
63- executablePath ?: string ;
78+ teardown ?: BenchHook ;
79+ }
80+
81+ let integrationInitialized = false ;
82+
83+ /**
84+ * Register the integration and environment with the instrumentation. This is
85+ * process-global, so it only needs to run once regardless of how many
86+ * benchmarks are defined.
87+ */
88+ function ensureIntegrationSetup ( ) : void {
89+ if ( integrationInitialized ) return ;
90+ integrationInitialized = true ;
91+
92+ InstrumentHooks . setIntegration ( "node-custom" , __VERSION__ ) ;
93+ InstrumentHooks . setEnvironment ( "nodejs" , "version" , process . versions . node ) ;
94+ InstrumentHooks . setEnvironment ( "nodejs" , "v8" , process . versions . v8 ) ;
95+ InstrumentHooks . writeEnvironment ( process . pid ) ;
6496}
6597
6698function resolveRounds ( optionRounds : number | undefined ) : number {
@@ -74,15 +106,32 @@ function resolveRounds(optionRounds: number | undefined): number {
74106 return n ;
75107}
76108
109+ /**
110+ * Resolve the path to the Electron binary.
111+ *
112+ * Playwright resolves Electron via `require("electron/index.js")` from inside
113+ * its own package directory. Under isolated installs (e.g. pnpm), Playwright
114+ * cannot see the project's `electron` dependency and bails out with
115+ * "Electron executablePath not found!". We resolve it ourselves from the
116+ * benchmark's working directory, where `electron` is a real dependency.
117+ */
118+ function resolveElectronExecutable ( cwd : string ) : string {
119+ const require = createRequire ( pathToFileURL ( `${ cwd } /` ) ) ;
120+ // `electron`'s main module exports the absolute path to its binary.
121+ return require ( "electron" ) as string ;
122+ }
123+
77124async function launchApp ( options : BenchOptions ) : Promise < ElectronApplication > {
125+ const cwd = options . cwd ?? process . cwd ( ) ;
78126 return electron . launch ( {
79127 args : [
80128 options . appPath ,
81129 ...( options . electronArgs ?? [ ] ) ,
82130 `--js-flags=${ DEFAULT_PROFILING_JS_FLAGS } ` ,
83131 ] ,
84- cwd : options . cwd ?? process . cwd ( ) ,
85- executablePath : options . executablePath ,
132+ cwd,
133+ executablePath :
134+ options . electronExecutablePath ?? resolveElectronExecutable ( cwd ) ,
86135 } ) ;
87136}
88137
@@ -94,6 +143,10 @@ async function runOneSample(
94143 const page = await app . firstWindow ( ) ;
95144
96145 try {
146+ if ( options . setup ) {
147+ await options . setup ( { page } ) ;
148+ }
149+
97150 const startTs = InstrumentHooks . currentTimestamp ( ) ;
98151 await fn ( { page } ) ;
99152 const endTs = InstrumentHooks . currentTimestamp ( ) ;
@@ -105,6 +158,10 @@ async function runOneSample(
105158 ) ;
106159 InstrumentHooks . addMarker ( process . pid , MARKER_TYPE_BENCHMARK_END , endTs ) ;
107160
161+ if ( options . teardown ) {
162+ await options . teardown ( { page } ) ;
163+ }
164+
108165 return endTs - startTs ;
109166 } finally {
110167 await app . close ( ) ;
@@ -177,10 +234,7 @@ export async function bench(
177234 const rounds = resolveRounds ( options . rounds ) ;
178235 const uri = `${ getCallingFile ( 0 ) } ::${ name } ` ;
179236
180- InstrumentHooks . setIntegration ( "node-custom" , __VERSION__ ) ;
181- InstrumentHooks . setEnvironment ( "nodejs" , "version" , process . versions . node ) ;
182- InstrumentHooks . setEnvironment ( "nodejs" , "v8" , process . versions . v8 ) ;
183- InstrumentHooks . writeEnvironment ( process . pid ) ;
237+ ensureIntegrationSetup ( ) ;
184238
185239 InstrumentHooks . setExecutedBenchmark ( process . pid , uri ) ;
186240 InstrumentHooks . startBenchmark ( ) ;
0 commit comments