@ 261105.16
current
'use strict';

const common = require('../common');
const assert = require('assert');
const { spawnSync } = require('child_process');
const fs = require('fs');
const fixtures = require('../common/fixtures');
const tmpdir = require('../common/tmpdir');

const basicPattern = fixtures.path('bench-runner/[ab].*');
const spawnTimeout = common.platformTimeout(30_000);

tmpdir.refresh();

function spawnNode(args, options = undefined) {
  const result = spawnSync(process.execPath, args, {
    __proto__: null,
    encoding: 'utf8',
    timeout: spawnTimeout,
    ...options,
  });
  assert.ifError(result.error);
  assert.strictEqual(result.signal, null);
  return result;
}

function spawnBench(args, options = undefined) {
  return spawnNode([
    '--no-warnings', '--experimental-bench', '--bench', ...args,
  ], options);
}

function parseRecords(result) {
  assert.strictEqual(result.stderr, '');
  return parseOutput(result.stdout);
}

function parseOutput(output) {
  return output.trim().split('\n').map((line) => JSON.parse(line));
}

for (const option of [
  '--bench',
  '--bench-isolation=process',
  '--bench-name-pattern=benchmark',
  '--bench-reporter=spec',
  '--bench-reporter-destination=stdout',
  '--bench-samples=1',
  '--bench-warmup=0',
]) {
  const result = spawnNode([option]);
  assert.notStrictEqual(result.status, 0);
  assert.match(result.stderr, /--experimental-bench is required/);
}

{
  const result = spawnBench([]);
  assert.strictEqual(result.status, 1);
  assert.strictEqual(result.stdout, '');
  assert.match(result.stderr, /--bench requires at least one file or glob/);
}

{
  const result = spawnBench(['does-not-exist-*.js']);
  assert.strictEqual(result.status, 1);
  assert.strictEqual(result.stdout, '');
  assert.match(result.stderr, /^Could not find/);
}

if (common.canCreateSymLink()) {
  const dangling = tmpdir.resolve('dangling.cjs');
  fs.symlinkSync(tmpdir.resolve('missing.cjs'), dangling);
  const result = spawnBench([dangling]);
  assert.strictEqual(result.status, 1);
  assert.strictEqual(result.stdout, '');
  assert.match(result.stderr, /^Could not find/);
}

for (const { patterns, message } of [
  {
    patterns: [
      fixtures.path('bench-runner/a.cjs'),
      fixtures.path('bench-runner/b.mjs'),
    ],
    message: /benchmark child process requires exactly one file/,
  },
  {
    patterns: [fixtures.path('bench-runner/missing.cjs')],
    message: /^Could not find/,
  },
]) {
  const result = spawnNode([
    '--no-warnings',
    '--experimental-bench',
    '--require', fixtures.path('bench-runner/fake-ipc.cjs'),
    '--bench',
    ...patterns,
  ], {
    __proto__: null,
    env: { __proto__: null, ...process.env, NODE_BENCH_CONTEXT: 'child' },
  });
  assert.strictEqual(result.status, 1);
  assert.strictEqual(result.stdout, '');
  assert.match(result.stderr, message);
}

{
  const result = spawnBench(['--bench-reporter=json', basicPattern]);
  assert.strictEqual(result.status, 0);
  const records = parseRecords(result);
  const plans = records.filter(
    ({ type }) => type === 'bench:plan').map(({ data }) => data);
  const completions = records.filter(
    ({ type }) => type === 'bench:complete');

  assert.deepStrictEqual(plans.map(({ name }) => name), ['alpha', 'beta']);
  assert.deepStrictEqual(completions.map(({ data }) => data.name), [
    'alpha',
    'beta',
  ]);
  assert.notStrictEqual(
    completions[0].data.params.pid,
    completions[1].data.params.pid,
  );
  assert.deepStrictEqual(completions.map(({ data }) => data.params.file), [
    'a',
    'b',
  ]);
  assert.match(completions[0].data.samples[0].duration_ns, /^\d+$/);
  assert(Number.isFinite(completions[0].data.summary.mean));

  const summaries = records.filter(
    ({ type }) => type === 'bench:summary');
  assert.strictEqual(summaries.length, 1);
  assert.strictEqual(summaries[0].data.file, null);
  assert.deepStrictEqual(summaries[0].data.counts, {
    completed: 2,
    failed: 0,
    skipped: 0,
    total: 2,
  });
}

