From 4093ea488718162b343c1bd83dced644a3646869 Mon Sep 17 00:00:00 2001 From: Luan Muniz Date: Fri, 5 Jun 2026 17:02:48 +0200 Subject: [PATCH 1/3] benchmark: add test runner hooks and options Add benchmarks for node:test hooks and test options. The hooks benchmark covers before, after, beforeEach, and afterEach, with a none mode as the baseline. The test options benchmark covers skip and todo behavior. This adds coverage for part of the benchmark/test_runner gaps tracked in the issue. Refs: https://github.com/nodejs/node/issues/55723 Signed-off-by: Luan Muniz --- benchmark/test_runner/hooks.js | 63 ++++++++++++++++++ benchmark/test_runner/test-options.js | 94 +++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 benchmark/test_runner/hooks.js create mode 100644 benchmark/test_runner/test-options.js diff --git a/benchmark/test_runner/hooks.js b/benchmark/test_runner/hooks.js new file mode 100644 index 00000000000000..b4d80c4b8e8c4c --- /dev/null +++ b/benchmark/test_runner/hooks.js @@ -0,0 +1,63 @@ +'use strict'; + +const common = require('../common'); +const { finished } = require('node:stream/promises'); +const reporter = require('../fixtures/empty-test-reporter'); +const { + after, + afterEach, + before, + beforeEach, + describe, + it, +} = require('node:test'); + +const bench = common.createBenchmark(main, { + n: [10, 100, 1000], + hook: ['none', 'before', 'after', 'beforeEach', 'afterEach'], +}, { + // We don't want to test the reporter here. + flags: ['--test-reporter=./benchmark/fixtures/empty-test-reporter.js'], +}); + +async function run({ n, hook }) { + // eslint-disable-next-line no-unused-vars + let avoidV8Optimization; + + const noop = () => {}; + + for (let i = 0; i < n; i++) { + describe(`${i}`, () => { + switch (hook) { + case 'before': + before(noop); + break; + case 'after': + after(noop); + break; + case 'beforeEach': + beforeEach(noop); + break; + case 'afterEach': + afterEach(noop); + break; + case 'none': + break; + } + + it(`${i}`, () => { + avoidV8Optimization = i; + }); + }); + } + + await finished(reporter); +} + +function main(params) { + bench.start(); + + run(params).then(() => { + bench.end(params.n); + }); +} diff --git a/benchmark/test_runner/test-options.js b/benchmark/test_runner/test-options.js new file mode 100644 index 00000000000000..8ce6886d5cf25e --- /dev/null +++ b/benchmark/test_runner/test-options.js @@ -0,0 +1,94 @@ +'use strict'; + +const common = require('../common'); +const { finished } = require('node:stream/promises'); +const reporter = require('../fixtures/empty-test-reporter'); +const { it } = require('node:test'); + +const bench = common.createBenchmark(main, { + n: [100, 1000], + option: [ + 'none', + 'skip', + 'skip-with-message', + 'skip-method', + 'skip-method-with-message', + 'todo', + 'todo-with-message', + 'todo-method', + 'todo-method-with-message', + ], +}, { + // We don't want to test the reporter here. + flags: ['--test-reporter=./benchmark/fixtures/empty-test-reporter.js'], +}); + +async function run({ n, option }) { + // eslint-disable-next-line no-unused-vars + let avoidV8Optimization; + + for (let i = 0; i < n; i++) { + switch (option) { + case 'none': + it(`${i}`, () => { + avoidV8Optimization = i; + }); + break; + case 'skip': + it(`${i}`, { skip: true }, () => { + throw new Error('This test should not run.'); + }); + break; + case 'skip-with-message': + it(`${i}`, { skip: 'skip reason' }, () => { + throw new Error('This test should not run.'); + }); + break; + case 'skip-method': + it(`${i}`, (t) => { + avoidV8Optimization = i; + t.skip(); + }); + break; + case 'skip-method-with-message': + it(`${i}`, (t) => { + avoidV8Optimization = i; + t.skip('skip reason'); + }); + break; + case 'todo': + it(`${i}`, { todo: true }, () => { + avoidV8Optimization = i; + }); + break; + case 'todo-with-message': + it(`${i}`, { todo: 'todo reason' }, () => { + avoidV8Optimization = i; + }); + break; + case 'todo-method': + it(`${i}`, (t) => { + avoidV8Optimization = i; + t.todo(); + }); + break; + case 'todo-method-with-message': + it(`${i}`, (t) => { + avoidV8Optimization = i; + t.todo('todo reason'); + }); + break; + } + } + + await finished(reporter); + return n; +} + +function main(params) { + bench.start(); + + run(params).then((ops) => { + bench.end(ops); + }); +} From b8487cd21806cec68bbdb3d3126376c4cb34cb05 Mon Sep 17 00:00:00 2001 From: Luan Muniz Date: Sun, 7 Jun 2026 21:27:27 +0200 Subject: [PATCH 2/3] benchmark: avoid switch in test runner benchmarks Select each benchmark variant before starting the timer so the measured loop does not include dispatch overhead. Signed-off-by: Luan Muniz --- benchmark/test_runner/hooks.js | 43 +++---- benchmark/test_runner/test-options.js | 154 +++++++++++++++----------- 2 files changed, 110 insertions(+), 87 deletions(-) diff --git a/benchmark/test_runner/hooks.js b/benchmark/test_runner/hooks.js index b4d80c4b8e8c4c..371cfe9d35ef61 100644 --- a/benchmark/test_runner/hooks.js +++ b/benchmark/test_runner/hooks.js @@ -13,37 +13,26 @@ const { } = require('node:test'); const bench = common.createBenchmark(main, { - n: [10, 100, 1000], - hook: ['none', 'before', 'after', 'beforeEach', 'afterEach'], + n: [1, 10, 100, 1000], + hook: ['before', 'after', 'beforeEach', 'afterEach'], }, { // We don't want to test the reporter here. flags: ['--test-reporter=./benchmark/fixtures/empty-test-reporter.js'], }); -async function run({ n, hook }) { - // eslint-disable-next-line no-unused-vars - let avoidV8Optimization; +const noop = () => {}; - const noop = () => {}; +const hookList = { + before: before, + after: after, + beforeEach: beforeEach, + afterEach: afterEach, +}; - for (let i = 0; i < n; i++) { +function run(loopAmount, avoidV8Optimization, hookFn) { + for (let i = 0; i < loopAmount; i++) { describe(`${i}`, () => { - switch (hook) { - case 'before': - before(noop); - break; - case 'after': - after(noop); - break; - case 'beforeEach': - beforeEach(noop); - break; - case 'afterEach': - afterEach(noop); - break; - case 'none': - break; - } + hookFn(noop); it(`${i}`, () => { avoidV8Optimization = i; @@ -51,13 +40,17 @@ async function run({ n, hook }) { }); } - await finished(reporter); + return finished(reporter); } function main(params) { + // eslint-disable-next-line prefer-const + let avoidV8Optimization = 0; + const hookFn = hookList[params.hook]; + bench.start(); - run(params).then(() => { + run(params.n, avoidV8Optimization, hookFn).then(() => { bench.end(params.n); }); } diff --git a/benchmark/test_runner/test-options.js b/benchmark/test_runner/test-options.js index 8ce6886d5cf25e..2b76784ca541ef 100644 --- a/benchmark/test_runner/test-options.js +++ b/benchmark/test_runner/test-options.js @@ -6,7 +6,7 @@ const reporter = require('../fixtures/empty-test-reporter'); const { it } = require('node:test'); const bench = common.createBenchmark(main, { - n: [100, 1000], + n: [1, 10, 100, 1000], option: [ 'none', 'skip', @@ -23,72 +23,102 @@ const bench = common.createBenchmark(main, { flags: ['--test-reporter=./benchmark/fixtures/empty-test-reporter.js'], }); -async function run({ n, option }) { - // eslint-disable-next-line no-unused-vars - let avoidV8Optimization; +const allTests = { + 'none': (loopAmount, avoidV8Optimization) => { + for (let i = 0; i < loopAmount; i++) { + it(`${i}`, () => { + avoidV8Optimization = i; + }); + } - for (let i = 0; i < n; i++) { - switch (option) { - case 'none': - it(`${i}`, () => { - avoidV8Optimization = i; - }); - break; - case 'skip': - it(`${i}`, { skip: true }, () => { - throw new Error('This test should not run.'); - }); - break; - case 'skip-with-message': - it(`${i}`, { skip: 'skip reason' }, () => { - throw new Error('This test should not run.'); - }); - break; - case 'skip-method': - it(`${i}`, (t) => { - avoidV8Optimization = i; - t.skip(); - }); - break; - case 'skip-method-with-message': - it(`${i}`, (t) => { - avoidV8Optimization = i; - t.skip('skip reason'); - }); - break; - case 'todo': - it(`${i}`, { todo: true }, () => { - avoidV8Optimization = i; - }); - break; - case 'todo-with-message': - it(`${i}`, { todo: 'todo reason' }, () => { - avoidV8Optimization = i; - }); - break; - case 'todo-method': - it(`${i}`, (t) => { - avoidV8Optimization = i; - t.todo(); - }); - break; - case 'todo-method-with-message': - it(`${i}`, (t) => { - avoidV8Optimization = i; - t.todo('todo reason'); - }); - break; + return finished(reporter); + }, + 'skip': (loopAmount) => { + for (let i = 0; i < loopAmount; i++) { + it(`${i}`, { skip: true }, () => { + throw new Error('This test should not run.'); + }); } - } - await finished(reporter); - return n; -} + return finished(reporter); + }, + 'skip-with-message': (loopAmount) => { + for (let i = 0; i < loopAmount; i++) { + it(`${i}`, { skip: 'skip reason' }, () => { + throw new Error('This test should not run.'); + }); + } + + return finished(reporter); + }, + 'skip-method': (loopAmount, avoidV8Optimization) => { + for (let i = 0; i < loopAmount; i++) { + it(`${i}`, (t) => { + avoidV8Optimization = i; + t.skip(); + }); + } + + return finished(reporter); + }, + 'skip-method-with-message': (loopAmount, avoidV8Optimization) => { + for (let i = 0; i < loopAmount; i++) { + it(`${i}`, (t) => { + avoidV8Optimization = i; + t.skip('skip reason'); + }); + } + + return finished(reporter); + }, + 'todo': (loopAmount, avoidV8Optimization) => { + for (let i = 0; i < loopAmount; i++) { + it(`${i}`, { todo: true }, () => { + avoidV8Optimization = i; + }); + } + + return finished(reporter); + }, + 'todo-with-message': (loopAmount, avoidV8Optimization) => { + for (let i = 0; i < loopAmount; i++) { + it(`${i}`, { todo: 'todo reason' }, () => { + avoidV8Optimization = i; + }); + } + + return finished(reporter); + }, + 'todo-method': (loopAmount, avoidV8Optimization) => { + for (let i = 0; i < loopAmount; i++) { + it(`${i}`, (t) => { + avoidV8Optimization = i; + t.todo(); + }); + } + + return finished(reporter); + }, + 'todo-method-with-message': (loopAmount, avoidV8Optimization) => { + for (let i = 0; i < loopAmount; i++) { + it(`${i}`, (t) => { + avoidV8Optimization = i; + t.todo('todo reason'); + }); + } + + return finished(reporter); + }, +}; + +function main({ n, option }) { + // eslint-disable-next-line prefer-const + let avoidV8Optimization = 0; + const runOption = allTests[option]; -function main(params) { bench.start(); - run(params).then((ops) => { - bench.end(ops); + runOption(n, avoidV8Optimization).then(() => { + bench.end(n); }); } From d00ebb9c0f89f9d865d2e01e24d0f2d57f826530 Mon Sep 17 00:00:00 2001 From: Luan Muniz Date: Mon, 8 Jun 2026 15:20:40 +0200 Subject: [PATCH 3/3] benchmark: exercise test runner hook bodies Update the hooks benchmark so registered hooks perform the same anti-optimization assignment as test bodies instead of calling a noop. This keeps the measured hook path from using an empty callback. Links for more information on this actions: https://github.com/nodejs/node/pull/48931#discussion_r1275465052 https://www.mail-archive.com/v8-users@googlegroups.com/msg05521.html Signed-off-by: Luan Muniz --- benchmark/test_runner/hooks.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/benchmark/test_runner/hooks.js b/benchmark/test_runner/hooks.js index 371cfe9d35ef61..cee8cdbfa47da2 100644 --- a/benchmark/test_runner/hooks.js +++ b/benchmark/test_runner/hooks.js @@ -20,8 +20,6 @@ const bench = common.createBenchmark(main, { flags: ['--test-reporter=./benchmark/fixtures/empty-test-reporter.js'], }); -const noop = () => {}; - const hookList = { before: before, after: after, @@ -32,7 +30,9 @@ const hookList = { function run(loopAmount, avoidV8Optimization, hookFn) { for (let i = 0; i < loopAmount; i++) { describe(`${i}`, () => { - hookFn(noop); + hookFn(() => { + avoidV8Optimization = i; + }); it(`${i}`, () => { avoidV8Optimization = i;