diff --git a/README.md b/README.md index c4966c8..3848ad1 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,67 @@ class V8OptimizeOnNextCallPlugin { } ``` -## Using Custom Reporter +# Using Reporter + +This module exports two reporters that control how benchmark results are displayed: +a detailed `textReport` for statistical analysis, and a visual `chartReport` that +displays a bar graph in the terminal. + +## `textReport` (Default) + +The `textReport` is the default reporter, which provides detailed statistical information +about each benchmark result. It includes the number of operations per second, the number +of runs sampled, and percentile statistics (`p75`, `p99`). This format is ideal for analyzing +performance with precision, allowing you to compare the efficiency of different operations +at a more granular level. + +**Example Output**: + +``` +Using delete property x 7,736,869 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(127.65ns ... 129.97ns) p75=129.76ns p99=129.97ns +Using delete property (proto: null) x 23,849,066 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(41.24ns ... 42.62ns) p75=42.44ns p99=42.62ns +Using undefined assignment x 114,484,354 ops/sec (11 runs sampled) v8-never-optimize=true min..max=(8.72ns ... 8.78ns) p75=8.76ns p99=8.78ns +... +``` + +Here’s how you can explicitly pass it as a reporter: + +```cjs +const { Suite, textReport } = require('bench-node'); + +const suite = new Suite({ + reporter: textReport, // Optional, since this is the default +}); +``` + +### `chartReport` + +The `chartReport` reporter provides a graphical representation of benchmark +results in the form of a bar chart, making it easier to visualize the relative +performance of each benchmark. It scales the bars based on the highest operations +per second (ops/sec) value, and displays the results incrementally as they are collected. + +Example output: + +``` +Platform: darwin arm64 +CPU Cores: 8 vCPUs | 16.0GB Mem + +single with matcher | ██████████████████████████████ | 747215.75 ops/sec +multiple replaces | █████████████████████████----- | 630285.56 ops/sec +``` + +Usage: + +```cjs +const { Suite, chartReport } = require('bench-node'); + +const suite = new Suite({ + reporter: chartReport, +}); +``` + +### Custom Reporter Customize data reporting by providing a `reporter` function when creating the `Suite`: diff --git a/examples/terminal-chart/node.js b/examples/terminal-chart/node.js new file mode 100644 index 0000000..82c2d8d --- /dev/null +++ b/examples/terminal-chart/node.js @@ -0,0 +1,21 @@ +const { Suite, chartReport } = require('../../lib'); +const assert = require('node:assert'); + +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); + }) + .run(); diff --git a/lib/index.js b/lib/index.js index 2ed8e60..e4c3c99 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,4 +1,4 @@ -const { reportConsoleBench } = require('./report'); +const { textReport, chartReport } = require('./report'); const { getInitialIterations, runBenchmark, runWarmup } = require('./lifecycle'); const { debugBench, timer } = require('./clock'); const { @@ -58,7 +58,7 @@ class Suite { } this.#reporter = options.reporter; } else { - this.#reporter = reportConsoleBench; + this.#reporter = textReport; } if (options?.plugins) { @@ -129,4 +129,6 @@ module.exports = { V8NeverOptimizePlugin, V8GetOptimizationStatus, V8OptimizeOnNextCallPlugin, + chartReport, + textReport, }; diff --git a/lib/report.js b/lib/report.js index 4c60d55..f142250 100644 --- a/lib/report.js +++ b/lib/report.js @@ -1,38 +1,7 @@ -const { timer } = require('./clock'); - -const formatter = Intl.NumberFormat(undefined, { - notation: 'standard', - maximumFractionDigits: 2, -}); - -function reportConsoleBench(bench, result) { - const opsSecReported = result.opsSec < 100 ? - result.opsSec.toFixed(2) : - result.opsSec.toFixed(0); - - process.stdout.write(bench.name); - process.stdout.write(' x '); - process.stdout.write(`${ formatter.format(opsSecReported) } ops/sec`); - // TODO: produce confidence on stddev - // process.stdout.write(result.histogram.stddev.toString()); - process.stdout.write(` (${ result.histogram.samples.length } runs sampled) `); - - for (const p of bench.plugins) { - if (typeof p.getReport === 'function') - process.stdout.write(`${p.getReport()} `); - } - - process.stdout.write('min..max=('); - process.stdout.write(timer.format(result.histogram.min)); - process.stdout.write(' ... '); - process.stdout.write(timer.format(result.histogram.max)); - process.stdout.write(') p75='); - process.stdout.write(timer.format(result.histogram.percentile(75))); - process.stdout.write(' p99='); - process.stdout.write(timer.format(result.histogram.percentile(99))); - process.stdout.write('\n'); -} +const { textReport } = require('./reporter/text'); +const { chartReport } = require('./reporter/chart'); module.exports = { - reportConsoleBench, + chartReport, + textReport, }; diff --git a/lib/reporter/chart.js b/lib/reporter/chart.js new file mode 100644 index 0000000..0f72392 --- /dev/null +++ b/lib/reporter/chart.js @@ -0,0 +1,28 @@ +const { platform, arch, cpus, totalmem } = require('node:os'); + +function drawBar(label, value, total, length = 30) { + const percentage = value / total; + const filledLength = Math.round(length * percentage); + const bar = '█'.repeat(filledLength) + '-'.repeat(length - filledLength); + + process.stdout.write(`${label.padEnd(45)} | ${bar} | ${value.toFixed(2)} ops/sec\n`); +} + +const environment = { + 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)); + + process.stdout.write(`Platform: ${environment.platform}\n` + + `CPU Cores: ${environment.hardware}\n\n`); + results.forEach(result => { + drawBar(result.name, result.opsSec, maxOpsSec); + }); +} + +module.exports = { + chartReport, +}; diff --git a/lib/reporter/text.js b/lib/reporter/text.js new file mode 100644 index 0000000..cfa1361 --- /dev/null +++ b/lib/reporter/text.js @@ -0,0 +1,41 @@ +const { timer } = require('../clock'); + +const formatter = Intl.NumberFormat(undefined, { + 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); + process.stdout.write(' x '); + process.stdout.write(`${ formatter.format(opsSecReported) } ops/sec`); + // TODO: produce confidence on stddev + // process.stdout.write(result.histogram.stddev.toString()); + process.stdout.write(` (${ result.histogram.samples.length } runs sampled) `); + + for (const p of result.plugins) { + if (p.report) { + process.stdout.write(`${p.report} `); + } + } + + process.stdout.write('min..max=('); + process.stdout.write(timer.format(result.histogram.min)); + process.stdout.write(' ... '); + process.stdout.write(timer.format(result.histogram.max)); + process.stdout.write(') p75='); + process.stdout.write(timer.format(result.histogram.percentile(75))); + process.stdout.write(' p99='); + process.stdout.write(timer.format(result.histogram.percentile(99))); + process.stdout.write('\n'); + } +} + +module.exports = { + textReport, +}; diff --git a/test/reporter.js b/test/reporter.js new file mode 100644 index 0000000..ead331d --- /dev/null +++ b/test/reporter.js @@ -0,0 +1,40 @@ +const { describe, it, before } = require('node:test'); +const assert = require('node:assert'); +const { Suite, chartReport } = require('../lib'); + +describe('chartReport outputs benchmark results as a bar chart', async (t) => { + let output = ''; + + before(async () => { + 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(); + }); + + it('should include bar chart chars', () => { + assert.ok(output.includes('█')); + }); + + it('should include ops/sec', () => { + assert.ok(output.includes('ops/sec')); + }) +});