Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add JSON Reporter #32

Merged
merged 3 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,46 @@ const suite = new Suite({
});
```

### `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

Customize data reporting by providing a `reporter` function when creating the `Suite`:
Expand Down
File renamed without changes.
File renamed without changes.
21 changes: 21 additions & 0 deletions examples/json-report/node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const { Suite, jsonReport } = require('../../lib');
const assert = require('node:assert');

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();
File renamed without changes.
8 changes: 7 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ const { Worker } = require('node:worker_threads');
const { types } = require('node:util');
const path = require('node:path');

const { textReport, chartReport, htmlReport } = require('./report');
const {
textReport,
chartReport,
htmlReport,
jsonReport,
} = require('./report');
const { getInitialIterations, runBenchmark, runWarmup } = require('./lifecycle');
const { debugBench, timer, createFnString } = require('./clock');
const {
Expand Down Expand Up @@ -203,4 +208,5 @@ module.exports = {
chartReport,
textReport,
htmlReport,
jsonReport,
};
2 changes: 2 additions & 0 deletions lib/report.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
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,
};
28 changes: 28 additions & 0 deletions lib/reporter/json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict'

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)

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))
}

module.exports = {
jsonReport
}
72 changes: 71 additions & 1 deletion test/reporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ const { describe, it, before } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');

const { Suite, chartReport, htmlReport } = require('../lib');
const {
Suite,
chartReport,
htmlReport,
jsonReport,
} = require('../lib');

describe('chartReport outputs benchmark results as a bar chart', async (t) => {
let output = '';
Expand Down Expand Up @@ -107,3 +112,68 @@ describe('htmlReport should create a file', async (t) => {
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')
}
});
});
Loading