Skip to content

Commit 84cad25

Browse files
jasnelladuh95
authored andcommitted
lib: add runId, fileRunId, entryFile, namePath to node:bench
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode PR-URL: #65631 Reviewed-By: Filip Skokan <panva.ip@gmail.com>
1 parent 07d004c commit 84cad25

15 files changed

Lines changed: 440 additions & 37 deletions

doc/api/bench.md

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ Benchmark files passed to `--bench` should declare benchmarks but must not call
137137
`--bench-warmup`, `--bench-reporter`, and `--bench-reporter-destination`. See
138138
the [command-line options documentation][] for details.
139139

140+
Preload modules passed through `--require` or `--import` should not declare
141+
benchmarks. Such declarations are not associated with an entry file and have
142+
an `entryFile` value of `null`. Their `fileRunId` identifies the runner or child
143+
execution in which they occurred. With process isolation, a preload is evaluated
144+
and its declarations run once for every benchmark child process.
145+
140146
## Benchmark reporters
141147

142148
The built-in reporters are available from the scheme-only
@@ -262,9 +268,19 @@ benchmark. Later benchmarks continue to run.
262268
A timeout or abort cannot interrupt synchronous JavaScript and does not forcibly
263269
cancel asynchronous work that ignores `context.signal`.
264270

265-
The stable `benchId` is based on the source file, hierarchical suite and
266-
benchmark names, and canonicalized parameters. Declaring the same identity
267-
more than once reports an error rather than merging the samples.
271+
The `benchId` is based on the declaration source file, hierarchical suite and
272+
benchmark names, and canonicalized parameters. It is stable for repeated runs
273+
from the same source location, but the embedded source value is not normalized
274+
across checkout roots, module formats, operating systems, or path casing.
275+
276+
Execution scope is represented separately. A `runId` identifies one logical
277+
run, while `fileRunId` identifies a file runner or child execution within that
278+
run. The `entryFile` field records which entry-file import caused a declaration
279+
and is `null` for declarations made by preload modules.
280+
The same `benchId` can therefore occur under multiple `fileRunId` values when
281+
entry files use a shared declaration helper. Declaring the same `benchId` more
282+
than once within one file execution scope reports an error rather than merging
283+
the samples.
268284

269285
### `bench.skip([name][, options], fn)`
270286

@@ -537,14 +553,20 @@ The events are emitted in execution order:
537553
* `'bench:diagnostic'`
538554
* `'bench:summary'`
539555

540-
Every benchmark-scoped event contains `benchId` and `parentId`.
556+
Every benchmark-scoped event contains `runId`, `fileRunId`, `entryFile`,
557+
`benchId`, `parentId`, and `namePath`. `runId` and `fileRunId` are opaque and
558+
change between runs. `entryFile` identifies the top-level benchmark file whose
559+
loading caused the declaration, while `file` identifies the source location of
560+
the declaration itself. `parentId` is based on the containing suite's source
561+
file and hierarchical name path.
562+
541563
`'bench:complete'` data contains a [benchmark result][]. A failed result has an
542564
additional `error` property and may contain samples recorded before the error.
543565
A skipped result has an additional `skip` property and an empty `samples`
544566
array. `'bench:diagnostic'` reports suite and hook errors. `'bench:summary'`
545-
contains overall `success`, `counts`, `duration_ns`, and `file` properties. The
546-
`file` is {string|null}; it is `null` when the summary aggregates multiple
547-
files.
567+
contains overall `runId`, `fileRunId`, `entryFile`, `success`, `counts`,
568+
`duration_ns`, and `file` properties. `fileRunId`, `entryFile`, and `file` are
569+
{string|null}; they are `null` when the summary aggregates multiple files.
548570

549571
## Sample result
550572

@@ -560,10 +582,15 @@ Each measured sample has the following properties:
560582

561583
A completed benchmark result contains:
562584

