From 11c11088e12d2b77547389eb0a5055ad3ff11427 Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Sat, 28 Dec 2024 17:41:43 -0300 Subject: [PATCH] feat: use biome linter (#34) * feat: use biome linter * fixup! feat: use biome linter * fixup! fixup! feat: use biome linter * fixup! fixup! fixup! feat: use biome linter * fixup! fixup! fixup! fixup! feat: use biome linter * fixup! fixup! fixup! fixup! fixup! feat: use biome linter * fixup! fixup! fixup! fixup! fixup! fixup! feat: use biome linter --- .github/workflows/test.yml | 3 + biome.json | 21 ++ lib/clock.js | 395 +++++++++++++++++--------------- lib/histogram.js | 280 ++++++++++++----------- lib/index.js | 406 +++++++++++++++++---------------- lib/lifecycle.js | 230 +++++++++++-------- lib/plugins.js | 81 +++---- lib/plugins/memory.js | 173 +++++++------- lib/plugins/v8-never-opt.js | 42 ++-- lib/plugins/v8-opt.js | 44 ++-- lib/plugins/v8-print-status.js | 162 ++++++------- lib/report.js | 16 +- lib/reporter/chart.js | 40 ++-- lib/reporter/html.js | 91 ++++---- lib/reporter/json.js | 40 ++-- lib/reporter/text.js | 72 +++--- lib/validators.js | 81 ++++--- lib/worker-runner.js | 25 +- package.json | 65 +++--- test/async.js | 6 +- test/basic.js | 382 +++++++++++++++++-------------- test/env.js | 180 +++++++++------ test/fixtures/bench.js | 8 +- test/fixtures/copy.js | 29 +-- test/fixtures/opt-managed.js | 68 +++--- test/managed.js | 6 +- test/plugins.js | 178 ++++++++------- test/reporter.js | 361 +++++++++++++++-------------- test/worker.js | 52 ++--- 29 files changed, 1890 insertions(+), 1647 deletions(-) create mode 100644 biome.json diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6a3cf80..88dab2a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,5 +29,8 @@ jobs: - name: Install dependencies run: npm install + - name: Run linter + run: npm run lint:ci + - name: Run tests run: npm test diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..e953a2a --- /dev/null +++ b/biome.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "noParameterAssign": "off" + } + } + }, + "formatter": { + "enabled": true + }, + "files": { + "ignore": ["examples/*", "test/plugin-api-doc.js"] + } +} diff --git a/lib/clock.js b/lib/clock.js index aecc121..d996f70 100644 --- a/lib/clock.js +++ b/lib/clock.js @@ -1,132 +1,132 @@ -const { debug, types } = require('node:util'); -const { validateNumber } = require('./validators'); +const { debug, types } = require("node:util"); +const { validateNumber } = require("./validators"); -let debugBench = debug('benchmark', (fn) => { - debugBench = fn; +let debugBench = debug("benchmark", (fn) => { + debugBench = fn; }); -const kUnmanagedTimerResult = Symbol('kUnmanagedTimerResult'); +const kUnmanagedTimerResult = Symbol("kUnmanagedTimerResult"); // If the smallest time measurement is 1ns // the minimum resolution of this timer is 0.5 const MIN_RESOLUTION = 0.5; class Timer { - constructor() { - this.now = process.hrtime.bigint; - } - - get scale() { - return 1e9; - } - - get resolution() { - return 1 / 1e9; - } - - /** - * @param {number} timeInNs - * @returns {string} - */ - format(timeInNs) { - validateNumber(timeInNs, 'timeInNs', 0); - - if (timeInNs > 1e9) { - return `${(timeInNs / 1e9).toFixed(2)}s`; - } - - if (timeInNs > 1e6) { - return `${(timeInNs / 1e6).toFixed(2)}ms`; - } - - if (timeInNs > 1e3) { - return `${(timeInNs / 1e3).toFixed(2)}us`; - } - - return `${(timeInNs).toFixed(2)}ns`; - } + constructor() { + this.now = process.hrtime.bigint; + } + + get scale() { + return 1e9; + } + + get resolution() { + return 1 / 1e9; + } + + /** + * @param {number} timeInNs + * @returns {string} + */ + format(timeInNs) { + validateNumber(timeInNs, "timeInNs", 0); + + if (timeInNs > 1e9) { + return `${(timeInNs / 1e9).toFixed(2)}s`; + } + + if (timeInNs > 1e6) { + return `${(timeInNs / 1e6).toFixed(2)}ms`; + } + + if (timeInNs > 1e3) { + return `${(timeInNs / 1e3).toFixed(2)}us`; + } + + return `${(timeInNs).toFixed(2)}ns`; + } } const timer = new Timer(); class ManagedTimer { - startTime; - endTime; - iterations; - recommendedCount; - - /** - * @param {number} recommendedCount - */ - constructor(recommendedCount) { - this.recommendedCount = recommendedCount; - } - - /** - * Returns the recommended value to be used to benchmark your code - * @returns {number} - */ - get count() { - return this.recommendedCount; - } - - /** - * Starts the timer - */ - start() { - this.startTime = timer.now(); - } - - /** - * Stops the timer - * @param {number} [iterations=1] The amount of iterations that run - */ - end(iterations = 1) { - this.endTime = timer.now(); - validateNumber(iterations, 'iterations', 1); - this.iterations = iterations; - } - - [kUnmanagedTimerResult](context) { - if (this.startTime === undefined) - throw new Error('You forgot to call .start()'); - - if (this.endTime === undefined) - throw new Error('You forgot to call .end(count)'); - - return [Number(this.endTime - this.startTime), this.iterations, context]; - } + startTime; + endTime; + iterations; + recommendedCount; + + /** + * @param {number} recommendedCount + */ + constructor(recommendedCount) { + this.recommendedCount = recommendedCount; + } + + /** + * Returns the recommended value to be used to benchmark your code + * @returns {number} + */ + get count() { + return this.recommendedCount; + } + + /** + * Starts the timer + */ + start() { + this.startTime = timer.now(); + } + + /** + * Stops the timer + * @param {number} [iterations=1] The amount of iterations that run + */ + end(iterations = 1) { + this.endTime = timer.now(); + validateNumber(iterations, "iterations", 1); + this.iterations = iterations; + } + + [kUnmanagedTimerResult](context) { + if (this.startTime === undefined) + throw new Error("You forgot to call .start()"); + + if (this.endTime === undefined) + throw new Error("You forgot to call .end(count)"); + + return [Number(this.endTime - this.startTime), this.iterations, context]; + } } function createRunUnmanagedBenchmark(bench, awaitOrEmpty) { - const varNames = { - awaitOrEmpty, - timer: 'timer', - context: 'context', - bench: 'bench', - }; - - let code = ` + const varNames = { + awaitOrEmpty, + timer: "timer", + context: "context", + bench: "bench", + }; + + let code = ` let i = 0; let ${varNames.context} = {}; `; - let benchFnCall = `${awaitOrEmpty}${varNames.bench}.fn()`; - const wrapFunctions = []; - for (const p of bench.plugins) { - if (typeof p.beforeClockTemplate === 'function') { - const [newCode, functionToCall] = p.beforeClockTemplate(varNames); - code += newCode; - if (functionToCall) { - wrapFunctions.push(functionToCall); - } - } - } - benchFnCall = wrapFunctions.reduce((prev, n) => { - return `${n}(${prev})` - }, benchFnCall); - - code += ` + let benchFnCall = `${awaitOrEmpty}${varNames.bench}.fn()`; + const wrapFunctions = []; + for (const p of bench.plugins) { + if (typeof p.beforeClockTemplate === "function") { + const [newCode, functionToCall] = p.beforeClockTemplate(varNames); + code += newCode; + if (functionToCall) { + wrapFunctions.push(functionToCall); + } + } + } + benchFnCall = wrapFunctions.reduce((prev, n) => { + return `${n}(${prev})`; + }, benchFnCall); + + code += ` const startedAt = ${varNames.timer}.now(); for (; i < count; i++) @@ -135,107 +135,126 @@ for (; i < count; i++) const duration = Number(${varNames.timer}.now() - startedAt); `; - for (const p of bench.plugins) { - if (typeof p.afterClockTemplate === 'function') { - [newCode] = p.afterClockTemplate(varNames); - code += newCode; - } - } + for (const p of bench.plugins) { + if (typeof p.afterClockTemplate === "function") { + [newCode] = p.afterClockTemplate(varNames); + code += newCode; + } + } - code += `return [duration, count, ${varNames.context}];`; - return code; + code += `return [duration, count, ${varNames.context}];`; + return code; } function createRunManagedBenchmark(bench, awaitOrEmpty) { - const varNames = { - awaitOrEmpty, - timer: 'timer', - context: 'context', - bench: 'bench', - }; - - let code = ` + const varNames = { + awaitOrEmpty, + timer: "timer", + context: "context", + bench: "bench", + }; + + let code = ` let i = 0; let ${varNames.context} = {}; `; - let benchFnCall = `${awaitOrEmpty}${varNames.bench}.fn(${varNames.timer})`; - const wrapFunctions = []; - for (const p of bench.plugins) { - if (typeof p.beforeClockTemplate === 'function') { - [newCode, functionToCall] = p.beforeClockTemplate(varNames); - code += newCode; - if (functionToCall) { - wrapFunctions.push(functionToCall); - } - } - } - benchFnCall = wrapFunctions.reduce((prev, n) => { - return `${n}(${prev})` - }, benchFnCall); - - code += ` + let benchFnCall = `${awaitOrEmpty}${varNames.bench}.fn(${varNames.timer})`; + const wrapFunctions = []; + for (const p of bench.plugins) { + if (typeof p.beforeClockTemplate === "function") { + [newCode, functionToCall] = p.beforeClockTemplate(varNames); + code += newCode; + if (functionToCall) { + wrapFunctions.push(functionToCall); + } + } + } + benchFnCall = wrapFunctions.reduce((prev, n) => { + return `${n}(${prev})`; + }, benchFnCall); + + code += ` ${benchFnCall}; const result = ${varNames.timer}[kUnmanagedTimerResult](${varNames.context}); -` - for (const p of bench.plugins) { - if (typeof p.afterClockTemplate === 'function'){ - [newCode] = p.afterClockTemplate(varNames); - code += newCode; - } - } - - code += 'return result;'; - return code; +`; + for (const p of bench.plugins) { + if (typeof p.afterClockTemplate === "function") { + [newCode] = p.afterClockTemplate(varNames); + code += newCode; + } + } + + code += "return result;"; + return code; } -const AsyncFunction = async function () { -}.constructor; -const SyncFunction = function () { -}.constructor; +const AsyncFunction = (async () => {}).constructor; +const SyncFunction = (() => {}).constructor; function createFnString(bench) { - const { isAsync, hasArg } = bench; - - const compiledFnStringFactory = hasArg ? createRunManagedBenchmark : createRunUnmanagedBenchmark; - const compiledFnString = compiledFnStringFactory(bench, isAsync ? 'await ' : ''); - return compiledFnString; + const { isAsync, hasArg } = bench; + + const compiledFnStringFactory = hasArg + ? createRunManagedBenchmark + : createRunUnmanagedBenchmark; + const compiledFnString = compiledFnStringFactory( + bench, + isAsync ? "await " : "", + ); + return compiledFnString; } function createRunner(bench, recommendedCount) { - const { isAsync, hasArg } = bench; - const compiledFnString = bench.fnStr; - - const createFnPrototype = isAsync ? AsyncFunction : SyncFunction; - const compiledFn = createFnPrototype('bench', 'timer', 'count', 'kUnmanagedTimerResult', compiledFnString); - const selectedTimer = hasArg ? new ManagedTimer(recommendedCount) : timer; - const runner = compiledFn.bind(globalThis, bench, selectedTimer, recommendedCount, kUnmanagedTimerResult); - debugBench(`Compiled Code: ${ compiledFnString }`); - debugBench(`Created compiled benchmark, hasArg=${ hasArg }, isAsync=${ isAsync }, recommendedCount=${ recommendedCount }`); - - return runner; + const { isAsync, hasArg } = bench; + const compiledFnString = bench.fnStr; + + const createFnPrototype = isAsync ? AsyncFunction : SyncFunction; + const compiledFn = createFnPrototype( + "bench", + "timer", + "count", + "kUnmanagedTimerResult", + compiledFnString, + ); + const selectedTimer = hasArg ? new ManagedTimer(recommendedCount) : timer; + const runner = compiledFn.bind( + globalThis, + bench, + selectedTimer, + recommendedCount, + kUnmanagedTimerResult, + ); + debugBench(`Compiled Code: ${compiledFnString}`); + debugBench( + `Created compiled benchmark, hasArg=${hasArg}, isAsync=${isAsync}, recommendedCount=${recommendedCount}`, + ); + + return runner; } async function clockBenchmark(bench, recommendedCount) { - const runner = createRunner(bench, recommendedCount); - const result = await runner(); - // Just to avoid issues with empty fn - result[0] = Math.max(MIN_RESOLUTION, result[0]); - for (const p of bench.plugins) { - if (typeof p.onCompleteBenchmark === 'function') { - // TODO: this won't work when useWorkers=true - p.onCompleteBenchmark(result, bench); - } - } - - debugBench(`Took ${ timer.format(result[0]) } to execute ${ result[1] } iterations`); - return result; + const runner = createRunner(bench, recommendedCount); + const result = await runner(); + // Just to avoid issues with empty fn + result[0] = Math.max(MIN_RESOLUTION, result[0]); + for (const p of bench.plugins) { + if (typeof p.onCompleteBenchmark === "function") { + // TODO: this won't work when useWorkers=true + p.onCompleteBenchmark(result, bench); + } + } + + debugBench( + `Took ${timer.format(result[0])} to execute ${result[1]} iterations`, + ); + return result; } module.exports = { - clockBenchmark, - createFnString, - timer, - MIN_RESOLUTION, - debugBench, + clockBenchmark, + createFnString, + timer, + MIN_RESOLUTION, + debugBench, }; diff --git a/lib/histogram.js b/lib/histogram.js index fe467f8..011453b 100644 --- a/lib/histogram.js +++ b/lib/histogram.js @@ -1,142 +1,150 @@ -const { validateNumber } = require('./validators'); +const { validateNumber } = require("./validators"); class StatisticalHistogram { - all = []; - min; - max; - mean; - cv; - stddev; - - /** - * @returns {number[]} - */ - get samples() { - return this.all.slice(); - } - - /** - * @param {number} percentile - * @returns {number} - */ - percentile(percentile) { - validateNumber(percentile, 'percentile'); - - if (Number.isNaN(percentile) || percentile < 0 || percentile > 100) - throw new Error('Invalid percentile value. Must be a number between 0 and 100.'); - - if (this.all.length === 0) - return 0; - - if (percentile === 0) - return this.all[0]; - - return this.all[Math.ceil(this.all.length * (percentile / 100)) - 1]; - } - - /** - * @param {number} value - */ - record(value) { - validateNumber(value, 'value', 0); - - this.all.push(value); - } - - finish() { - this.removeOutliers(); - - this.calculateMinMax(); - this.calculateMean(); - this.calculateStd(); - this.calculateCv(); - } - - /** - * References: - * - https://gist.github.com/rmeissn/f5b42fb3e1386a46f60304a57b6d215a - * - https://en.wikipedia.org/wiki/Interquartile_range - */ - removeOutliers() { - this.all.sort((a, b) => a - b); - - const size = this.all.length; - - if (size < 4) - return; - - let q1, q3; - - if ((size - 1) / 4 % 1 === 0 || size / 4 % 1 === 0) { - q1 = 1 / 2 * (this.all[Math.floor(size / 4) - 1] + this.all[Math.floor(size / 4)]); - q3 = 1 / 2 * (this.all[Math.ceil(size * 3 / 4) - 1] + this.all[Math.ceil(size * 3 / 4)]); - } else { - q1 = this.all[Math.floor(size / 4)]; - q3 = this.all[Math.floor(size * 3 / 4)]; - } - - const iqr = q3 - q1; - const minValue = q1 - iqr * 1.5; - const maxValue = q3 + iqr * 1.5; - - this.all = this.all.filter( - (value) => (value <= maxValue) && (value >= minValue), - ); - } - - calculateMinMax() { - this.min = Infinity; - this.max = -Infinity; - - for (let i = 0; i < this.all.length; i++) { - this.min = Math.min(this.all[i], this.min); - this.max = Math.max(this.all[i], this.max); - } - } - - calculateMean() { - if (this.all.length === 0) { - this.mean = 0; - return; - } - - if (this.all.length === 1) { - this.mean = this.all[0]; - return; - } - - this.mean = this.all.reduce( - (acc, value) => Math.min(Number.MAX_SAFE_INTEGER, acc + value), - 0, - ) / this.all.length; - } - - calculateStd() { - if (this.all.length < 2) { - this.stddev = 0; - return; - } - const variance = this.all.reduce((acc, value) => { - return acc + Math.pow(value - this.mean, 2); - }, 0) / (this.all.length - 1); - this.stddev = Math.sqrt(variance); - } - - /** - * References: - * - https://en.wikipedia.org/wiki/Coefficient_of_variation - * - https://github.com/google/benchmark/blob/159eb2d0ffb85b86e00ec1f983d72e72009ec387/src/statistics.ccL81-L88 - */ - calculateCv() { - if (this.all.length < 2 || this.mean === 0) { - this.cv = 0; - return; - } - - this.cv = this.stddev / this.mean * 100; - } + all = []; + min; + max; + mean; + cv; + stddev; + + /** + * @returns {number[]} + */ + get samples() { + return this.all.slice(); + } + + /** + * @param {number} percentile + * @returns {number} + */ + percentile(percentile) { + validateNumber(percentile, "percentile"); + + if (Number.isNaN(percentile) || percentile < 0 || percentile > 100) + throw new Error( + "Invalid percentile value. Must be a number between 0 and 100.", + ); + + if (this.all.length === 0) return 0; + + if (percentile === 0) return this.all[0]; + + return this.all[Math.ceil(this.all.length * (percentile / 100)) - 1]; + } + + /** + * @param {number} value + */ + record(value) { + validateNumber(value, "value", 0); + + this.all.push(value); + } + + finish() { + this.removeOutliers(); + + this.calculateMinMax(); + this.calculateMean(); + this.calculateStd(); + this.calculateCv(); + } + + /** + * References: + * - https://gist.github.com/rmeissn/f5b42fb3e1386a46f60304a57b6d215a + * - https://en.wikipedia.org/wiki/Interquartile_range + */ + removeOutliers() { + this.all.sort((a, b) => a - b); + + const size = this.all.length; + + if (size < 4) return; + + let q1; + let q3; + + if (((size - 1) / 4) % 1 === 0 || (size / 4) % 1 === 0) { + q1 = + (1 / 2) * + (this.all[Math.floor(size / 4) - 1] + this.all[Math.floor(size / 4)]); + q3 = + (1 / 2) * + (this.all[Math.ceil((size * 3) / 4) - 1] + + this.all[Math.ceil((size * 3) / 4)]); + } else { + q1 = this.all[Math.floor(size / 4)]; + q3 = this.all[Math.floor((size * 3) / 4)]; + } + + const iqr = q3 - q1; + const minValue = q1 - iqr * 1.5; + const maxValue = q3 + iqr * 1.5; + + this.all = this.all.filter( + (value) => value <= maxValue && value >= minValue, + ); + } + + calculateMinMax() { + this.min = Number.POSITIVE_INFINITY; + this.max = Number.NEGATIVE_INFINITY; + + for (let i = 0; i < this.all.length; i++) { + this.min = Math.min(this.all[i], this.min); + this.max = Math.max(this.all[i], this.max); + } + } + + calculateMean() { + if (this.all.length === 0) { + this.mean = 0; + return; + } + + if (this.all.length === 1) { + this.mean = this.all[0]; + return; + } + + this.mean = + this.all.reduce( + (acc, value) => Math.min(Number.MAX_SAFE_INTEGER, acc + value), + 0, + ) / this.all.length; + } + + calculateStd() { + if (this.all.length < 2) { + this.stddev = 0; + return; + } + const variance = + this.all.reduce((acc, value) => { + return acc + (value - this.mean) ** 2; + }, 0) / + (this.all.length - 1); + this.stddev = Math.sqrt(variance); + } + + /** + * References: + * - https://en.wikipedia.org/wiki/Coefficient_of_variation + * - https://github.com/google/benchmark/blob/159eb2d0ffb85b86e00ec1f983d72e72009ec387/src/statistics.ccL81-L88 + */ + calculateCv() { + if (this.all.length < 2 || this.mean === 0) { + this.cv = 0; + return; + } + + this.cv = (this.stddev / this.mean) * 100; + } } module.exports = { - StatisticalHistogram, + StatisticalHistogram, }; diff --git a/lib/index.js b/lib/index.js index 4ae5843..d7e7633 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,212 +1,232 @@ -const { Worker } = require('node:worker_threads'); -const { types } = require('node:util'); -const path = require('node:path'); +const { Worker } = require("node:worker_threads"); +const { types } = require("node:util"); +const path = require("node:path"); +const { textReport, chartReport, htmlReport, jsonReport } = require("./report"); const { - textReport, - chartReport, - htmlReport, - jsonReport, -} = require('./report'); -const { getInitialIterations, runBenchmark, runWarmup } = require('./lifecycle'); -const { debugBench, timer, createFnString } = require('./clock'); + getInitialIterations, + runBenchmark, + runWarmup, +} = require("./lifecycle"); +const { debugBench, timer, createFnString } = require("./clock"); const { - validatePlugins, - V8NeverOptimizePlugin, - V8GetOptimizationStatus, - V8OptimizeOnNextCallPlugin, -} = require('./plugins'); + validatePlugins, + V8NeverOptimizePlugin, + V8GetOptimizationStatus, + V8OptimizeOnNextCallPlugin, +} = require("./plugins"); const { - validateFunction, - validateNumber, - validateObject, - validateString, - validateArray, -} = require('./validators'); - -const getFunctionBody = (string) => string.substring( - string.indexOf("{") + 1, - string.lastIndexOf("}") -); + validateFunction, + validateNumber, + validateObject, + validateString, + validateArray, +} = require("./validators"); + +const getFunctionBody = (string) => + string.substring(string.indexOf("{") + 1, string.lastIndexOf("}")); class Benchmark { - name = 'Benchmark'; - fn; - minTime; - maxTime; - plugins; - repeatSuite; - - constructor(name, fn, minTime, maxTime, plugins, repeatSuite) { - this.name = name; - this.fn = fn; - this.minTime = minTime; - this.maxTime = maxTime; - this.plugins = plugins; - this.repeatSuite = repeatSuite; - - this.hasArg = this.fn.length >= 1; - if (this.fn.length > 1) { - process.emitWarning(`The benchmark "${ this.name }" function should not have more than 1 argument.`); - } - - this.isAsync = types.isAsyncFunction(this.fn); - - this.fnStr = createFnString(this); - } - - serializeBenchmark() { - // Regular functions can't be passed to worker.postMessage - // So we pass the string and deserialize fnStr into a new Function - // on worker - this.fn = getFunctionBody(this.fn.toString()); - } + name = "Benchmark"; + fn; + minTime; + maxTime; + plugins; + repeatSuite; + + constructor(name, fn, minTime, maxTime, plugins, repeatSuite) { + this.name = name; + this.fn = fn; + this.minTime = minTime; + this.maxTime = maxTime; + this.plugins = plugins; + this.repeatSuite = repeatSuite; + + this.hasArg = this.fn.length >= 1; + if (this.fn.length > 1) { + process.emitWarning( + `The benchmark "${this.name}" function should not have more than 1 argument.`, + ); + } + + this.isAsync = types.isAsyncFunction(this.fn); + + this.fnStr = createFnString(this); + } + + serializeBenchmark() { + // Regular functions can't be passed to worker.postMessage + // So we pass the string and deserialize fnStr into a new Function + // on worker + this.fn = getFunctionBody(this.fn.toString()); + } } const defaultBenchOptions = { - // 0.05s - Arbitrary number used in some benchmark tools - minTime: 0.05, - // 0.5s - Arbitrary number used in some benchmark tools - maxTime: 0.5, - // Number of times the benchmark will be repeated - repeatSuite: 1, + // 0.05s - Arbitrary number used in some benchmark tools + minTime: 0.05, + // 0.5s - Arbitrary number used in some benchmark tools + maxTime: 0.5, + // Number of times the benchmark will be repeated + repeatSuite: 1, }; function throwIfNoNativesSyntax() { - if (process.execArgv.includes('--allow-natives-syntax') === false) { - throw new Error('bench-node module must be run with --allow-natives-syntax argument'); - } + if (process.execArgv.includes("--allow-natives-syntax") === false) { + throw new Error( + "bench-node module must be run with --allow-natives-syntax argument", + ); + } } class Suite { - #benchmarks; - #reporter; - #plugins; - #useWorkers; - - constructor(options = {}) { - this.#benchmarks = []; - validateObject(options, 'options'); - if (options?.reporter !== undefined) { - if (options?.reporter !== false && options?.reporter !== null) { - validateFunction(options.reporter, 'reporter'); - } - this.#reporter = options.reporter; - } else { - this.#reporter = textReport; - } - - this.#useWorkers = options.useWorkers || false; - if (options?.plugins) { - validateArray(options.plugins, 'plugin'); - validatePlugins(options.plugins); - } - this.#plugins = options?.plugins || [new V8NeverOptimizePlugin()]; - } - - add(name, options, fn) { - validateString(name, 'name'); - if (typeof options === 'function') { - fn = options; - options = defaultBenchOptions; - } else { - validateObject(options, 'options'); - options = { - ...defaultBenchOptions, - ...options - }; - validateNumber(options.minTime, 'options.minTime', timer.resolution * 1e3); - validateNumber(options.maxTime, 'options.maxTime', options.minTime); - validateNumber(options.repeatSuite, 'options.repeatSuite', options.repeatSuite); - } - validateFunction(fn, 'fn'); - - const benchmark = new Benchmark( - name, - fn, - options.minTime, - options.maxTime, - this.#plugins, - options.repeatSuite, - ); - this.#benchmarks.push(benchmark); - return this; - } - - async run() { - throwIfNoNativesSyntax(); - const results = new Array(this.#benchmarks.length); - - // It doesn't make sense to warmup a fresh new instance of Worker. - // TODO: support warmup directly in the Worker. - if (!this.#useWorkers) { - // This is required to avoid variance on first benchmark run - for (let i = 0; i < this.#benchmarks.length; ++i) { - const benchmark = this.#benchmarks[i]; - debugBench(`Warmup ${ benchmark.name } with minTime=${ benchmark.minTime }, maxTime=${ benchmark.maxTime }`); - const initialIteration = await getInitialIterations(benchmark); - await runWarmup(benchmark, initialIteration, { minTime: 0.005, maxTime: 0.05 }); - } - } - - for (let i = 0; i < this.#benchmarks.length; ++i) { - const benchmark = this.#benchmarks[i]; - // Warmup is calculated to reduce noise/bias on the results - const initialIterations = await getInitialIterations(benchmark); - debugBench(`Starting ${ benchmark.name } with minTime=${ benchmark.minTime }, maxTime=${ benchmark.maxTime }, repeatSuite=${ benchmark.repeatSuite }`); - - let result; - if (this.#useWorkers) { - result = await this.runWorkerBenchmark(benchmark, initialIterations); - } else { - result = await runBenchmark(benchmark, initialIterations, benchmark.repeatSuite); - } - results[i] = result; - } - - if (this.#reporter) { - this.#reporter(results); - } - return results; - } - - runWorkerBenchmark(benchmark, initialIterations) { - benchmark.serializeBenchmark(); - const worker = new Worker(path.join(__dirname, './worker-runner.js')); - - worker.postMessage({ - benchmark, - initialIterations, - repeatSuite: benchmark.repeatSuite, - }); - return new Promise((resolve, reject) => { - worker.on('message', (result) => { - resolve(result); - // TODO: await? - worker.terminate(); - }); - - worker.on('error', (err) => { - reject(err); - worker.terminate(); - }); - - worker.on('exit', (code) => { - if (code !== 0) { - reject(new Error(`Worker stopped with exit code ${code}`)); - } - }); - }); - } + #benchmarks; + #reporter; + #plugins; + #useWorkers; + + constructor(options = {}) { + this.#benchmarks = []; + validateObject(options, "options"); + if (options?.reporter !== undefined) { + if (options?.reporter !== false && options?.reporter !== null) { + validateFunction(options.reporter, "reporter"); + } + this.#reporter = options.reporter; + } else { + this.#reporter = textReport; + } + + this.#useWorkers = options.useWorkers || false; + if (options?.plugins) { + validateArray(options.plugins, "plugin"); + validatePlugins(options.plugins); + } + this.#plugins = options?.plugins || [new V8NeverOptimizePlugin()]; + } + + add(name, options, fn) { + validateString(name, "name"); + if (typeof options === "function") { + fn = options; + options = defaultBenchOptions; + } else { + validateObject(options, "options"); + options = { + ...defaultBenchOptions, + ...options, + }; + validateNumber( + options.minTime, + "options.minTime", + timer.resolution * 1e3, + ); + validateNumber(options.maxTime, "options.maxTime", options.minTime); + validateNumber( + options.repeatSuite, + "options.repeatSuite", + options.repeatSuite, + ); + } + validateFunction(fn, "fn"); + + const benchmark = new Benchmark( + name, + fn, + options.minTime, + options.maxTime, + this.#plugins, + options.repeatSuite, + ); + this.#benchmarks.push(benchmark); + return this; + } + + async run() { + throwIfNoNativesSyntax(); + const results = new Array(this.#benchmarks.length); + + // It doesn't make sense to warmup a fresh new instance of Worker. + // TODO: support warmup directly in the Worker. + if (!this.#useWorkers) { + // This is required to avoid variance on first benchmark run + for (let i = 0; i < this.#benchmarks.length; ++i) { + const benchmark = this.#benchmarks[i]; + debugBench( + `Warmup ${benchmark.name} with minTime=${benchmark.minTime}, maxTime=${benchmark.maxTime}`, + ); + const initialIteration = await getInitialIterations(benchmark); + await runWarmup(benchmark, initialIteration, { + minTime: 0.005, + maxTime: 0.05, + }); + } + } + + for (let i = 0; i < this.#benchmarks.length; ++i) { + const benchmark = this.#benchmarks[i]; + // Warmup is calculated to reduce noise/bias on the results + const initialIterations = await getInitialIterations(benchmark); + debugBench( + `Starting ${benchmark.name} with minTime=${benchmark.minTime}, maxTime=${benchmark.maxTime}, repeatSuite=${benchmark.repeatSuite}`, + ); + + let result; + if (this.#useWorkers) { + result = await this.runWorkerBenchmark(benchmark, initialIterations); + } else { + result = await runBenchmark( + benchmark, + initialIterations, + benchmark.repeatSuite, + ); + } + results[i] = result; + } + + if (this.#reporter) { + this.#reporter(results); + } + return results; + } + + runWorkerBenchmark(benchmark, initialIterations) { + benchmark.serializeBenchmark(); + const worker = new Worker(path.join(__dirname, "./worker-runner.js")); + + worker.postMessage({ + benchmark, + initialIterations, + repeatSuite: benchmark.repeatSuite, + }); + return new Promise((resolve, reject) => { + worker.on("message", (result) => { + resolve(result); + // TODO: await? + worker.terminate(); + }); + + worker.on("error", (err) => { + reject(err); + worker.terminate(); + }); + + worker.on("exit", (code) => { + if (code !== 0) { + reject(new Error(`Worker stopped with exit code ${code}`)); + } + }); + }); + } } module.exports = { - Suite, - V8NeverOptimizePlugin, - V8GetOptimizationStatus, - V8OptimizeOnNextCallPlugin, - chartReport, - textReport, - htmlReport, - jsonReport, + Suite, + V8NeverOptimizePlugin, + V8GetOptimizationStatus, + V8OptimizeOnNextCallPlugin, + chartReport, + textReport, + htmlReport, + jsonReport, }; diff --git a/lib/lifecycle.js b/lib/lifecycle.js index bab5b42..3fd4838 100644 --- a/lib/lifecycle.js +++ b/lib/lifecycle.js @@ -1,129 +1,157 @@ -const { clockBenchmark, debugBench, MIN_RESOLUTION, timer } = require('./clock'); -const { StatisticalHistogram } = require('./histogram'); +const { + clockBenchmark, + debugBench, + MIN_RESOLUTION, + timer, +} = require("./clock"); +const { StatisticalHistogram } = require("./histogram"); /** * @param {number} durationPerOp The amount of time each operation takes * @param {number} targetTime The amount of time we want the benchmark to execute */ function getItersForOpDuration(durationPerOp, targetTime) { - const totalOpsForMinTime = targetTime / (durationPerOp / timer.scale); + const totalOpsForMinTime = targetTime / (durationPerOp / timer.scale); - return Math.min(Number.MAX_SAFE_INTEGER, Math.max(1, Math.round(totalOpsForMinTime))); + return Math.min( + Number.MAX_SAFE_INTEGER, + Math.max(1, Math.round(totalOpsForMinTime)), + ); } function parsePluginsResult(plugins, name) { - const result = [] - for (const p of plugins) { - result.push({ - name: p.toString(), - result: p.getResult?.(name) ?? 'enabled', - report: p.getReport?.(name) ?? '', - }); - } - return result; + const result = []; + for (const p of plugins) { + result.push({ + name: p.toString(), + result: p.getResult?.(name) ?? "enabled", + report: p.getReport?.(name) ?? "", + }); + } + return result; } async function getInitialIterations(bench) { - const { 0: duration, 1: realIterations } = await clockBenchmark(bench, 30); + const { 0: duration, 1: realIterations } = await clockBenchmark(bench, 30); - // Just to avoid issues with empty fn - const durationPerOp = Math.max(MIN_RESOLUTION, duration / realIterations); - debugBench(`Duration per operation on initial count: ${ timer.format(durationPerOp) }`); + // Just to avoid issues with empty fn + const durationPerOp = Math.max(MIN_RESOLUTION, duration / realIterations); + debugBench( + `Duration per operation on initial count: ${timer.format(durationPerOp)}`, + ); - // TODO: is this a correct assumpion? - if ((durationPerOp / timer.scale) >= bench.maxTime) - process.emitWarning(`The benchmark "${ bench.name }" has a duration per operation greater than the maxTime.`); + // TODO: is this a correct assumpion? + if (durationPerOp / timer.scale >= bench.maxTime) + process.emitWarning( + `The benchmark "${bench.name}" has a duration per operation greater than the maxTime.`, + ); - return getItersForOpDuration(durationPerOp, bench.minTime); + return getItersForOpDuration(durationPerOp, bench.minTime); } async function runWarmup(bench, initialIterations, { minTime, maxTime }) { - minTime = minTime ?? bench.minTime; - maxTime = maxTime ?? bench.minTime; - - const maxDuration = maxTime * timer.scale; - const minSamples = 10; - - let iterations = 0; - let timeSpent = 0; - let samples = 0; - - while (timeSpent < maxDuration || samples <= minSamples) { - const { 0: duration, 1: realIterations } = await clockBenchmark(bench, initialIterations); - timeSpent += duration - - iterations += realIterations; - iterations = Math.min(Number.MAX_SAFE_INTEGER, iterations); - - // Just to avoid issues with empty fn - const durationPerOp = Math.max(MIN_RESOLUTION, duration / realIterations); - - const minWindowTime = Math.max(0, Math.min((maxDuration - timeSpent) / timer.scale, minTime)); - initialIterations = getItersForOpDuration(durationPerOp, minWindowTime); - samples++; - } + minTime = minTime ?? bench.minTime; + maxTime = maxTime ?? bench.minTime; + + const maxDuration = maxTime * timer.scale; + const minSamples = 10; + + let iterations = 0; + let timeSpent = 0; + let samples = 0; + + while (timeSpent < maxDuration || samples <= minSamples) { + const { 0: duration, 1: realIterations } = await clockBenchmark( + bench, + initialIterations, + ); + timeSpent += duration; + + iterations += realIterations; + iterations = Math.min(Number.MAX_SAFE_INTEGER, iterations); + + // Just to avoid issues with empty fn + const durationPerOp = Math.max(MIN_RESOLUTION, duration / realIterations); + + const minWindowTime = Math.max( + 0, + Math.min((maxDuration - timeSpent) / timer.scale, minTime), + ); + initialIterations = getItersForOpDuration(durationPerOp, minWindowTime); + samples++; + } } -async function runBenchmarkOnce(bench, histogram, { initialIterations, maxDuration, minSamples }) { - let iterations = 0; - let timeSpent = 0; - while (timeSpent < maxDuration || histogram.samples.length <= minSamples) { - const { 0: duration, 1: realIterations } = await clockBenchmark(bench, initialIterations); - timeSpent += duration - - iterations += realIterations; - iterations = Math.min(Number.MAX_SAFE_INTEGER, iterations); - - // Just to avoid issues with empty fn - const durationPerOp = Math.max(MIN_RESOLUTION, duration / realIterations); - - histogram.record(durationPerOp); - - const minWindowTime = Math.max(0, Math.min((maxDuration - timeSpent) / timer.scale, bench.minTime)); - initialIterations = getItersForOpDuration(durationPerOp, minWindowTime); - } - - return { iterations, timeSpent }; +async function runBenchmarkOnce( + bench, + histogram, + { initialIterations, maxDuration, minSamples }, +) { + let iterations = 0; + let timeSpent = 0; + while (timeSpent < maxDuration || histogram.samples.length <= minSamples) { + const { 0: duration, 1: realIterations } = await clockBenchmark( + bench, + initialIterations, + ); + timeSpent += duration; + + iterations += realIterations; + iterations = Math.min(Number.MAX_SAFE_INTEGER, iterations); + + // Just to avoid issues with empty fn + const durationPerOp = Math.max(MIN_RESOLUTION, duration / realIterations); + + histogram.record(durationPerOp); + + const minWindowTime = Math.max( + 0, + Math.min((maxDuration - timeSpent) / timer.scale, bench.minTime), + ); + initialIterations = getItersForOpDuration(durationPerOp, minWindowTime); + } + + return { iterations, timeSpent }; } async function runBenchmark(bench, initialIterations, repeatSuite) { - const histogram = new StatisticalHistogram(); - - const maxDuration = bench.maxTime * timer.scale; - const minSamples = 10; - - let totalIterations = 0; - let totalTimeSpent = 0; - - for (let i = 0; i < repeatSuite; ++i) { - const { iterations, timeSpent } = await runBenchmarkOnce( - bench, - histogram, - { initialIterations, maxDuration, minSamples } - ); - totalTimeSpent += timeSpent; - totalIterations += iterations; - } - histogram.finish() - - const opsSec = totalIterations / (totalTimeSpent / timer.scale); - - return { - opsSec, - iterations: totalIterations, - // StatisticalHistogram is not a serializable object - histogram: { - samples: histogram.samples.length, - min: histogram.min, - max: histogram.max, - }, - name: bench.name, - plugins: parsePluginsResult(bench.plugins, bench.name), - }; + const histogram = new StatisticalHistogram(); + + const maxDuration = bench.maxTime * timer.scale; + const minSamples = 10; + + let totalIterations = 0; + let totalTimeSpent = 0; + + for (let i = 0; i < repeatSuite; ++i) { + const { iterations, timeSpent } = await runBenchmarkOnce(bench, histogram, { + initialIterations, + maxDuration, + minSamples, + }); + totalTimeSpent += timeSpent; + totalIterations += iterations; + } + histogram.finish(); + + const opsSec = totalIterations / (totalTimeSpent / timer.scale); + + return { + opsSec, + iterations: totalIterations, + // StatisticalHistogram is not a serializable object + histogram: { + samples: histogram.samples.length, + min: histogram.min, + max: histogram.max, + }, + name: bench.name, + plugins: parsePluginsResult(bench.plugins, bench.name), + }; } module.exports = { - getInitialIterations, - runBenchmark, - runWarmup, + getInitialIterations, + runBenchmark, + runWarmup, }; diff --git a/lib/plugins.js b/lib/plugins.js index b18a631..2e3735f 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -1,57 +1,46 @@ -const { - V8OptimizeOnNextCallPlugin -} = require('./plugins/v8-opt'); -const { - V8NeverOptimizePlugin -} = require('./plugins/v8-never-opt'); +const { V8OptimizeOnNextCallPlugin } = require("./plugins/v8-opt"); +const { V8NeverOptimizePlugin } = require("./plugins/v8-never-opt"); -const { - V8GetOptimizationStatus, -} = require('./plugins/v8-print-status'); -const { - MemoryPlugin, -} = require('./plugins/memory'); +const { V8GetOptimizationStatus } = require("./plugins/v8-print-status"); +const { MemoryPlugin } = require("./plugins/memory"); -const { - validateFunction, - validateArray, -} = require('./validators'); +const { validateFunction, validateArray } = require("./validators"); function validatePlugins(plugins) { - for (p of plugins) { - validateFunction(p.isSupported, 'Plugins must have a isSupported method.'); - validateFunction(p.toString, 'Plugins must have a toString() method.'); + for (p of plugins) { + validateFunction(p.isSupported, "Plugins must have a isSupported method."); + validateFunction(p.toString, "Plugins must have a toString() method."); - if (!p.isSupported()) { - throw new Error(`Plugin: ${p.toString()} is not supported.`); - } + if (!p.isSupported()) { + throw new Error(`Plugin: ${p.toString()} is not supported.`); + } - if (typeof p.beforeClockTemplate === 'function') { - const result = p.beforeClockTemplate({ - bench: '', - awaitOrEmpty: '', - context: '', - timer: '', - }); - validateArray(result, `${p.toString()}.beforeClockTemplate()`); - } + if (typeof p.beforeClockTemplate === "function") { + const result = p.beforeClockTemplate({ + bench: "", + awaitOrEmpty: "", + context: "", + timer: "", + }); + validateArray(result, `${p.toString()}.beforeClockTemplate()`); + } - if (typeof p.afterClockTemplate === 'function') { - const result = p.afterClockTemplate({ - bench: '', - awaitOrEmpty: '', - context: '', - timer: '', - }); - validateArray(result, `${p.toString()}.afterClockTemplate()`); - } - } + if (typeof p.afterClockTemplate === "function") { + const result = p.afterClockTemplate({ + bench: "", + awaitOrEmpty: "", + context: "", + timer: "", + }); + validateArray(result, `${p.toString()}.afterClockTemplate()`); + } + } } module.exports = { - MemoryPlugin, - V8NeverOptimizePlugin, - V8GetOptimizationStatus, - V8OptimizeOnNextCallPlugin, - validatePlugins, + MemoryPlugin, + V8NeverOptimizePlugin, + V8GetOptimizationStatus, + V8OptimizeOnNextCallPlugin, + validatePlugins, }; diff --git a/lib/plugins/memory.js b/lib/plugins/memory.js index 489ae87..cd2abc9 100644 --- a/lib/plugins/memory.js +++ b/lib/plugins/memory.js @@ -1,97 +1,102 @@ const { - kStatisticalHistogramRecord, - StatisticalHistogram, - kStatisticalHistogramFinish, -} = require('../histogram'); + kStatisticalHistogramRecord, + StatisticalHistogram, + kStatisticalHistogramFinish, +} = require("../histogram"); function formatBytes(bytes) { - if (bytes < 1024) - return `${ Math.round(bytes) }B`; + if (bytes < 1024) return `${Math.round(bytes)}B`; - const kbytes = bytes / 1024; - if (kbytes < 1024) - return `${ (kbytes, 2).toFixed() }Kb`; + const kbytes = bytes / 1024; + if (kbytes < 1024) return `${kbytes.toFixed(2)}Kb`; - const mbytes = kbytes / 1024; - if (mbytes < 1024) - return `${ (mbytes, 2).toFixed() }MB`; + const mbytes = kbytes / 1024; + if (mbytes < 1024) return `${mbytes.toFixed(2)}MB`; - const gbytes = mbytes / 1024; - return `${ (gbytes, 2).toFixed() }GB`; + const gbytes = mbytes / 1024; + return `${gbytes.toFixed(2)}GB`; } class MemoryPlugin { - static MEMORY_BEFORE_RUN = 'memoryBeforeRun'; - static MEMORY_AFTER_RUN = 'memoryAfterRun'; - static #WARNING_REPORTED = false; - - /** - * @type {StatisticalHistogram} - */ - #heapUsedHistogram; - - constructor() { - this.reset(); - } - - isSupported() { - return typeof globalThis.gc === 'function'; - } - - beforeClockTemplate({ managed, globalThisVar, contextVar }) { - if (managed && !MemoryPlugin.#WARNING_REPORTED) { - MemoryPlugin.#WARNING_REPORTED = true; - process.emitWarning('The memory statistics can be inaccurate since it will include the tear-up and teardown of your benchmark.') - } - - let code = ''; - - code += `${ contextVar }.${ MemoryPlugin.MEMORY_BEFORE_RUN } = 0;\n`; - code += `${ contextVar }.${ MemoryPlugin.MEMORY_AFTER_RUN } = 0;\n`; - code += `${ globalThisVar }.gc();\n`; - code += `${ contextVar }.${ MemoryPlugin.MEMORY_BEFORE_RUN } = ${ globalThisVar }.process.memoryUsage();\n`; - - return code; - } - - afterClockTemplate({ globalThisVar, contextVar }) { - return `${ contextVar }.${ MemoryPlugin.MEMORY_AFTER_RUN } = ${ globalThisVar }.process.memoryUsage();\n`; - } - - onCompleteClock(result) { - const realIterations = result[1]; - const context = result[2]; - - const heapUsed = context[MemoryPlugin.MEMORY_AFTER_RUN].heapUsed - context[MemoryPlugin.MEMORY_BEFORE_RUN].heapUsed; - const externalUsed = context[MemoryPlugin.MEMORY_AFTER_RUN].external - context[MemoryPlugin.MEMORY_BEFORE_RUN].external; - - const memoryAllocated = (heapUsed + externalUsed) / realIterations; - - // below 0, we just coerce to be zero - this.#heapUsedHistogram[kStatisticalHistogramRecord](Math.max(0, memoryAllocated)); - } - - onCompleteBenchmark() { - this.#heapUsedHistogram[kStatisticalHistogramFinish](); - } - - toString() { - return 'MemoryPlugin'; - } - - getReport() { - return `heap usage=${ formatBytes(this.#heapUsedHistogram.mean) } (${ formatBytes(this.#heapUsedHistogram.min) } ... ${ formatBytes(this.#heapUsedHistogram.max) })`; - } - - getResult() { - return { - proto: null, - type: this.toString(), - histogram: this.#heapUsedHistogram, - }; - } + static MEMORY_BEFORE_RUN = "memoryBeforeRun"; + static MEMORY_AFTER_RUN = "memoryAfterRun"; + static #WARNING_REPORTED = false; + + /** + * @type {StatisticalHistogram} + */ + #heapUsedHistogram; + + constructor() { + this.reset(); + } + + isSupported() { + return typeof globalThis.gc === "function"; + } + + beforeClockTemplate({ managed, globalThisVar, contextVar }) { + if (managed && !MemoryPlugin.#WARNING_REPORTED) { + MemoryPlugin.#WARNING_REPORTED = true; + process.emitWarning( + "The memory statistics can be inaccurate since it will include the tear-up and teardown of your benchmark.", + ); + } + + let code = ""; + + code += `${contextVar}.${MemoryPlugin.MEMORY_BEFORE_RUN} = 0;\n`; + code += `${contextVar}.${MemoryPlugin.MEMORY_AFTER_RUN} = 0;\n`; + code += `${globalThisVar}.gc();\n`; + code += `${contextVar}.${MemoryPlugin.MEMORY_BEFORE_RUN} = ${globalThisVar}.process.memoryUsage();\n`; + + return code; + } + + afterClockTemplate({ globalThisVar, contextVar }) { + return `${contextVar}.${MemoryPlugin.MEMORY_AFTER_RUN} = ${globalThisVar}.process.memoryUsage();\n`; + } + + onCompleteClock(result) { + const realIterations = result[1]; + const context = result[2]; + + const heapUsed = + context[MemoryPlugin.MEMORY_AFTER_RUN].heapUsed - + context[MemoryPlugin.MEMORY_BEFORE_RUN].heapUsed; + const externalUsed = + context[MemoryPlugin.MEMORY_AFTER_RUN].external - + context[MemoryPlugin.MEMORY_BEFORE_RUN].external; + + const memoryAllocated = (heapUsed + externalUsed) / realIterations; + + // below 0, we just coerce to be zero + this.#heapUsedHistogram[kStatisticalHistogramRecord]( + Math.max(0, memoryAllocated), + ); + } + + onCompleteBenchmark() { + this.#heapUsedHistogram[kStatisticalHistogramFinish](); + } + + toString() { + return "MemoryPlugin"; + } + + getReport() { + return `heap usage=${formatBytes(this.#heapUsedHistogram.mean)} (${formatBytes(this.#heapUsedHistogram.min)} ... ${formatBytes(this.#heapUsedHistogram.max)})`; + } + + getResult() { + return { + proto: null, + type: this.toString(), + histogram: this.#heapUsedHistogram, + }; + } } module.exports = { - MemoryPlugin, + MemoryPlugin, }; diff --git a/lib/plugins/v8-never-opt.js b/lib/plugins/v8-never-opt.js index bcc78eb..e775328 100644 --- a/lib/plugins/v8-never-opt.js +++ b/lib/plugins/v8-never-opt.js @@ -1,34 +1,34 @@ class V8NeverOptimizePlugin { - isSupported() { - try { - new Function(`%NeverOptimizeFunction(() => {})`)(); + isSupported() { + try { + new Function("%NeverOptimizeFunction(() => {})")(); - return true; - } catch (e) { - return false; - } - } + return true; + } catch (e) { + return false; + } + } - beforeClockTemplate(_varNames) { - let code = ''; + beforeClockTemplate(_varNames) { + let code = ""; - code += ` + code += ` function DoNotOptimize(x) {} // Prevent DoNotOptimize from optimizing or being inlined. %NeverOptimizeFunction(DoNotOptimize); -` - return [code, 'DoNotOptimize']; - } +`; + return [code, "DoNotOptimize"]; + } - toString() { - return 'V8NeverOptimizePlugin'; - } + toString() { + return "V8NeverOptimizePlugin"; + } - getReport() { - return 'v8-never-optimize=true' - } + getReport() { + return "v8-never-optimize=true"; + } } module.exports = { - V8NeverOptimizePlugin, + V8NeverOptimizePlugin, }; diff --git a/lib/plugins/v8-opt.js b/lib/plugins/v8-opt.js index 6315373..5f150af 100644 --- a/lib/plugins/v8-opt.js +++ b/lib/plugins/v8-opt.js @@ -1,33 +1,33 @@ class V8OptimizeOnNextCallPlugin { - isSupported() { - try { - new Function(`%OptimizeFunctionOnNextCall(() => {})`)(); + isSupported() { + try { + new Function("%OptimizeFunctionOnNextCall(() => {})")(); - return true; - } catch (e) { - return false; - } - } + return true; + } catch (e) { + return false; + } + } - beforeClockTemplate({ awaitOrEmpty, bench, timer }) { - let code = ''; + beforeClockTemplate({ awaitOrEmpty, bench, timer }) { + let code = ""; - code += `%OptimizeFunctionOnNextCall(${ bench }.fn);\n`; - code += `${ awaitOrEmpty }${ bench }.fn(${timer});\n`; - code += `${ awaitOrEmpty }${ bench }.fn(${timer});\n`; + code += `%OptimizeFunctionOnNextCall(${bench}.fn);\n`; + code += `${awaitOrEmpty}${bench}.fn(${timer});\n`; + code += `${awaitOrEmpty}${bench}.fn(${timer});\n`; - return [code]; - } + return [code]; + } - getReport() { - return 'v8-optimize-next-call=enabled'; - } + getReport() { + return "v8-optimize-next-call=enabled"; + } - toString() { - return 'V8OptimizeOnNextCallPlugin'; - } + toString() { + return "V8OptimizeOnNextCallPlugin"; + } } module.exports = { - V8OptimizeOnNextCallPlugin, + V8OptimizeOnNextCallPlugin, }; diff --git a/lib/plugins/v8-print-status.js b/lib/plugins/v8-print-status.js index 8c6f4f8..f16236a 100644 --- a/lib/plugins/v8-print-status.js +++ b/lib/plugins/v8-print-status.js @@ -1,98 +1,104 @@ function checkBitmap(value, bit) { - return ((value & bit) === bit); + return (value & bit) === bit; } function translateStatus(optStatus) { - if (optStatus === -1) { - return 'unknown'; - } + if (optStatus === -1) { + return "unknown"; + } - const optStat = []; - if (checkBitmap(optStatus, 2)) { - optStat.push("Never Optimized"); - } - if (checkBitmap(optStatus, 4)) { - optStat.push("Always Optimized"); - } - if (checkBitmap(optStatus, 8)) { - optStat.push("Maybe Deopted"); - } - if (checkBitmap(optStatus, 16)) { - optStat.push("Optimized"); - } - if (checkBitmap(optStatus, 32)) { - optStat.push("TurboFanned"); - } - if (checkBitmap(optStatus, 64)) { - optStat.push("Interpreted"); - } - if (checkBitmap(optStatus, 128)) { - optStat.push("Marked for Optimization"); - } - if (checkBitmap(optStatus, 256)) { - optStat.push("Marked for Concurrent Optimization"); - } - if (checkBitmap(optStatus, 512)) { - optStat.push("Concurrently Optimizing"); - } - if (checkBitmap(optStatus, 1024)) { - optStat.push("Is Executing"); - } - if (checkBitmap(optStatus, 2048)) { - optStat.push("Topmost frame is Turbo Fanned"); - } - if (checkBitmap(optStatus, 4096)) { - optStat.push("Lite Mode") - } - if (checkBitmap(optStatus, 8192)) { - optStat.push("Marked for de-optimization") - } + const optStat = []; + if (checkBitmap(optStatus, 2)) { + optStat.push("Never Optimized"); + } + if (checkBitmap(optStatus, 4)) { + optStat.push("Always Optimized"); + } + if (checkBitmap(optStatus, 8)) { + optStat.push("Maybe Deopted"); + } + if (checkBitmap(optStatus, 16)) { + optStat.push("Optimized"); + } + if (checkBitmap(optStatus, 32)) { + optStat.push("TurboFanned"); + } + if (checkBitmap(optStatus, 64)) { + optStat.push("Interpreted"); + } + if (checkBitmap(optStatus, 128)) { + optStat.push("Marked for Optimization"); + } + if (checkBitmap(optStatus, 256)) { + optStat.push("Marked for Concurrent Optimization"); + } + if (checkBitmap(optStatus, 512)) { + optStat.push("Concurrently Optimizing"); + } + if (checkBitmap(optStatus, 1024)) { + optStat.push("Is Executing"); + } + if (checkBitmap(optStatus, 2048)) { + optStat.push("Topmost frame is Turbo Fanned"); + } + if (checkBitmap(optStatus, 4096)) { + optStat.push("Lite Mode"); + } + if (checkBitmap(optStatus, 8192)) { + optStat.push("Marked for de-optimization"); + } - return optStat.join(', '); + return optStat.join(", "); } class V8GetOptimizationStatus { - #optimizationStatuses = []; + #optimizationStatuses = []; - isSupported() { - try { - new Function(`%GetOptimizationStatus(() => {})`)(); + isSupported() { + try { + new Function("%GetOptimizationStatus(() => {})")(); - return true; - } catch (e) { - return false; - } - } + return true; + } catch (e) { + return false; + } + } - afterClockTemplate({ bench, context }) { - let code = ''; - code += `${context}.v8OptimizationStatus = %GetOptimizationStatus(${bench}.fn);\n`; - return [code]; - } + afterClockTemplate({ bench, context }) { + let code = ""; + code += `${context}.v8OptimizationStatus = %GetOptimizationStatus(${bench}.fn);\n`; + return [code]; + } - onCompleteBenchmark(result) { - const context = result[2]; - this.#optimizationStatuses.push(context.v8OptimizationStatus); - } + onCompleteBenchmark(result) { + const context = result[2]; + this.#optimizationStatuses.push(context.v8OptimizationStatus); + } - toString() { - return 'V8GetOptimizationStatus'; - } + toString() { + return "V8GetOptimizationStatus"; + } - getReport() { - const allAvailableStatus = this.#optimizationStatuses.reduce((acc, v) => acc | v, 0); - return `v8-opt-status="${translateStatus(allAvailableStatus)}"`; - } + getReport() { + const allAvailableStatus = this.#optimizationStatuses.reduce( + (acc, v) => acc | v, + 0, + ); + return `v8-opt-status="${translateStatus(allAvailableStatus)}"`; + } - getResult() { - const allAvailableStatus = this.#optimizationStatuses.reduce((acc, v) => acc | v, 0); - return { - type: this.toString(), - optimizationStatuses: translateStatus(allAvailableStatus), - }; - } + getResult() { + const allAvailableStatus = this.#optimizationStatuses.reduce( + (acc, v) => acc | v, + 0, + ); + return { + type: this.toString(), + optimizationStatuses: translateStatus(allAvailableStatus), + }; + } } module.exports = { - V8GetOptimizationStatus, + V8GetOptimizationStatus, }; diff --git a/lib/report.js b/lib/report.js index d53953b..425e037 100644 --- a/lib/report.js +++ b/lib/report.js @@ -1,11 +1,11 @@ -const { textReport } = require('./reporter/text'); -const { chartReport } = require('./reporter/chart'); -const { htmlReport } = require('./reporter/html'); -const { jsonReport } = require('./reporter/json'); +const { textReport } = require("./reporter/text"); +const { chartReport } = require("./reporter/chart"); +const { htmlReport } = require("./reporter/html"); +const { jsonReport } = require("./reporter/json"); module.exports = { - chartReport, - textReport, - htmlReport, - jsonReport, + chartReport, + textReport, + htmlReport, + jsonReport, }; diff --git a/lib/reporter/chart.js b/lib/reporter/chart.js index 34a48d2..443475a 100644 --- a/lib/reporter/chart.js +++ b/lib/reporter/chart.js @@ -1,36 +1,38 @@ -const { platform, arch, cpus, totalmem } = require('node:os'); +const { platform, arch, cpus, totalmem } = require("node:os"); const formatter = Intl.NumberFormat(undefined, { - notation: 'standard', - maximumFractionDigits: 2, + notation: "standard", + maximumFractionDigits: 2, }); function drawBar(label, value, total, length = 30) { - const percentage = value / total; - const filledLength = Math.round(length * percentage); - const bar = '█'.repeat(filledLength) + '-'.repeat(length - filledLength); + const percentage = value / total; + const filledLength = Math.round(length * percentage); + const bar = "█".repeat(filledLength) + "-".repeat(length - filledLength); - const opsSecReported = value < 100 ? - value.toFixed(2) : - value.toFixed(0); - process.stdout.write(`${label.padEnd(45)} | ${bar} | ${formatter.format(opsSecReported)} ops/sec\n`); + const opsSecReported = value < 100 ? value.toFixed(2) : value.toFixed(0); + process.stdout.write( + `${label.padEnd(45)} | ${bar} | ${formatter.format(opsSecReported)} ops/sec\n`, + ); } const environment = { - platform: `${platform()} ${arch()}`, - hardware: `${cpus().length} vCPUs | ${(totalmem() / (1024 ** 3)).toFixed(1)}GB Mem`, + platform: `${platform()} ${arch()}`, + hardware: `${cpus().length} vCPUs | ${(totalmem() / 1024 ** 3).toFixed(1)}GB Mem`, }; function chartReport(results) { - const maxOpsSec = Math.max(...results.map(b => b.opsSec)); + const maxOpsSec = Math.max(...results.map((b) => b.opsSec)); - process.stdout.write(`Platform: ${environment.platform}\n` + - `CPU Cores: ${environment.hardware}\n\n`); - results.forEach(result => { - drawBar(result.name, result.opsSec, maxOpsSec); - }); + process.stdout.write( + `Platform: ${environment.platform}\n` + + `CPU Cores: ${environment.hardware}\n\n`, + ); + for (const result of results) { + drawBar(result.name, result.opsSec, maxOpsSec); + } } module.exports = { - chartReport, + chartReport, }; diff --git a/lib/reporter/html.js b/lib/reporter/html.js index 75e5df2..58b2973 100644 --- a/lib/reporter/html.js +++ b/lib/reporter/html.js @@ -1,24 +1,35 @@ -const fs = require('node:fs'); -const path = require('node:path'); +const fs = require("node:fs"); +const path = require("node:path"); const formatter = Intl.NumberFormat(undefined, { - notation: 'standard', - maximumFractionDigits: 2, + notation: "standard", + maximumFractionDigits: 2, }); const opsToDuration = (maxOps, ops, scalingFactor = 10) => { - const baseSpeed = (maxOps / ops) * scalingFactor; - return Math.max(baseSpeed, 2); // Normalize speed with a minimum of 2 seconds + const baseSpeed = (maxOps / ops) * scalingFactor; + return Math.max(baseSpeed, 2); // Normalize speed with a minimum of 2 seconds }; const generateHTML = (template, durations) => { - let css = '' - let circleDiv = '' - let labelDiv = '' - let position = 20; - const colors = ['blue', 'orange', 'yellow', 'purple', 'black', 'grey', 'red', 'green', 'pink', 'cyan']; - for (const d of durations) { - css += ` + let css = ""; + let circleDiv = ""; + let labelDiv = ""; + let position = 20; + const colors = [ + "blue", + "orange", + "yellow", + "purple", + "black", + "grey", + "red", + "green", + "pink", + "cyan", + ]; + for (const d of durations) { + css += ` #label-${d.name} { top: ${position}px; } @@ -27,46 +38,44 @@ const generateHTML = (template, durations) => { background-color: ${colors.shift()}; top: ${position}px; } - ` - circleDiv += ` + `; + circleDiv += `
${d.name} (${d.opsSecFormatted} ops/sec)
- ` - labelDiv += ` + `; + labelDiv += `
- ` + `; - position += 80; - } + position += 80; + } - return template - .replaceAll('{{DURATIONS}}', JSON.stringify(durations)) - .replaceAll('{{CSS}}', css) - .replaceAll('{{LABEL_DIV}}', labelDiv) - .replaceAll('{{CIRCLE_DIV}}', circleDiv) - .replaceAll('{{CONTAINER_HEIGHT}}', `${durations.length * 100}px;`); + return template + .replaceAll("{{DURATIONS}}", JSON.stringify(durations)) + .replaceAll("{{CSS}}", css) + .replaceAll("{{LABEL_DIV}}", labelDiv) + .replaceAll("{{CIRCLE_DIV}}", circleDiv) + .replaceAll("{{CONTAINER_HEIGHT}}", `${durations.length * 100}px;`); }; -const templatePath = path.join(__dirname, 'template.html'); -const template = fs.readFileSync(templatePath, 'utf8'); +const templatePath = path.join(__dirname, "template.html"); +const template = fs.readFileSync(templatePath, "utf8"); function htmlReport(results) { - const maxOpsSec = Math.max(...results.map(b => b.opsSec)); + const maxOpsSec = Math.max(...results.map((b) => b.opsSec)); - const durations = results.map((r) => ({ - name: r.name.replaceAll(' ', '-'), - duration: opsToDuration(maxOpsSec, r.opsSec), - opsSecFormatted: formatter.format(r.opsSec) - })); + const durations = results.map((r) => ({ + name: r.name.replaceAll(" ", "-"), + duration: opsToDuration(maxOpsSec, r.opsSec), + opsSecFormatted: formatter.format(r.opsSec), + })); - const htmlContent = generateHTML(template, durations); - fs.writeFileSync('result.html', htmlContent, 'utf8'); - process.stdout.write('HTML file has been generated: result.html'); + const htmlContent = generateHTML(template, durations); + fs.writeFileSync("result.html", htmlContent, "utf8"); + process.stdout.write("HTML file has been generated: result.html"); } - module.exports = { - htmlReport, -} - + htmlReport, +}; diff --git a/lib/reporter/json.js b/lib/reporter/json.js index 48f60d3..d63aaec 100644 --- a/lib/reporter/json.js +++ b/lib/reporter/json.js @@ -1,28 +1,24 @@ -'use strict' +const { timer } = require("../clock"); -const { timer } = require('../clock') +function jsonReport(results) { + const output = results.map((result) => { + const opsSecReported = + result.opsSec < 100 ? result.opsSec.toFixed(2) : result.opsSec.toFixed(0); -function jsonReport (results) { - const output = results.map((result) => { - const opsSecReported = - result.opsSec < 100 - ? result.opsSec.toFixed(2) - : result.opsSec.toFixed(0) + return { + name: result.name, + opsSec: Number(opsSecReported), + runsSampled: result.histogram.samples, + min: timer.format(result.histogram.min), + max: timer.format(result.histogram.max), + // Report anything the plugins returned + plugins: result.plugins.map((p) => p.report).filter(Boolean), + }; + }); - return { - name: result.name, - opsSec: Number(opsSecReported), - runsSampled: result.histogram.samples, - min: timer.format(result.histogram.min), - max: timer.format(result.histogram.max), - // Report anything the plugins returned - plugins: result.plugins.map((p) => p.report).filter(Boolean) - } - }) - - console.log(JSON.stringify(output, null, 2)) + console.log(JSON.stringify(output, null, 2)); } module.exports = { - jsonReport -} + jsonReport, +}; diff --git a/lib/reporter/text.js b/lib/reporter/text.js index a792d7a..680f926 100644 --- a/lib/reporter/text.js +++ b/lib/reporter/text.js @@ -1,44 +1,52 @@ -const util = require('node:util'); +const util = require("node:util"); const styleText = - typeof util.styleText === 'function' ? - util.styleText - : (_style, text) => text; + typeof util.styleText === "function" + ? util.styleText + : (_style, text) => text; -const { timer } = require('../clock'); +const { timer } = require("../clock"); const formatter = Intl.NumberFormat(undefined, { - notation: 'standard', - maximumFractionDigits: 2, + notation: "standard", + maximumFractionDigits: 2, }); function textReport(results) { - for (const result of results) { - const opsSecReported = result.opsSec < 100 ? - result.opsSec.toFixed(2) : - result.opsSec.toFixed(0); - - process.stdout.write(result.name.padEnd(45)); - process.stdout.write(' x '); - process.stdout.write(styleText(['cyan', 'bold'], `${ formatter.format(opsSecReported) } ops/sec`)); - // TODO: produce confidence on stddev - // process.stdout.write(result.histogram.stddev.toString()); - process.stdout.write(` (${ result.histogram.samples } runs sampled) `); - - for (const p of result.plugins) { - if (p.report) { - process.stdout.write(styleText('dim', `${p.report} `)); - } - } - - process.stdout.write('min..max=('); - process.stdout.write(styleText('green', timer.format(result.histogram.min))); - process.stdout.write(styleText('dim', '...')); - process.stdout.write(styleText('red', `${timer.format(result.histogram.max)})`)); - process.stdout.write('\n'); - } + for (const result of results) { + const opsSecReported = + result.opsSec < 100 ? result.opsSec.toFixed(2) : result.opsSec.toFixed(0); + + process.stdout.write(result.name.padEnd(45)); + process.stdout.write(" x "); + process.stdout.write( + styleText( + ["cyan", "bold"], + `${formatter.format(opsSecReported)} ops/sec`, + ), + ); + // TODO: produce confidence on stddev + // process.stdout.write(result.histogram.stddev.toString()); + process.stdout.write(` (${result.histogram.samples} runs sampled) `); + + for (const p of result.plugins) { + if (p.report) { + process.stdout.write(styleText("dim", `${p.report} `)); + } + } + + process.stdout.write("min..max=("); + process.stdout.write( + styleText("green", timer.format(result.histogram.min)), + ); + process.stdout.write(styleText("dim", "...")); + process.stdout.write( + styleText("red", `${timer.format(result.histogram.max)})`), + ); + process.stdout.write("\n"); + } } module.exports = { - textReport, + textReport, }; diff --git a/lib/validators.js b/lib/validators.js index 76d47b3..bc701c0 100644 --- a/lib/validators.js +++ b/lib/validators.js @@ -1,54 +1,71 @@ -function ERR_INVALID_ARG_TYPE (message) { - const err = new Error(message); - err.code = 'ERR_INVALID_ARG_TYPE'; - return err; +function ERR_INVALID_ARG_TYPE(message) { + const err = new Error(message); + err.code = "ERR_INVALID_ARG_TYPE"; + return err; } -function ERR_INVALID_ARG_VALUE (message) { - const err = new Error(message); - err.code = 'ERR_INVALID_ARG_VALUE'; - return err; +function ERR_INVALID_ARG_VALUE(message) { + const err = new Error(message); + err.code = "ERR_INVALID_ARG_VALUE"; + return err; } -function validateNumber(value, name, min = undefined, max) { - if (typeof value !== 'number') - throw ERR_INVALID_ARG_TYPE(`value must be a number, name: ${name}, value: ${value}`); +function validateNumber(value, name, min, max) { + if (typeof value !== "number") + throw ERR_INVALID_ARG_TYPE( + `value must be a number, name: ${name}, value: ${value}`, + ); - if ((min != null && value < min) || (max != null && value > max) || - ((min != null || max != null) && Number.isNaN(value))) { - throw ERR_INVALID_ARG_VALUE(`value must be ${min != null ? `>= ${min}` : ''}${min != null && max != null ? ' && ' : ''}${max != null ? `<= ${max}` : ''}, name: ${name}, value: ${value}`); - } + if ( + (min != null && value < min) || + (max != null && value > max) || + ((min != null || max != null) && Number.isNaN(value)) + ) { + throw ERR_INVALID_ARG_VALUE( + `value must be ${min != null ? `>= ${min}` : ""}${min != null && max != null ? " && " : ""}${max != null ? `<= ${max}` : ""}, name: ${name}, value: ${value}`, + ); + } } function validateObject(value, name) { - if (value === null || Array.isArray(value)) { - throw ERR_INVALID_ARG_TYPE(`value must be an object, name: ${name}, value: ${value}`); - } + if (value === null || Array.isArray(value)) { + throw ERR_INVALID_ARG_TYPE( + `value must be an object, name: ${name}, value: ${value}`, + ); + } - if (typeof value !== 'object') { - throw ERR_INVALID_ARG_TYPE(`value must be an object, name: ${name}, value: ${value}`); - } + if (typeof value !== "object") { + throw ERR_INVALID_ARG_TYPE( + `value must be an object, name: ${name}, value: ${value}`, + ); + } } function validateFunction(value, name) { - if (typeof value !== 'function') - throw ERR_INVALID_ARG_TYPE(`value must be a function, name: ${name}, value: ${value}`); + if (typeof value !== "function") + throw ERR_INVALID_ARG_TYPE( + `value must be a function, name: ${name}, value: ${value}`, + ); } function validateString(value, name) { - if (typeof value !== 'string') - throw ERR_INVALID_ARG_TYPE(`value must be a string, name: ${name}, value: ${value}`); + if (typeof value !== "string") + throw ERR_INVALID_ARG_TYPE( + `value must be a string, name: ${name}, value: ${value}`, + ); } function validateArray(value, name) { - if (!Array.isArray(value)) - throw ERR_INVALID_ARG_TYPE(`value must be a array, name: ${name}, value: ${value}`); + if (!Array.isArray(value)) + throw ERR_INVALID_ARG_TYPE( + `value must be a array, name: ${name}, value: ${value}`, + ); } module.exports = { - validateFunction, - validateNumber, - validateObject, - validateString, - validateArray, + validateFunction, + validateNumber, + validateObject, + validateString, + validateArray, }; diff --git a/lib/worker-runner.js b/lib/worker-runner.js index f4fc3f4..fc1dcb3 100644 --- a/lib/worker-runner.js +++ b/lib/worker-runner.js @@ -1,15 +1,20 @@ -'use strict'; - -const { parentPort } = require('worker_threads'); -const { runBenchmark } = require('./lifecycle'); +const { parentPort } = require("node:worker_threads"); +const { runBenchmark } = require("./lifecycle"); // Deserialize the benchmark function function deserializeBenchmark(benchmark) { - benchmark.fn = new Function(benchmark.fn); + benchmark.fn = new Function(benchmark.fn); } -parentPort.on('message', async ({ benchmark, initialIterations, repeatSuite }) => { - deserializeBenchmark(benchmark); - const result = await runBenchmark(benchmark, initialIterations, repeatSuite); - parentPort.postMessage(result); -}); +parentPort.on( + "message", + async ({ benchmark, initialIterations, repeatSuite }) => { + deserializeBenchmark(benchmark); + const result = await runBenchmark( + benchmark, + initialIterations, + repeatSuite, + ); + parentPort.postMessage(result); + }, +); diff --git a/package.json b/package.json index fad2318..d84c151 100644 --- a/package.json +++ b/package.json @@ -1,32 +1,37 @@ { - "name": "bench-node", - "version": "0.5.0", - "description": "", - "main": "lib/index.js", - "scripts": { - "test": "node --test --allow-natives-syntax" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/RafaelGSS/bench-node.git" - }, - "keywords": [ - "benchmark", - "nodejs" - ], - "author": "RafaelGSS ", - "contributors": [ - { - "name": "H4ad", - "author": true - } - ], - "license": "MIT", - "bugs": { - "url": "https://github.com/RafaelGSS/bench-node/issues" - }, - "homepage": "https://github.com/RafaelGSS/bench-node#readme", - "dependencies": { - "piscina": "^4.8.0" - } + "name": "bench-node", + "version": "0.5.0", + "description": "", + "main": "lib/index.js", + "scripts": { + "test": "node --test --allow-natives-syntax", + "lint": "biome lint .", + "lint:ci": "biome ci .", + "lint:fix": "biome lint --write .", + "lint:force-fix": "biome lint --write --unsafe .", + "format": "biome format --write ." + }, + "repository": { + "type": "git", + "url": "git+https://github.com/RafaelGSS/bench-node.git" + }, + "keywords": ["benchmark", "nodejs"], + "author": "RafaelGSS ", + "contributors": [ + { + "name": "H4ad", + "author": true + } + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/RafaelGSS/bench-node/issues" + }, + "homepage": "https://github.com/RafaelGSS/bench-node#readme", + "dependencies": { + "piscina": "^4.8.0" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4" + } } diff --git a/test/async.js b/test/async.js index a23d5cf..00f182f 100644 --- a/test/async.js +++ b/test/async.js @@ -1,5 +1,3 @@ -const { todo } = require('node:test'); +const { todo } = require("node:test"); -todo('async tasks should behave similar to sync tasks', async () => { - -}); +todo("async tasks should behave similar to sync tasks", async () => {}); diff --git a/test/basic.js b/test/basic.js index 710476b..4871f61 100644 --- a/test/basic.js +++ b/test/basic.js @@ -1,188 +1,218 @@ -const { Suite } = require('../lib/index'); -const { describe, it, todo } = require('node:test'); -const assert = require('node:assert'); -const { spawnSync } = require('node:child_process'); -const path = require('node:path'); +const { Suite } = require("../lib/index"); +const { describe, it, todo } = require("node:test"); +const assert = require("node:assert"); +const { spawnSync } = require("node:child_process"); +const path = require("node:path"); function noop() {} -describe('API Interface', () => { - it('options should be an object', () => { - [1, 'ds', null].forEach((r) => { - assert.throws(() => { - new Suite(r); - }, { - code: 'ERR_INVALID_ARG_TYPE', - }); - }); - // doesNotThrow - new Suite({}); - }); - - it('reporter should be a function', () => { - [1, 'ds', {}].forEach((r) => { - assert.throws(() => { - new Suite({ reporter: r }); - }, { - code: 'ERR_INVALID_ARG_TYPE' - }); - }); - // doesNotThrow - new Suite({ reporter: () => {} }); - }); - - it('reporter can be false or null', () => { - [false, null].forEach((r) => { - // doesNotThrow - new Suite({ reporter: r }); - }); - }); - - describe('suite.add', () => { - const bench = new Suite({ reporter: noop }); - it('name should be an string', () => { - [1, undefined, null, {}].forEach((r) => { - assert.throws(() => { - bench.add(r); - }); - }); - // doesNotThrow - bench.add('example', noop); - }); - - it('options should be an valid object', () => { - [1, 'ds', null].forEach((r) => { - assert.throws(() => { - bench.add('name', r, noop); - }, { - code: 'ERR_INVALID_ARG_TYPE', - }); - }); - }); - - it('minTime should be a valid number', () => { - ['ds', {}, () => {}].forEach((r) => { - assert.throws(() => { - bench.add('name', { minTime: r }, noop); - }, { - code: 'ERR_INVALID_ARG_TYPE', - }); - }); - assert.throws(() => { - bench.add('name', { minTime: 0 }, noop); - }, { - code: 'ERR_INVALID_ARG_VALUE', - }); - assert.throws(() => { - bench.add('name', { minTime: 0.000001 }, noop); - }, { - code: 'ERR_INVALID_ARG_VALUE', - }); - // doesNotThrow - bench.add('name', { minTime: 0.5 }, noop); - }) - - it('maxTime should be a valid number', () => { - ['ds', {}, () => {}].forEach((r) => { - assert.throws(() => { - bench.add('name', { minTime: r }, noop); - }, { - code: 'ERR_INVALID_ARG_TYPE', - }); - }); - }); - it('maxTime should be greater than minTime', () => { - assert.throws(() => { - bench.add('name', { maxTime: 0 }, noop); - }, { - code: 'ERR_INVALID_ARG_VALUE', - }); - assert.throws(() => { - bench.add('name', { maxTime: 0.1, minTime: 0.2 }, noop); - }, { - code: 'ERR_INVALID_ARG_VALUE', - }); - // doesNotThrow - bench.add('name', { minTime: 0.01, maxTime: 0.02 }, noop); - }); - - it('fn should be a function', () => { - ['ds', {}, 42].forEach((r) => { - assert.throws(() => { - bench.add('name', {}, r); - }, { - code: 'ERR_INVALID_ARG_TYPE', - }); - }); - // doesNotThrow - bench.add('name', noop); - }); - - it('repeatSuite should be a valid number', () => { - ['ds', {}, () => {}].forEach((r) => { - assert.throws(() => { - bench.add('name', { repeatSuite: r }, noop); - }, { - code: 'ERR_INVALID_ARG_TYPE', - }); - }); - }); - }); +describe("API Interface", () => { + it("options should be an object", () => { + for (const r of [1, "ds", null]) { + assert.throws( + () => { + new Suite(r); + }, + { + code: "ERR_INVALID_ARG_TYPE", + }, + ); + } + // doesNotThrow + new Suite({}); + }); + + it("reporter should be a function", () => { + for (const r of [1, "ds", {}]) { + assert.throws( + () => { + new Suite({ reporter: r }); + }, + { + code: "ERR_INVALID_ARG_TYPE", + }, + ); + } + // doesNotThrow + new Suite({ reporter: () => {} }); + }); + + it("reporter can be false or null", () => { + for (const r of [false, null]) { + // doesNotThrow + new Suite({ reporter: r }); + } + }); + + describe("suite.add", () => { + const bench = new Suite({ reporter: noop }); + it("name should be an string", () => { + for (const r of [1, undefined, null, {}]) { + assert.throws(() => { + bench.add(r); + }); + } + // doesNotThrow + bench.add("example", noop); + }); + + it("options should be an valid object", () => { + for (const r of [1, "ds", null]) { + assert.throws( + () => { + bench.add("name", r, noop); + }, + { + code: "ERR_INVALID_ARG_TYPE", + }, + ); + } + }); + + it("minTime should be a valid number", () => { + for (const r of ["ds", {}, () => {}]) { + assert.throws( + () => { + bench.add("name", { minTime: r }, noop); + }, + { + code: "ERR_INVALID_ARG_TYPE", + }, + ); + } + assert.throws( + () => { + bench.add("name", { minTime: 0 }, noop); + }, + { + code: "ERR_INVALID_ARG_VALUE", + }, + ); + assert.throws( + () => { + bench.add("name", { minTime: 0.000001 }, noop); + }, + { + code: "ERR_INVALID_ARG_VALUE", + }, + ); + // doesNotThrow + bench.add("name", { minTime: 0.5 }, noop); + }); + + it("maxTime should be a valid number", () => { + for (const r of ["ds", {}, () => {}]) { + assert.throws( + () => { + bench.add("name", { minTime: r }, noop); + }, + { + code: "ERR_INVALID_ARG_TYPE", + }, + ); + } + }); + it("maxTime should be greater than minTime", () => { + assert.throws( + () => { + bench.add("name", { maxTime: 0 }, noop); + }, + { + code: "ERR_INVALID_ARG_VALUE", + }, + ); + assert.throws( + () => { + bench.add("name", { maxTime: 0.1, minTime: 0.2 }, noop); + }, + { + code: "ERR_INVALID_ARG_VALUE", + }, + ); + // doesNotThrow + bench.add("name", { minTime: 0.01, maxTime: 0.02 }, noop); + }); + + it("fn should be a function", () => { + for (const r of ["ds", {}, 42]) { + assert.throws( + () => { + bench.add("name", {}, r); + }, + { + code: "ERR_INVALID_ARG_TYPE", + }, + ); + } + // doesNotThrow + bench.add("name", noop); + }); + + it("repeatSuite should be a valid number", () => { + for (const r of ["ds", {}, () => {}]) { + assert.throws( + () => { + bench.add("name", { repeatSuite: r }, noop); + }, + { + code: "ERR_INVALID_ARG_TYPE", + }, + ); + } + }); + }); }); -describe('simple usage', async () => { - const bench = new Suite({ reporter: noop }); - bench - .add('foo', async () => { - await new Promise((resolve) => setTimeout((resolve), 50)); - }) - .add('bar', async () => { - await new Promise((resolve) => setTimeout(resolve, 100)); - }); - - const [bench1, bench2] = await bench.run(); - - - it('benchmark name should be returned in results', () => { - assert.strictEqual(bench1.name, 'foo'); - assert.strictEqual(bench2.name, 'bar'); - }); - - it('ops/sec should match the expected duration', () => { - // 1000(ms)/50 = 20 + cost of creating promises - assert.ok(bench1.opsSec > 18 && bench1.opsSec <= 20); - // 1000(ms)/100 = 100 + cost of creating promises - assert.ok(bench2.opsSec > 8 && bench2.opsSec <= 10); - }); - - it('tasks should have at least 10 samples', () => { - assert.ok(bench1.iterations >= 10); - assert.ok(bench2.iterations >= 10); - }); +describe("simple usage", async () => { + const bench = new Suite({ reporter: noop }); + bench + .add("foo", async () => { + await new Promise((resolve) => setTimeout(resolve, 50)); + }) + .add("bar", async () => { + await new Promise((resolve) => setTimeout(resolve, 100)); + }); + + const [bench1, bench2] = await bench.run(); + + it("benchmark name should be returned in results", () => { + assert.strictEqual(bench1.name, "foo"); + assert.strictEqual(bench2.name, "bar"); + }); + + it("ops/sec should match the expected duration", () => { + // 1000(ms)/50 = 20 + cost of creating promises + assert.ok(bench1.opsSec > 18 && bench1.opsSec <= 20); + // 1000(ms)/100 = 100 + cost of creating promises + assert.ok(bench2.opsSec > 8 && bench2.opsSec <= 10); + }); + + it("tasks should have at least 10 samples", () => { + assert.ok(bench1.iterations >= 10); + assert.ok(bench2.iterations >= 10); + }); }); -describe('throws when a benchmark task throw', async () => { - const bench = new Suite(); - const err = new Error(); +describe("throws when a benchmark task throw", async () => { + const bench = new Suite(); + const err = new Error(); - bench.add('error', () => { - throw err; - }); - assert.rejects(() => bench.run()); + bench.add("error", () => { + throw err; + }); + assert.rejects(() => bench.run()); }); -describe('when no --allow-natives-syntax', async () => { - it('should throw', () => { - const file = path.join(__dirname, 'fixtures', 'bench.js'); - const { status, stderr } = spawnSync( - process.execPath, - [file], - ); - assert.strictEqual(status, 1); - assert.match(stderr.toString(), /bench-node module must be run with --allow-natives-syntax/); - }); +describe("when no --allow-natives-syntax", async () => { + it("should throw", () => { + const file = path.join(__dirname, "fixtures", "bench.js"); + const { status, stderr } = spawnSync(process.execPath, [file]); + assert.strictEqual(status, 1); + assert.match( + stderr.toString(), + /bench-node module must be run with --allow-natives-syntax/, + ); + }); }); -todo('histogram values', async () => { - -}); +todo("histogram values", async () => {}); diff --git a/test/env.js b/test/env.js index 43e4607..504ff1b 100644 --- a/test/env.js +++ b/test/env.js @@ -1,95 +1,127 @@ -const { describe, it, before } = require('node:test'); -const assert = require('node:assert'); -const { Suite } = require('../lib'); -const copyBench = require('./fixtures/copy'); -const { managedBench, managedOptBench } = require('./fixtures/opt-managed'); +const { describe, it, before } = require("node:test"); +const assert = require("node:assert"); +const { Suite } = require("../lib"); +const copyBench = require("./fixtures/copy"); +const { managedBench, managedOptBench } = require("./fixtures/opt-managed"); -function assertMinBenchmarkDifference(results, { percentageLimit, ciPercentageLimit }) { - assertBenchmarkDifference(results, { percentageLimit, ciPercentageLimit, greaterThan: true }); +function assertMinBenchmarkDifference( + results, + { percentageLimit, ciPercentageLimit }, +) { + assertBenchmarkDifference(results, { + percentageLimit, + ciPercentageLimit, + greaterThan: true, + }); } -function assertMaxBenchmarkDifference(results, { percentageLimit, ciPercentageLimit }) { - assertBenchmarkDifference(results, { percentageLimit, ciPercentageLimit, greaterThan: false }); +function assertMaxBenchmarkDifference( + results, + { percentageLimit, ciPercentageLimit }, +) { + assertBenchmarkDifference(results, { + percentageLimit, + ciPercentageLimit, + greaterThan: false, + }); } -function assertBenchmarkDifference(results, { percentageLimit, ciPercentageLimit, greaterThan }) { - for (let i = 0; i < results.length; i++) { - for (let j = 0; j < results.length; j++) { - if (i !== j) { - const opsSec1 = results[i].opsSec; - const opsSec2 = results[j].opsSec; +function assertBenchmarkDifference( + results, + { percentageLimit, ciPercentageLimit, greaterThan }, +) { + for (let i = 0; i < results.length; i++) { + for (let j = 0; j < results.length; j++) { + if (i !== j) { + const opsSec1 = results[i].opsSec; + const opsSec2 = results[j].opsSec; - // Calculate the percentage difference - const difference = Math.abs(opsSec1 - opsSec2); - const percentageDifference = (difference / Math.min(opsSec1, opsSec2)) * 100; + // Calculate the percentage difference + const difference = Math.abs(opsSec1 - opsSec2); + const percentageDifference = + (difference / Math.min(opsSec1, opsSec2)) * 100; - // Check if the percentage difference is less than or equal to 10% - if (process.env.CI) { - // CI runs in a shared-env so the percentage of difference - // must be greather there due to high variance of hardware - assert.ok( - greaterThan ? percentageLimit >= ciPercentageLimit : percentageDifference <= ciPercentageLimit, - `"${results[i].name}" too different from "${results[j].name}" - ${percentageDifference} != ${ciPercentageLimit}` - ); - } else { - assert.ok( - greaterThan ? percentageLimit >= percentageLimit : percentageDifference <= percentageLimit, - `${results[i].name} too different from ${results[j].name} - ${percentageDifference} != ${percentageLimit}` - ); - } - } - } - } + // Check if the percentage difference is less than or equal to 10% + if (process.env.CI) { + // CI runs in a shared-env so the percentage of difference + // must be greather there due to high variance of hardware + assert.ok( + greaterThan + ? percentageDifference >= ciPercentageLimit + : percentageDifference <= ciPercentageLimit, + `"${results[i].name}" too different from "${results[j].name}" - ${percentageDifference} != ${ciPercentageLimit}`, + ); + } else { + assert.ok( + greaterThan + ? percentageDifference >= percentageLimit + : percentageDifference <= percentageLimit, + `${results[i].name} too different from ${results[j].name} - ${percentageDifference} != ${percentageLimit}`, + ); + } + } + } + } } -describe('Same benchmark function', () => { - let results; +describe("Same benchmark function", () => { + let results; - before(async () => { - results = await copyBench.run(); - }); + before(async () => { + results = await copyBench.run(); + }); - it('must have a similar benchmark result', () => { - assertMaxBenchmarkDifference(results, { percentageLimit: 10, ciPercentageLimit: 30 }); - }); + it("must have a similar benchmark result", () => { + assertMaxBenchmarkDifference(results, { + percentageLimit: 10, + ciPercentageLimit: 30, + }); + }); }); -describe('Managed can be V8 optimized', () => { - let optResults, results; +describe("Managed can be V8 optimized", () => { + let optResults; + let results; - before(async () => { - optResults = await managedOptBench.run(); - results = await managedBench.run(); - }); + before(async () => { + optResults = await managedOptBench.run(); + results = await managedBench.run(); + }); - it('should be more than 50% different from unmanaged', () => { - assertMinBenchmarkDifference(optResults, { percentageLimit: 50, ciPercentageLimit: 30 }); - }); + it("should be more than 50% different from unmanaged", () => { + assertMinBenchmarkDifference(optResults, { + percentageLimit: 50, + ciPercentageLimit: 30, + }); + }); - // it('should be similar when avoiding V8 optimizatio', () => { - // assertBenchmarkDifference(results, 50, 30); - // }); + // it('should be similar when avoiding V8 optimizatio', () => { + // assertBenchmarkDifference(results, 50, 30); + // }); }); -describe('Workers should have parallel context', () => { - let results; - before(async () => { - const bench = new Suite({ - reporter: () => {}, - useWorkers: true, - }); +describe("Workers should have parallel context", () => { + let results; + before(async () => { + const bench = new Suite({ + reporter: () => {}, + useWorkers: true, + }); - bench - .add('Import with node: prefix', () => { - return import('node:fs'); - }) - .add('Import without node: prefix', () => { - return import('fs'); - }); - results = await bench.run(); - }); + bench + .add("Import with node: prefix", () => { + return import("node:fs"); + }) + .add("Import without node: prefix", () => { + return import("node:fs"); + }); + results = await bench.run(); + }); - it('should have a similar result as they will not share import.meta.cache', () => { - assertMaxBenchmarkDifference(results, { percentageLimit: 10, ciPercentageLimit: 30 }); - }); + it("should have a similar result as they will not share import.meta.cache", () => { + assertMaxBenchmarkDifference(results, { + percentageLimit: 10, + ciPercentageLimit: 30, + }); + }); }); diff --git a/test/fixtures/bench.js b/test/fixtures/bench.js index 7d3bdb3..c94d279 100644 --- a/test/fixtures/bench.js +++ b/test/fixtures/bench.js @@ -1,8 +1,8 @@ -const { Suite } = require('../../lib'); +const { Suite } = require("../../lib"); const suite = new Suite(); suite - .add(`empty`, function () {}) - .add(`empty async`, async function () {}) - .run(); + .add("empty", () => {}) + .add("empty async", async () => {}) + .run(); diff --git a/test/fixtures/copy.js b/test/fixtures/copy.js index 91ee516..3b191a7 100644 --- a/test/fixtures/copy.js +++ b/test/fixtures/copy.js @@ -1,19 +1,22 @@ -const { Suite } = require('../../lib'); +const { Suite } = require("../../lib"); const suite = new Suite({ reporter: false }); suite - .add('Using includes', function () { - const text = 'text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8' - const r = text.includes('application/json') - }) - .add('Using includes 2', function () { - const text = 'text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8' - const r = text.includes('application/json') - }) - .add('Using includes 3', function () { - const text = 'text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8' - const r = text.includes('application/json') - }) + .add("Using includes", () => { + const text = + "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; + const r = text.includes("application/json"); + }) + .add("Using includes 2", () => { + const text = + "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; + const r = text.includes("application/json"); + }) + .add("Using includes 3", () => { + const text = + "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; + const r = text.includes("application/json"); + }); module.exports = suite; diff --git a/test/fixtures/opt-managed.js b/test/fixtures/opt-managed.js index 93bfcf3..fc99c5c 100644 --- a/test/fixtures/opt-managed.js +++ b/test/fixtures/opt-managed.js @@ -1,45 +1,49 @@ -const { Suite } = require('../../lib'); -const assert = require('node:assert'); +const { Suite } = require("../../lib"); +const assert = require("node:assert"); const suite = new Suite({ reporter: false }); suite - .add('Using includes', function () { - const text = 'text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8' - const r = text.includes('application/json') - assert.ok(r) - }) - .add('[Managed] Using includes', function (timer) { - timer.start() - for (let i = 0; i < timer.count; i++) { - const text = 'text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8' - const r = text.includes('application/json') - assert.ok(r) - } - timer.end(timer.count) - }) + .add("Using includes", () => { + const text = + "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; + const r = text.includes("application/json"); + assert.ok(r); + }) + .add("[Managed] Using includes", (timer) => { + timer.start(); + for (let i = 0; i < timer.count; i++) { + const text = + "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; + const r = text.includes("application/json"); + assert.ok(r); + } + timer.end(timer.count); + }); suite.run(); const optSuite = new Suite({ reporter: false }); optSuite - .add('Using includes', function () { - const text = 'text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8' - const r = text.includes('application/json') - // assert.ok(r) - }) - .add('[Managed] Using includes', function (timer) { - timer.start() - for (let i = 0; i < timer.count; i++) { - const text = 'text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8' - const r = text.includes('application/json') - // assert.ok(r) - } - timer.end(timer.count) - }) + .add("Using includes", () => { + const text = + "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; + const r = text.includes("application/json"); + // assert.ok(r) + }) + .add("[Managed] Using includes", (timer) => { + timer.start(); + for (let i = 0; i < timer.count; i++) { + const text = + "text/html,application/xhtml+xml,application/xml;application/json;q=0.9,image/avif,image/webp,*/*;q=0.8"; + const r = text.includes("application/json"); + // assert.ok(r) + } + timer.end(timer.count); + }); module.exports = { - managedBench: suite, - managedOptBench: optSuite, + managedBench: suite, + managedOptBench: optSuite, }; diff --git a/test/managed.js b/test/managed.js index 10c61a4..f8a5df2 100644 --- a/test/managed.js +++ b/test/managed.js @@ -1,5 +1,3 @@ -const { todo } = require('node:test'); +const { todo } = require("node:test"); -todo('managed benchmarks', async () => { - -}); +todo("managed benchmarks", async () => {}); diff --git a/test/plugins.js b/test/plugins.js index ea3cf60..760b5c9 100644 --- a/test/plugins.js +++ b/test/plugins.js @@ -1,90 +1,114 @@ const { - Suite, - V8NeverOptimizePlugin, - V8GetOptimizationStatus, - V8OptimizeOnNextCallPlugin, -} = require('../lib/index'); -const { describe, it } = require('node:test'); -const assert = require('node:assert'); + Suite, + V8NeverOptimizePlugin, + V8GetOptimizationStatus, + V8OptimizeOnNextCallPlugin, +} = require("../lib/index"); +const { describe, it } = require("node:test"); +const assert = require("node:assert"); class InvalidPlugin {} class ValidPlugin { - toString() { return '' } - isSupported() { return true } + toString() { + return ""; + } + isSupported() { + return true; + } } -describe('Plugins validation', () => { - it('should be an object with expected methods', () => { - [1, 'ds', {}].forEach((r) => { - assert.throws(() => { - new Suite({ plugins: r }); - }, { - code: 'ERR_INVALID_ARG_TYPE', - }); - }); - assert.throws(() => { - new Suite({ plugins: [new InvalidPlugin()] }); - }, { - code: 'ERR_INVALID_ARG_TYPE', - }); - // doesNotThrow - new Suite({ plugins: [new ValidPlugin()] }); - }); +describe("Plugins validation", () => { + it("should be an object with expected methods", () => { + for (const r of [1, "ds", {}]) { + assert.throws( + () => { + new Suite({ plugins: r }); + }, + { + code: "ERR_INVALID_ARG_TYPE", + }, + ); + } + assert.throws( + () => { + new Suite({ plugins: [new InvalidPlugin()] }); + }, + { + code: "ERR_INVALID_ARG_TYPE", + }, + ); + // doesNotThrow + new Suite({ plugins: [new ValidPlugin()] }); + }); - it('beforeClockTemplate should return an array', () => { - class InvalidPlugin2 { - beforeClockTemplate() { - return 'error' - } - toString() { return '' } - isSupported() { return true } - } + it("beforeClockTemplate should return an array", () => { + class InvalidPlugin2 { + beforeClockTemplate() { + return "error"; + } + toString() { + return ""; + } + isSupported() { + return true; + } + } - assert.throws(() => { - new Suite({ plugins: [new InvalidPlugin2()] }); - }, { - code: 'ERR_INVALID_ARG_TYPE', - }); - }); + assert.throws( + () => { + new Suite({ plugins: [new InvalidPlugin2()] }); + }, + { + code: "ERR_INVALID_ARG_TYPE", + }, + ); + }); - it('afterClockTemplate should return an array', () => { - class InvalidPlugin2 { - beforeClockTemplate() { - return [''] - } - afterClockTemplate() { - return 'error' - } - toString() { return '' } - isSupported() { return true } - } + it("afterClockTemplate should return an array", () => { + class InvalidPlugin2 { + beforeClockTemplate() { + return [""]; + } + afterClockTemplate() { + return "error"; + } + toString() { + return ""; + } + isSupported() { + return true; + } + } - assert.throws(() => { - new Suite({ plugins: [new InvalidPlugin2()] }); - }, { - code: 'ERR_INVALID_ARG_TYPE', - }); - }); + assert.throws( + () => { + new Suite({ plugins: [new InvalidPlugin2()] }); + }, + { + code: "ERR_INVALID_ARG_TYPE", + }, + ); + }); }); -describe('Official plugins validation', () => { - it('V8NeverOptimizePlugin validation', () => { - const bench = new Suite({ - plugins: [new V8NeverOptimizePlugin()], - }); - assert.ok(bench); - }); - it('V8GetOptimizationStatus validation', () => { - const bench = new Suite({ - plugins: [new V8GetOptimizationStatus()], - }); - assert.ok(bench); - }); - it('V8OptimizeOnNextCallPlugin validation', () => { - const bench = new Suite({ - plugins: [new V8OptimizeOnNextCallPlugin()], - }); - assert.ok(bench); - }); -}) +describe("Official plugins validation", () => { + it("V8NeverOptimizePlugin validation", () => { + const bench = new Suite({ + plugins: [new V8NeverOptimizePlugin()], + }); + assert.ok(bench); + }); + it("V8GetOptimizationStatus validation", () => { + const bench = new Suite({ + plugins: [new V8GetOptimizationStatus()], + }); + assert.ok(bench); + }); + it("V8OptimizeOnNextCallPlugin validation", () => { + const bench = new Suite({ + plugins: [new V8OptimizeOnNextCallPlugin()], + }); + assert.ok(bench); + }); +}); diff --git a/test/reporter.js b/test/reporter.js index 3636e9e..f92317f 100644 --- a/test/reporter.js +++ b/test/reporter.js @@ -1,179 +1,192 @@ -const { describe, it, before } = require('node:test'); -const assert = require('node:assert'); -const fs = require('node:fs'); - -const { - Suite, - chartReport, - htmlReport, - jsonReport, -} = require('../lib'); - -describe('chartReport outputs benchmark results as a bar chart', async (t) => { - let output = ''; - - before(async () => { - const originalStdoutWrite = process.stdout.write; - process.stdout.write = function (data) { - output += data; - }; - - const suite = new Suite({ - reporter: chartReport, - }); - - suite - .add('single with matcher', function () { - const pattern = /[123]/g - const replacements = { 1: 'a', 2: 'b', 3: 'c' } - const subject = '123123123123123123123123123123123123123123123123' - const r = subject.replace(pattern, m => replacements[m]) - assert.ok(r); - }) - .add('multiple replaces', function () { - const subject = '123123123123123123123123123123123123123123123123' - const r = subject.replace(/1/g, 'a').replace(/2/g, 'b').replace(/3/g, 'c') - assert.ok(r); - }) - await suite.run(); - - process.stdout.write = originalStdoutWrite; - }); - - it('should include bar chart chars', () => { - assert.ok(output.includes('█')); - }); - - it('should include ops/sec', () => { - assert.ok(output.includes('ops/sec')); - }) +const { describe, it, before } = require("node:test"); +const assert = require("node:assert"); +const fs = require("node:fs"); + +const { Suite, chartReport, htmlReport, jsonReport } = require("../lib"); + +describe("chartReport outputs benchmark results as a bar chart", async (t) => { + let output = ""; + + before(async () => { + const originalStdoutWrite = process.stdout.write; + process.stdout.write = (data) => { + output += data; + }; + + const suite = new Suite({ + reporter: chartReport, + }); + + suite + .add("single with matcher", () => { + const pattern = /[123]/g; + const replacements = { 1: "a", 2: "b", 3: "c" }; + const subject = "123123123123123123123123123123123123123123123123"; + const r = subject.replace(pattern, (m) => replacements[m]); + assert.ok(r); + }) + .add("multiple replaces", () => { + const subject = "123123123123123123123123123123123123123123123123"; + const r = subject + .replace(/1/g, "a") + .replace(/2/g, "b") + .replace(/3/g, "c"); + assert.ok(r); + }); + await suite.run(); + + process.stdout.write = originalStdoutWrite; + }); + + it("should include bar chart chars", () => { + assert.ok(output.includes("█")); + }); + + it("should include ops/sec", () => { + assert.ok(output.includes("ops/sec")); + }); }); -describe('htmlReport should create a file', async (t) => { - let output = ''; - let htmlName = ''; - let htmlContent = ''; - - before(async () => { - const originalStdoutWrite = process.stdout.write; - const originalWriteFileSync = fs.writeFileSync; - - fs.writeFileSync = function (name, content) { - htmlName = name; - htmlContent = content; - }; - - process.stdout.write = function (data) { - output += data; - }; - - const suite = new Suite({ - reporter: htmlReport, - }); - - suite - .add('single with matcher', function () { - const pattern = /[123]/g - const replacements = { 1: 'a', 2: 'b', 3: 'c' } - const subject = '123123123123123123123123123123123123123123123123' - const r = subject.replace(pattern, m => replacements[m]) - assert.ok(r); - }) - .add('Multiple replaces', function () { - const subject = '123123123123123123123123123123123123123123123123' - const r = subject.replace(/1/g, 'a').replace(/2/g, 'b').replace(/3/g, 'c') - assert.ok(r); - }) - await suite.run(); - - fs.writeFileSync = originalWriteFileSync; - process.stdout.write = originalStdoutWrite; - }); - - it('should print that a HTML file has been generated', () => { - assert.ok(output.includes('HTML file has been generated')); - }); - - it('htmlName should be result.html', () => { - assert.strictEqual(htmlName, 'result.html'); - }); - - it('htmlContent should not be empty', () => { - assert.ok(htmlContent.length > 100); - }); - - it('htmlContent bench suite should be used as class name', () => { - assert.ok(htmlContent.includes('circle-Multiple-replaces')); - assert.ok(htmlContent.includes('circle-single-with-matcher')); - }); - - it('htmlContent should not contain replace tags {{}}', () => { - assert.ok(htmlContent.includes('{{') === false); - assert.ok(htmlContent.includes('}}') === false); - }); +describe("htmlReport should create a file", async (t) => { + let output = ""; + let htmlName = ""; + let htmlContent = ""; + + before(async () => { + const originalStdoutWrite = process.stdout.write; + const originalWriteFileSync = fs.writeFileSync; + + fs.writeFileSync = (name, content) => { + htmlName = name; + htmlContent = content; + }; + + process.stdout.write = (data) => { + output += data; + }; + + const suite = new Suite({ + reporter: htmlReport, + }); + + suite + .add("single with matcher", () => { + const pattern = /[123]/g; + const replacements = { 1: "a", 2: "b", 3: "c" }; + const subject = "123123123123123123123123123123123123123123123123"; + const r = subject.replace(pattern, (m) => replacements[m]); + assert.ok(r); + }) + .add("Multiple replaces", () => { + const subject = "123123123123123123123123123123123123123123123123"; + const r = subject + .replace(/1/g, "a") + .replace(/2/g, "b") + .replace(/3/g, "c"); + assert.ok(r); + }); + await suite.run(); + + fs.writeFileSync = originalWriteFileSync; + process.stdout.write = originalStdoutWrite; + }); + + it("should print that a HTML file has been generated", () => { + assert.ok(output.includes("HTML file has been generated")); + }); + + it("htmlName should be result.html", () => { + assert.strictEqual(htmlName, "result.html"); + }); + + it("htmlContent should not be empty", () => { + assert.ok(htmlContent.length > 100); + }); + + it("htmlContent bench suite should be used as class name", () => { + assert.ok(htmlContent.includes("circle-Multiple-replaces")); + assert.ok(htmlContent.includes("circle-single-with-matcher")); + }); + + it("htmlContent should not contain replace tags {{}}", () => { + assert.ok(htmlContent.includes("{{") === false); + assert.ok(htmlContent.includes("}}") === false); + }); }); -describe('jsonReport should produce valid JSON output', async () => { - let output = '' - - before(async () => { - const originalStdoutWrite = process.stdout.write - process.stdout.write = function (data) { - output += data - } - - // Create a new Suite with the JSON reporter - const suite = new Suite({ - reporter: jsonReport - }) - - suite - .add('single with matcher', function () { - const pattern = /[123]/g - const replacements = { 1: 'a', 2: 'b', 3: 'c' } - const subject = '123123123123123123123123123123123123123123123123' - const r = subject.replace(pattern, (m) => replacements[m]) - assert.ok(r) - }) - .add('Multiple replaces', function () { - const subject = '123123123123123123123123123123123123123123123123' - const r = subject.replace(/1/g, 'a').replace(/2/g, 'b').replace(/3/g, 'c') - assert.ok(r) - }) - - // Run the suite - await suite.run() - - // Restore stdout - process.stdout.write = originalStdoutWrite - }); - - it('should print valid JSON', () => { - // Verify if the output can be parsed as JSON - let data - try { - data = JSON.parse(output) - } catch (err) { - assert.fail(`Output is not valid JSON: ${err.message}`) - } - - assert.ok(Array.isArray(data), 'Output should be an array of results') - }); - - it('should contain the required benchmark fields', () => { - const data = JSON.parse(output) - - // We expect the two benchmarks we added: 'single with matcher' and 'Multiple replaces' - assert.strictEqual(data.length, 2, 'Should have results for 2 benchmarks') - - for (const entry of data) { - // Ensure each entry has expected keys - assert.ok(typeof entry.name === 'string', 'name should be a string') - assert.ok(typeof entry.opsSec === 'number', 'opsSec should be a number') - assert.ok(typeof entry.runsSampled === 'number', 'runsSampled should be a number') - assert.ok(typeof entry.min === 'string', 'min should be a string (formatted time)') - assert.ok(typeof entry.max === 'string', 'max should be a string (formatted time)') - assert.ok(Array.isArray(entry.plugins), 'plugins should be an array') - } - }); +describe("jsonReport should produce valid JSON output", async () => { + let output = ""; + + before(async () => { + const originalStdoutWrite = process.stdout.write; + process.stdout.write = (data) => { + output += data; + }; + + // Create a new Suite with the JSON reporter + const suite = new Suite({ + reporter: jsonReport, + }); + + suite + .add("single with matcher", () => { + const pattern = /[123]/g; + const replacements = { 1: "a", 2: "b", 3: "c" }; + const subject = "123123123123123123123123123123123123123123123123"; + const r = subject.replace(pattern, (m) => replacements[m]); + assert.ok(r); + }) + .add("Multiple replaces", () => { + const subject = "123123123123123123123123123123123123123123123123"; + const r = subject + .replace(/1/g, "a") + .replace(/2/g, "b") + .replace(/3/g, "c"); + assert.ok(r); + }); + + // Run the suite + await suite.run(); + + // Restore stdout + process.stdout.write = originalStdoutWrite; + }); + + it("should print valid JSON", () => { + // Verify if the output can be parsed as JSON + let data; + try { + data = JSON.parse(output); + } catch (err) { + assert.fail(`Output is not valid JSON: ${err.message}`); + } + + assert.ok(Array.isArray(data), "Output should be an array of results"); + }); + + it("should contain the required benchmark fields", () => { + const data = JSON.parse(output); + + // We expect the two benchmarks we added: 'single with matcher' and 'Multiple replaces' + assert.strictEqual(data.length, 2, "Should have results for 2 benchmarks"); + + for (const entry of data) { + // Ensure each entry has expected keys + assert.ok(typeof entry.name === "string", "name should be a string"); + assert.ok(typeof entry.opsSec === "number", "opsSec should be a number"); + assert.ok( + typeof entry.runsSampled === "number", + "runsSampled should be a number", + ); + assert.ok( + typeof entry.min === "string", + "min should be a string (formatted time)", + ); + assert.ok( + typeof entry.max === "string", + "max should be a string (formatted time)", + ); + assert.ok(Array.isArray(entry.plugins), "plugins should be an array"); + } + }); }); diff --git a/test/worker.js b/test/worker.js index 4d2e105..c4de713 100644 --- a/test/worker.js +++ b/test/worker.js @@ -1,35 +1,35 @@ -const workerThreads = require('node:worker_threads'); -const { describe, it, before, after, mock } = require('node:test'); -const assert = require('node:assert'); +const workerThreads = require("node:worker_threads"); +const { describe, it, before, after, mock } = require("node:test"); +const assert = require("node:assert"); function noop() {} -describe('Using worker_threads', () => { - before(async () => { - mock.method(workerThreads, 'Worker'); +describe("Using worker_threads", () => { + before(async () => { + mock.method(workerThreads, "Worker"); - const { Suite } = require('../lib/index'); + const { Suite } = require("../lib/index"); - const bench = new Suite({ - reporter: noop, - useWorkers: true - }); + const bench = new Suite({ + reporter: noop, + useWorkers: true, + }); - bench - .add('Import with node: prefix', () => { - return import('node:fs'); - }) - .add('Import without node: prefix', () => { - return import('fs'); - }); - await bench.run(); - }); + bench + .add("Import with node: prefix", () => { + return import("node:fs"); + }) + .add("Import without node: prefix", () => { + return import("node:fs"); + }); + await bench.run(); + }); - after(() => { - mock.restoreAll(); - }) + after(() => { + mock.restoreAll(); + }); - it('should create a new Worker 2 times', () => { - assert.strictEqual(workerThreads.Worker.mock.calls.length, 2); - }); + it("should create a new Worker 2 times", () => { + assert.strictEqual(workerThreads.Worker.mock.calls.length, 2); + }); });