Skip to content

Commit

Permalink
lib,test: add terminal chart reporter
Browse files Browse the repository at this point in the history
  • Loading branch information
RafaelGSS committed Oct 20, 2024
1 parent 962a3c9 commit c6bcf6f
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 38 deletions.
62 changes: 61 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:

Expand Down
21 changes: 21 additions & 0 deletions examples/terminal-chart/node.js
Original file line number Diff line number Diff line change
@@ -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();
6 changes: 4 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -58,7 +58,7 @@ class Suite {
}
this.#reporter = options.reporter;
} else {
this.#reporter = reportConsoleBench;
this.#reporter = textReport;
}

if (options?.plugins) {
Expand Down Expand Up @@ -129,4 +129,6 @@ module.exports = {
V8NeverOptimizePlugin,
V8GetOptimizationStatus,
V8OptimizeOnNextCallPlugin,
chartReport,
textReport,
};
39 changes: 4 additions & 35 deletions lib/report.js
Original file line number Diff line number Diff line change
@@ -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,
};
28 changes: 28 additions & 0 deletions lib/reporter/chart.js
Original file line number Diff line number Diff line change
@@ -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,
};
41 changes: 41 additions & 0 deletions lib/reporter/text.js
Original file line number Diff line number Diff line change
@@ -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,
};
40 changes: 40 additions & 0 deletions test/reporter.js
Original file line number Diff line number Diff line change
@@ -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'));
})
});

0 comments on commit c6bcf6f

Please sign in to comment.