for (const isolation of ['process', 'none']) {
  const result = spawnBench([
    `--bench-isolation=${isolation}`,
    '--bench-reporter=json',
    fixtures.path('bench-runner/identity-entry-*.cjs'),
  ]);
  assert.strictEqual(result.status, 0);
  const records = parseRecords(result);
  const plans = records.filter(
    ({ type }) => type === 'bench:plan').map(({ data }) => data);
  const completions = records.filter(
    ({ type }) => type === 'bench:complete').map(({ data }) => data);
  const summary = records.at(-1).data;

  assert.strictEqual(plans.length, 2);
  for (const plan of plans) {
    const planIndex = records.findIndex(({ type, data }) =>
      type === 'bench:plan' && data.fileRunId === plan.fileRunId);
    const startIndex = records.findIndex(({ type, data }) =>
      type === 'bench:start' && data.fileRunId === plan.fileRunId);
    assert(planIndex < startIndex);
  }
  assert.strictEqual(completions.length, 2);
  assert.strictEqual(completions[0].benchId, completions[1].benchId);
  assert.strictEqual(completions[0].runId, completions[1].runId);
  assert.strictEqual(completions[0].runId, summary.runId);
  assert.notStrictEqual(
    completions[0].fileRunId, completions[1].fileRunId);
  assert.deepStrictEqual(completions.map(({ entryFile }) => entryFile), [
    fixtures.path('bench-runner/identity-entry-a.cjs'),
    fixtures.path('bench-runner/identity-entry-b.cjs'),
  ]);
  assert.deepStrictEqual(completions.map(({ namePath }) => namePath), [
    ['shared suite', 'shared identity'],
    ['shared suite', 'shared identity'],
  ]);
  assert.strictEqual(summary.fileRunId, null);
  assert.strictEqual(summary.entryFile, null);
}

{
  const result = spawnBench([
    '--bench-reporter=json',
    fixtures.path('bench-runner/identity-suite.cjs'),
  ]);
  assert.strictEqual(result.status, 0);
  const records = parseRecords(result);
  const completions = records.filter(
    ({ type }) => type === 'bench:complete').map(({ data }) => data);
  const parentId = JSON.stringify([
    fixtures.path('bench-runner/identity-suite.cjs'),
    ['cross-module suite'],
  ]);

  assert.deepStrictEqual(completions.map(({ name }) => name), [
    'child a',
    'child b',
  ]);
  assert(completions.every((completion) =>
    completion.parentId === parentId));
  assert.deepStrictEqual(completions.map(({ namePath }) => namePath), [
    ['cross-module suite', 'child a'],
    ['cross-module suite', 'child b'],
  ]);
  assert(completions.every(({ entryFile }) =>
    entryFile === fixtures.path('bench-runner/identity-suite.cjs')));
}

for (const isolation of ['process', 'none']) {
  const result = spawnBench([
    '--require', fixtures.path('bench-runner/identity-preload.cjs'),
    `--bench-isolation=${isolation}`,
    '--bench-reporter=json',
    fixtures.path('bench-runner/identity-entry-*.cjs'),
  ]);
  assert.strictEqual(result.status, 0);
  const records = parseRecords(result);
  const preloads = records.filter(
    ({ type, data }) => type === 'bench:complete' &&
      data.name === 'preload identity').map(({ data }) => data);
  assert.strictEqual(preloads.length, isolation === 'process' ? 2 : 1);
  assert.strictEqual(
    new Set(preloads.map(({ fileRunId }) => fileRunId)).size,
    preloads.length,
  );
  assert(preloads.every(({ entryFile }) => entryFile === null));
  assert(preloads.every(
    ({ runId }) => runId === records.at(-1).data.runId));
}

{
  const result = spawnBench([
    '--bench-isolation=none',
    '--bench-reporter=json',
    fixtures.path('bench-runner/a.cjs'),
    fixtures.path('bench-runner/identity-hook.cjs'),
  ]);
  assert.strictEqual(result.status, 1);
  const records = parseRecords(result);
  const diagnostic = records.find(
    ({ type, data }) => type === 'bench:diagnostic' &&
      data.message === 'scoped hook failed').data;
  const completion = records.find(
    ({ type, data }) => type === 'bench:complete' &&
      data.name === 'scoped hook benchmark').data;
  assert.strictEqual(diagnostic.entryFile,
                     fixtures.path('bench-runner/identity-hook.cjs'));
  assert.strictEqual(diagnostic.fileRunId, completion.fileRunId);
}

{
  const result = spawnBench([
    '--bench-reporter=json',
    '--bench-isolation=none',
    basicPattern,
  ]);
  assert.strictEqual(result.status, 0);
  const records = parseRecords(result);
  const completions = records.filter(
    ({ type }) => type === 'bench:complete');
  assert.strictEqual(completions.length, 2);
  assert.strictEqual(
    completions[0].data.params.pid,
    completions[1].data.params.pid,
  );
  assert.strictEqual(records.at(-1).data.file, null);
}

