bench-node
Advanced tools
Comparing version 0.4.1 to 0.5.0
@@ -9,2 +9,5 @@ const { Suite } = require('../../lib'); | ||
}) | ||
.add(`new Uint32Array(1024) with 10 repetitions`, {repeatSuite: 10}, function () { | ||
return new Uint32Array(1024); | ||
}) | ||
.add(`[Managed] new Uint32Array(1024)`, function (timer) { | ||
@@ -11,0 +14,0 @@ const assert = require('node:assert'); |
@@ -197,19 +197,18 @@ const { debug, types } = require('node:util'); | ||
function createRunner(bench, recommendedCount) { | ||
const isAsync = types.isAsyncFunction(bench.fn); | ||
const hasArg = bench.fn.length >= 1; | ||
function createFnString(bench) { | ||
const { isAsync, hasArg } = bench; | ||
if (bench.fn.length > 1) { | ||
process.emitWarning(`The benchmark "${ bench.name }" function should not have more than 1 argument.`); | ||
} | ||
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 }`); | ||
@@ -228,2 +227,3 @@ debugBench(`Created compiled benchmark, hasArg=${ hasArg }, isAsync=${ isAsync }, recommendedCount=${ recommendedCount }`); | ||
if (typeof p.onCompleteBenchmark === 'function') { | ||
// TODO: this won't work when useWorkers=true | ||
p.onCompleteBenchmark(result, bench); | ||
@@ -239,2 +239,3 @@ } | ||
clockBenchmark, | ||
createFnString, | ||
timer, | ||
@@ -241,0 +242,0 @@ MIN_RESOLUTION, |
103
lib/index.js
@@ -1,4 +0,13 @@ | ||
const { textReport, chartReport, htmlReport } = require('./report'); | ||
const { Worker } = require('node:worker_threads'); | ||
const { types } = require('node:util'); | ||
const path = require('node:path'); | ||
const { | ||
textReport, | ||
chartReport, | ||
htmlReport, | ||
jsonReport, | ||
} = require('./report'); | ||
const { getInitialIterations, runBenchmark, runWarmup } = require('./lifecycle'); | ||
const { debugBench, timer } = require('./clock'); | ||
const { debugBench, timer, createFnString } = require('./clock'); | ||
const { | ||
@@ -18,2 +27,7 @@ validatePlugins, | ||
const getFunctionBody = (string) => string.substring( | ||
string.indexOf("{") + 1, | ||
string.lastIndexOf("}") | ||
); | ||
class Benchmark { | ||
@@ -25,4 +39,5 @@ name = 'Benchmark'; | ||
plugins; | ||
repeatSuite; | ||
constructor(name, fn, minTime, maxTime, plugins) { | ||
constructor(name, fn, minTime, maxTime, plugins, repeatSuite) { | ||
this.name = name; | ||
@@ -33,3 +48,20 @@ this.fn = fn; | ||
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()); | ||
} | ||
} | ||
@@ -42,2 +74,4 @@ | ||
maxTime: 0.5, | ||
// Number of times the benchmark will be repeated | ||
repeatSuite: 1, | ||
}; | ||
@@ -55,2 +89,3 @@ | ||
#plugins; | ||
#useWorkers; | ||
@@ -69,2 +104,3 @@ constructor(options = {}) { | ||
this.#useWorkers = options.useWorkers || false; | ||
if (options?.plugins) { | ||
@@ -90,2 +126,3 @@ validateArray(options.plugins, 'plugin'); | ||
validateNumber(options.maxTime, 'options.maxTime', options.minTime); | ||
validateNumber(options.repeatSuite, 'options.repeatSuite', options.repeatSuite); | ||
} | ||
@@ -100,2 +137,3 @@ validateFunction(fn, 'fn'); | ||
this.#plugins, | ||
options.repeatSuite, | ||
); | ||
@@ -110,8 +148,12 @@ this.#benchmarks.push(benchmark); | ||
// 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 }); | ||
// 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 }); | ||
} | ||
} | ||
@@ -122,5 +164,11 @@ | ||
// Warmup is calculated to reduce noise/bias on the results | ||
const initialIteration = await getInitialIterations(benchmark); | ||
debugBench(`Starting ${ benchmark.name } with minTime=${ benchmark.minTime }, maxTime=${ benchmark.maxTime }`); | ||
const result = await runBenchmark(benchmark, initialIteration); | ||
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; | ||
@@ -132,5 +180,33 @@ } | ||
} | ||
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}`)); | ||
} | ||
}); | ||
}); | ||
} | ||
} | ||
@@ -146,2 +222,3 @@ | ||
htmlReport, | ||
jsonReport, | ||
}; |
@@ -67,11 +67,5 @@ const { clockBenchmark, debugBench, MIN_RESOLUTION, timer } = require('./clock'); | ||
async function runBenchmark(bench, initialIterations) { | ||
const histogram = new StatisticalHistogram(); | ||
const maxDuration = bench.maxTime * timer.scale; | ||
const minSamples = 10; | ||
async function runBenchmarkOnce(bench, histogram, { initialIterations, maxDuration, minSamples }) { | ||
let iterations = 0; | ||
let timeSpent = 0; | ||
while (timeSpent < maxDuration || histogram.samples.length <= minSamples) { | ||
@@ -92,10 +86,37 @@ const { 0: duration, 1: realIterations } = await clockBenchmark(bench, initialIterations); | ||
} | ||
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 = iterations / (timeSpent / timer.scale); | ||
const opsSec = totalIterations / (totalTimeSpent / timer.scale); | ||
return { | ||
opsSec, | ||
iterations, | ||
histogram, | ||
iterations: totalIterations, | ||
// StatisticalHistogram is not a serializable object | ||
histogram: { | ||
samples: histogram.samples.length, | ||
min: histogram.min, | ||
max: histogram.max, | ||
}, | ||
name: bench.name, | ||
@@ -102,0 +123,0 @@ plugins: parsePluginsResult(bench.plugins, bench.name), |
const { textReport } = require('./reporter/text'); | ||
const { chartReport } = require('./reporter/chart'); | ||
const { htmlReport } = require('./reporter/html'); | ||
const { jsonReport } = require('./reporter/json'); | ||
@@ -9,2 +10,3 @@ module.exports = { | ||
htmlReport, | ||
jsonReport, | ||
}; |
@@ -26,3 +26,3 @@ const util = require('node:util'); | ||
// process.stdout.write(result.histogram.stddev.toString()); | ||
process.stdout.write(` (${ result.histogram.samples.length } runs sampled) `); | ||
process.stdout.write(` (${ result.histogram.samples } runs sampled) `); | ||
@@ -29,0 +29,0 @@ for (const p of result.plugins) { |
{ | ||
"name": "bench-node", | ||
"version": "0.4.1", | ||
"version": "0.5.0", | ||
"description": "", | ||
@@ -28,3 +28,6 @@ "main": "lib/index.js", | ||
}, | ||
"homepage": "https://github.com/RafaelGSS/bench-node#readme" | ||
"homepage": "https://github.com/RafaelGSS/bench-node#readme", | ||
"dependencies": { | ||
"piscina": "^4.8.0" | ||
} | ||
} |
@@ -96,3 +96,4 @@ # `bench-node` | ||
* `maxTime` {number} Maximum duration for the benchmark to run. **Default:** `0.5` seconds. | ||
* `fn` {Function|AsyncFunction} The benchmark function. Can be synchronous or asynchronous. | ||
* `repeatSuite` {number} Number of times to repeat benchmark to run. **Default:** `1` times. | ||
* `fn` {Function|AsyncFunction} The benchmark function. Can be synchronous or asynchronous. | ||
* Returns: {Suite} | ||
@@ -240,2 +241,42 @@ | ||
### `jsonReport` | ||
The `jsonReport` plugin provides benchmark results in **JSON format**. | ||
It includes key performance metrics—such as `opsSec`, `runsSampled`, `min` | ||
and `max` times, and any reporter data from your **plugins**—so you can easily | ||
store, parse, or share the information. | ||
Example output: | ||
```json | ||
[ | ||
{ | ||
"name": "single with matcher", | ||
"opsSec": 180000, | ||
"runsSampled": 50, | ||
"min": "13.20μs", | ||
"max": "82.57μs", | ||
"plugins": [] | ||
}, | ||
{ | ||
"name": "Multiple replaces", | ||
"opsSec": 170000, | ||
"runsSampled": 50, | ||
"min": "15.31μs", | ||
"max": "77.49μs", | ||
"plugins": [] | ||
} | ||
] | ||
``` | ||
**Usage:** | ||
```cjs | ||
const { Suite, jsonReport } = require('bench-node'); | ||
const suite = new Suite({ | ||
reporter: jsonReport, | ||
}); | ||
``` | ||
### Custom Reporter | ||
@@ -379,1 +420,15 @@ | ||
> results between versions of V8/Node.js. | ||
### Worker Threads | ||
> Stability: 1.0 (Experimental) | ||
`bench-node` provides experimental support for **Worker Threads**. When you set `useWorkers: true`, | ||
the library runs each benchmark in a separate worker thread, ensuring that one benchmark | ||
does not affect another. Usage is straightforward: | ||
```cjs | ||
const suite = new Suite({ | ||
useWorkers: true, | ||
}); | ||
``` |
@@ -120,2 +120,12 @@ const { Suite } = require('../lib/index'); | ||
}); | ||
it('repeatSuite should be a valid number', () => { | ||
['ds', {}, () => {}].forEach((r) => { | ||
assert.throws(() => { | ||
bench.add('name', { repeatSuite: r }, noop); | ||
}, { | ||
code: 'ERR_INVALID_ARG_TYPE', | ||
}); | ||
}); | ||
}); | ||
}); | ||
@@ -122,0 +132,0 @@ }); |
const { describe, it, before } = require('node:test'); | ||
const assert = require('node:assert'); | ||
const { Suite } = require('../lib'); | ||
const copyBench = require('./fixtures/copy'); | ||
@@ -72,1 +73,24 @@ const { managedBench, managedOptBench } = require('./fixtures/opt-managed'); | ||
}); | ||
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(); | ||
}); | ||
it('should have a similar result as they will not share import.meta.cache', () => { | ||
assertMaxBenchmarkDifference(results, { percentageLimit: 10, ciPercentageLimit: 30 }); | ||
}); | ||
}); |
@@ -70,3 +70,3 @@ // @ts-check | ||
"isSupported()", | ||
"onCompleteBenchmark([number, number, object], {fn, maxTime, minTime, name, plugins})", | ||
"onCompleteBenchmark([number, number, object], {fn, fnStr, hasArg, isAsync, maxTime, minTime, name, plugins, repeatSuite})", | ||
"toJSON(string)", | ||
@@ -73,0 +73,0 @@ "toString()", |
@@ -5,3 +5,8 @@ const { describe, it, before } = require('node:test'); | ||
const { Suite, chartReport, htmlReport } = require('../lib'); | ||
const { | ||
Suite, | ||
chartReport, | ||
htmlReport, | ||
jsonReport, | ||
} = require('../lib'); | ||
@@ -111,1 +116,66 @@ describe('chartReport outputs benchmark results as a bar chart', async (t) => { | ||
}); | ||
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') | ||
} | ||
}); | ||
}); |
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
122442
68
2299
432
1
1
+ Addedpiscina@^4.8.0
+ Added@napi-rs/nice@1.0.1(transitive)
+ Added@napi-rs/nice-android-arm-eabi@1.0.1(transitive)
+ Added@napi-rs/nice-android-arm64@1.0.1(transitive)
+ Added@napi-rs/nice-darwin-arm64@1.0.1(transitive)
+ Added@napi-rs/nice-darwin-x64@1.0.1(transitive)
+ Added@napi-rs/nice-freebsd-x64@1.0.1(transitive)
+ Added@napi-rs/nice-linux-arm-gnueabihf@1.0.1(transitive)
+ Added@napi-rs/nice-linux-arm64-gnu@1.0.1(transitive)
+ Added@napi-rs/nice-linux-arm64-musl@1.0.1(transitive)
+ Added@napi-rs/nice-linux-ppc64-gnu@1.0.1(transitive)
+ Added@napi-rs/nice-linux-riscv64-gnu@1.0.1(transitive)
+ Added@napi-rs/nice-linux-s390x-gnu@1.0.1(transitive)
+ Added@napi-rs/nice-linux-x64-gnu@1.0.1(transitive)
+ Added@napi-rs/nice-linux-x64-musl@1.0.1(transitive)
+ Added@napi-rs/nice-win32-arm64-msvc@1.0.1(transitive)
+ Added@napi-rs/nice-win32-ia32-msvc@1.0.1(transitive)
+ Added@napi-rs/nice-win32-x64-msvc@1.0.1(transitive)
+ Addedpiscina@4.8.0(transitive)