563-
* `benchId` {string} The stable benchmark identity.
585+
* `runId` {string} The opaque logical run identity.
586+
* `fileRunId` {string} The opaque file runner or child execution identity.
587+
* `entryFile` {string|null} The top-level file that caused this declaration.
588+
* `benchId` {string} The stable declaration identity within the same source
589+
layout.
564590
* `parentId` {string|null} The stable containing suite identity.
565591
* `name` {string} The benchmark name.
566-
* `file` {string} The source file.
592+
* `namePath` {string\[]} The hierarchical suite and benchmark names.
593+
* `file` {string} The declaration source file.
567594
* `line` {number} The source line.
568595
* `column` {number} The source column.
569596
* `tags` {string\[]} The inherited canonical tags.

lib/internal/bench_runner/benchmark.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const { structuredClone } = require('internal/worker/js_transferable');
4848
const { bigint: hrtime } = process.hrtime;
4949
const kDefaultSamples = 30;
5050
const kDefaultWarmup = 0;
51+
const kEmptyNamePath = ObjectFreeze([]);
5152
const kEmptyParams = ObjectFreeze({ __proto__: null });
5253
const kEmptyTags = ObjectFreeze([]);
5354

@@ -163,10 +164,19 @@ class Suite extends AsyncResource {
163164
this.name = name;
164165
this.fn = fn;
165166
this.loc = createLocation(loc, harness.entryFile);
167+
this.isRoot = isRoot;
168+
this.fileScope = isRoot ? null :
169+
(parent.isRoot ? harness.getFileScope() : parent.fileScope);
170+
this.namePath = isRoot ? kEmptyNamePath :
171+
ObjectFreeze(getNamePath(parent, name));
172+
this.suiteId = isRoot ? null : JSONStringify([
173+
this.loc.file,
174+
this.namePath,
175+
]);
176+
this.parentId = isRoot || parent.isRoot ? null : parent.suiteId;
166177
this.only = validated.only;
167178
this.skip = validated.skip;
168179
this.tags = validated.tags;
169-
this.isRoot = isRoot;
170180
this.children = [];
171181
this.hooks = {
172182
__proto__: null,
@@ -214,17 +224,15 @@ class Bench extends AsyncResource {
214224
this.warmup = warmup;
215225
this.timeout = timeout;
216226
this.outerSignal = signal;
217-
this.namePath = getNamePath(parent, name);
227+
this.fileScope = parent.isRoot ? harness.getFileScope() : parent.fileScope;
228+
this.namePath = ObjectFreeze(getNamePath(parent, name));
218229
this.fullName = ArrayPrototypeJoin(this.namePath, ' ');
219230
this.benchId = JSONStringify([
220231
this.loc.file,
221232
this.namePath,
222233
this.params,
223234
]);
224-
this.parentId = parent.isRoot ? null : JSONStringify([
225-
this.loc.file,
226-
getNamePath(parent.parent, parent.name),
227-
]);
235+
this.parentId = parent.suiteId;
228236
this.finished = false;
229237
this.result = null;
230238
this.completion = PromiseWithResolvers();

lib/internal/bench_runner/cli.js

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const {
44
ArrayFrom,
5+
ArrayIsArray,
56
ArrayPrototypeFilter,
67
ArrayPrototypeIncludes,
78
ArrayPrototypeJoin,
@@ -32,6 +33,9 @@ const {
3233
kBenchmarksStreamDrain,
3334
} = require('internal/bench_runner/benchmarks_stream');
3435
const {
36+
configureRunScope,
37+
createRunId,
38+
runInFileScope,
3539
runBenchmarks,
3640
} = require('internal/bench_runner/harness');
3741
const { deserializeError, serializeError } = require('internal/error_serdes');
@@ -116,6 +120,19 @@ function createChildFileList(patterns, cwd) {
116120
return null;
117121
}
118122

123+
function createFileScopes(files, options) {
124+
const scopes = [];
125+
for (let i = 0; i < files.length; i++) {
126+
ArrayPrototypePush(scopes, {
127+
__proto__: null,
128+
entryFile: resolve(options.cwd, files[i]),
129+
fileRunId: options.isChild && i === 0 &&
130+
options.fileRunId !== undefined ? options.fileRunId : createRunId(),
131+
});
132+
}
133+
return scopes;
134+
}
135+
119136
function parseNamePattern(value) {
120137
if (value.length === 0) return undefined;
121138
try {
@@ -163,11 +180,15 @@ function parseCommandLine() {
163180
__proto__: null,
164181
cwd: process.cwd(),
165182
destinations,
183+
fileRunId: isChild && process.env.NODE_BENCH_FILE_RUN_ID ?
184+
process.env.NODE_BENCH_FILE_RUN_ID : undefined,
166185
isChild,
167186
isolation: getOptionValue('--bench-isolation'),
168187
namePattern: parseNamePattern(getOptionValue('--bench-name-pattern')),
169188
namePatternSource: getOptionValue('--bench-name-pattern'),
170189
reporters,
190+
runId: isChild && process.env.NODE_BENCH_RUN_ID ?
191+
process.env.NODE_BENCH_RUN_ID : undefined,
171192
samples,
172193
warmup,
173194
};
@@ -292,7 +313,24 @@ function deserializeRecord(record) {
292313
function validateRecord(record) {
293314
if (record === null || typeof record !== 'object' ||
294315
!kEventTypes.has(record.type) || record.data === null ||
295-
typeof record.data !== 'object') {
316+
typeof record.data !== 'object' ||
317+
typeof record.data.runId !== 'string' ||
318+
(record.data.fileRunId !== null &&
319+
typeof record.data.fileRunId !== 'string') ||
320+
(record.data.entryFile !== null &&
321+
typeof record.data.entryFile !== 'string')) {
322+
throw new ERR_INVALID_ARG_VALUE(
323+
'benchmark child message', record, 'is not a valid benchmark record');
324+
}
325+
if ((record.type === 'bench:start' || record.type === 'bench:sample' ||
326+
record.type === 'bench:complete') &&
327+
(typeof record.data.benchId !== 'string' ||
328+
(record.data.parentId !== null &&
329+
typeof record.data.parentId !== 'string') ||
330+
typeof record.data.name !== 'string' ||
331+
!ArrayIsArray(record.data.namePath) ||
332+
ArrayPrototypeSome(
333+
record.data.namePath, (name) => typeof name !== 'string'))) {
296334
throw new ERR_INVALID_ARG_VALUE(
297335
'benchmark child message', record, 'is not a valid benchmark record');
298336
}
@@ -345,14 +383,17 @@ async function loadBenchmarkFiles(files, options, modules, onRecord) {
345383
for (let i = 0; i < files.length; i++) {
346384
const file = resolve(options.cwd, files[i]);
347385
try {
348-
await loader.import(pathToFileURL(file), parentURL, kEmptyObject);
386+
await runInFileScope(options.fileScopes[i], () =>
387+
loader.import(pathToFileURL(file), parentURL, kEmptyObject));
349388
} catch (error) {
350389
loadFailed = true;
351390
await onRecord({
352391
__proto__: null,
353392
type: 'bench:diagnostic',
354393
data: {
355394
__proto__: null,
395+
runId: options.runId,
396+
...options.fileScopes[i],
356397
message: error?.message ?? String(error),
357398
error,
358399
level: 'error',
@@ -377,6 +418,10 @@ async function loadBenchmarkFiles(files, options, modules, onRecord) {
377418
if (record.type === 'bench:summary') {
378419
record.data.file = files.length === 1 ?
379420
resolve(options.cwd, files[0]) : null;
421+
record.data.fileRunId = files.length === 1 ?
422+
options.fileScopes[0].fileRunId : null;
423+
record.data.entryFile = files.length === 1 ?
424+
options.fileScopes[0].entryFile : null;
380425
summary = record.data;
381426
}
382427
await onRecord(record);
@@ -445,14 +490,16 @@ function getChildArgs(path, options) {
445490
return args;
446491
}
447492

448-
async function runChild(path, options, onRecord) {
493+
async function runChild(path, options, scope, onRecord) {
449494
const child = spawn(process.execPath, getChildArgs(path, options), {
450495
__proto__: null,
451496
cwd: options.cwd,
452497
env: {
453498
__proto__: null,
454499
...process.env,
455500
NODE_BENCH_CONTEXT: 'child',
501+
NODE_BENCH_FILE_RUN_ID: scope.fileRunId,
502+
NODE_BENCH_RUN_ID: options.runId,
456503
},
457504
serialization: 'advanced',
458505
stdio: ['inherit', 'pipe', 'pipe', 'ipc'],
@@ -487,6 +534,8 @@ async function runChild(path, options, onRecord) {
487534
type: 'bench:diagnostic',
488535
data: {
489536
__proto__: null,
537+
runId: options.runId,
538+
...scope,
490539
message,
491540
level: 'info',
492541
file: path,
@@ -506,8 +555,15 @@ async function runChild(path, options, onRecord) {
506555
child.on('message', (message) => {
507556
if (message?.type !== kChildMessageType) return;
508557
try {
509-
const pending = handleRecord(
510-
deserializeRecord(validateRecord(message.record)));
558+
const record = deserializeRecord(validateRecord(message.record));
559+
record.data.runId = options.runId;
560+
if (record.data.fileRunId !== null) {
561+
record.data.fileRunId = scope.fileRunId;
562+
}
563+
if (record.data.entryFile !== null) {
564+
record.data.entryFile = scope.entryFile;
565+
}
566+
const pending = handleRecord(record);
511567
trackPending(pending);
512568
} catch (error) {
513569
protocolError = error;
@@ -533,10 +589,11 @@ async function runIsolated(files, options, output) {
533589

534590
for (let i = 0; i < files.length; i++) {
535591
const path = files[i];
592+
const scope = options.fileScopes[i];
536593
let childSummary;
537594
let result;
538595
try {
539-
result = await runChild(path, options, (record) => {
596+
result = await runChild(path, options, scope, (record) => {
540597
if (record.type === 'bench:summary') {
541598
childSummary = record.data;
542599
return;
@@ -547,6 +604,8 @@ async function runIsolated(files, options, output) {
547604
success = false;
548605
output.diagnostic({
549606
__proto__: null,
607+
runId: options.runId,
608+
...scope,
550609
message: error.message,
551610
error,
552611
level: 'error',
@@ -570,6 +629,8 @@ async function runIsolated(files, options, output) {
570629
`exit code ${result.code}` : `signal ${result.signal}`;
571630
output.diagnostic({
572631
__proto__: null,
632+
runId: options.runId,
633+
...scope,
573634
message: `Benchmark file '${path}' failed with ${status}`,
574635
level: 'error',
575636
file: path,
@@ -578,8 +639,12 @@ async function runIsolated(files, options, output) {
578639
}
579640
}
580641

642+
const scope = files.length === 1 ? options.fileScopes[0] : null;
581643
const summary = {
582644
__proto__: null,
645+
runId: options.runId,
646+
fileRunId: scope?.fileRunId ?? null,
647+
entryFile: scope?.entryFile ?? null,
583648
success: success && (process.exitCode ?? 0) === 0,
584649
counts,
585650
duration_ns: hrtime() - start,
@@ -596,6 +661,19 @@ async function run(patterns) {
596661
createBenchmarkFileList(patterns, options.cwd);
597662
if (files === null) return { __proto__: null, success: false };
598663

664+
options.runId ??= createRunId();
665+
options.fileScopes = createFileScopes(files, options);
666+
const scope = files.length === 1 ? options.fileScopes[0] : {
667+
__proto__: null,
668+
entryFile: null,
669+
fileRunId: options.runId,
670+
};
671+
configureRunScope({
672+
__proto__: null,
673+
runId: options.runId,
674+
...scope,
675+
});
676+
599677
if (options.isChild) {
600678
try {
601679
const modules = await loadUserImports(options);

0 commit comments

Comments
 (0)