{
  const result = spawnBench([
    '--bench-reporter=json',
    '--bench-name-pattern=selected',
    '--bench-samples=2',
    '--bench-warmup=3',
    fixtures.path('bench-runner/options.cjs'),
  ]);
  assert.strictEqual(result.status, 0);
  const records = parseRecords(result);
  const plans = records.filter(
    ({ type }) => type === 'bench:plan').map(({ data }) => data);
  const samples = records.filter(({ type }) => type === 'bench:sample');
  assert.deepStrictEqual(samples.map(({ data }) => data.operations), [4, 5]);
  assert.deepStrictEqual(plans.map((plan) => ({
    name: plan.name,
    samples: plan.samples,
    selected: plan.selected,
    skip: plan.skip,
    timeout: plan.timeout,
    warmup: plan.warmup,
    yieldBetweenSamples: plan.yieldBetweenSamples,
  })), [
    {
      name: 'selected',
      samples: 2,
      selected: true,
      skip: undefined,
      timeout: null,
      warmup: 3,
      yieldBetweenSamples: true,
    },
    {
      name: 'filtered out',
      samples: 2,
      selected: false,
      skip: 'name pattern',
      timeout: null,
      warmup: 3,
      yieldBetweenSamples: true,
    },
  ]);

  const completions = records.filter(
    ({ type }) => type === 'bench:complete');
  const selected = completions.find(({ data }) => data.name === 'selected').data;
  assert.deepStrictEqual(selected.params, {
    boolean: true,
    number: 42,
    string: 'value',
  });
  assert.strictEqual(selected.samples.length, 2);
  assert.strictEqual(
    selected.summary.mean,
    (selected.samples[0].rate + selected.samples[1].rate) / 2,
  );
  assert(Number.isFinite(selected.summary.medianConfidenceInterval.lower));

  const filtered = completions.find(
    ({ data }) => data.name === 'filtered out').data;
  assert.strictEqual(filtered.skip, 'name pattern');
}

{
  const result = spawnBench([
    '--require',
    fixtures.path('bench-runner/preload.cjs'),
    '--bench-reporter=json',
    fixtures.path('bench-runner/preloaded.cjs'),
  ]);
  assert.strictEqual(result.status, 0);
  const records = parseRecords(result);
  const completion = records.find(
    ({ type }) => type === 'bench:complete');
  assert.deepStrictEqual(completion.data.params, { preloaded: true });
}

for (const isolation of ['process', 'none']) {
  const result = spawnBench([
    `--bench-isolation=${isolation}`,
    '--bench-reporter=json',
    '--import', fixtures.fileURL('bench-runner/import.mjs'),
    fixtures.path('bench-runner/imported.cjs'),
  ]);
  assert.strictEqual(result.status, 0);
  const records = parseRecords(result);
  const completion = records.find(
    ({ type }) => type === 'bench:complete');
  assert.deepStrictEqual(completion.data.params, { imported: 'loaded' });
}

{
  const lock = tmpdir.resolve('serial.lock');
  const result = spawnBench([
    '--bench-reporter=json',
    fixtures.path('bench-runner/serial-[ab].cjs'),
  ], {
    env: { __proto__: null, ...process.env, NODE_BENCH_LOCK: lock },
  });
  assert.strictEqual(result.status, 0);
  const records = parseRecords(result);
  assert.deepStrictEqual(records.filter(
    ({ type }) => type === 'bench:complete').map(({ data }) => data.name), [
    'serial a',
    'serial b',
  ]);
  assert.strictEqual(fs.existsSync(lock), false);
}

{
  const result = spawnBench([
    '--bench-reporter=json',
    fixtures.path('bench-runner/ipc.cjs'),
  ]);
  assert.strictEqual(result.status, 0);
  const records = parseRecords(result);
  assert.strictEqual(records.find(
    ({ type }) => type === 'bench:complete').data.name, 'user IPC');
  assert.strictEqual(records.at(-1).data.file,
                     fixtures.path('bench-runner/ipc.cjs'));
}

{
  const result = spawnBench([
    '--bench-reporter=json',
    '--random-seed=17',
    '--stack-trace-limit=17',
    fixtures.path('bench-runner/v8-option.cjs'),
  ]);
  assert.strictEqual(result.status, 0);
  const records = parseRecords(result);
  assert.strictEqual(records.find(
    ({ type }) => type === 'bench:complete').data.name, 'V8 option');
}

if (common.hasInspector) {
  const result = spawnBench([
    '--inspect=0',
    '--bench-reporter=json',
    fixtures.path('bench-runner/inspector.cjs'),
  ]);
  assert.strictEqual(result.status, 0);
  const records = parseRecords(result);
  const diagnostics = records.filter(
    ({ type }) => type === 'bench:diagnostic')
    .map(({ data }) => data.message).join('');
  assert.match(diagnostics, /Debugger listening on ws:\/\//);
  const completion = records.find(
    ({ type }) => type === 'bench:complete').data;
  assert.strictEqual(completion.params.inspectPort, '--inspect-port=0');
}

{
  // --experimental-bench is passed on the command line rather than via the
  // config file's "nodeOptions" section because builds configured with
  // --without-node-options ignore that section entirely.
  const result = spawnNode([
    '--no-warnings',
    '--experimental-bench',
    `--config-file=${fixtures.path('bench-runner/node.config.json')}`,
    fixtures.path('bench-runner/a.cjs'),
  ]);
  assert.strictEqual(result.status, 0);
  const records = parseRecords(result);
  assert.strictEqual(records.find(
    ({ type }) => type === 'bench:complete').data.samples.length, 1);
}