Skip to content

Commit c1416e2

Browse files
committed
feat: Deduplicate the data summarization code.
Multiple reporters are using substantially the same logic.
1 parent 4985b35 commit c1416e2

File tree

5 files changed

+143
-101
lines changed

5 files changed

+143
-101
lines changed

lib/reporter/csv.js

Lines changed: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,33 @@
1-
const { timer } = require("../clock");
1+
const { summarize } = require("../utils/analyze");
22

33
const formatter = Intl.NumberFormat(undefined, {
44
notation: "standard",
55
maximumFractionDigits: 2,
66
});
77

8-
// Helper function to format time with appropriate unit for CSV
9-
const formatTimeForCSV = (time) => {
10-
if (time < 0.000001) {
11-
return `${(time * 1000000000).toFixed(2)} ns`;
12-
} else if (time < 0.001) {
13-
return `${(time * 1000000).toFixed(2)} µs`;
14-
} else if (time < 1) {
15-
return `${(time * 1000).toFixed(2)} ms`;
16-
} else {
17-
return `${time.toFixed(2)} s`;
18-
}
19-
};
20-
218
function csvReport(results) {
229
const primaryMetric =
2310
results[0]?.opsSec !== undefined ? "opsSec" : "totalTime";
2411
const header = `name,${primaryMetric === "opsSec" ? "ops/sec" : "total time"},samples,plugins,min,max\n`;
2512
process.stdout.write(header);
2613

27-
for (const result of results) {
28-
process.stdout.write(`${result.name},`);
14+
const output = summarize(results);
15+
16+
for (const entry of output) {
17+
process.stdout.write(`${entry.name},`);
2918

3019
if (primaryMetric === "opsSec") {
31-
const opsSecReported =
32-
result.opsSec < 100
33-
? result.opsSec.toFixed(2)
34-
: result.opsSec.toFixed(0);
35-
process.stdout.write(`"${formatter.format(opsSecReported)}",`);
20+
process.stdout.write(`"${formatter.format(entry.opsSec)}",`);
3621
} else {
37-
// primaryMetric === 'totalTime'
38-
process.stdout.write(`"${formatTimeForCSV(result.totalTime)}",`);
22+
process.stdout.write(`${entry.totalTimeFormatted},`);
3923
}
4024

41-
process.stdout.write(`${result.histogram.samples},`);
42-
43-
process.stdout.write(
44-
`"${result.plugins
45-
.filter((p) => p.report)
46-
.map((p) => p.report)
47-
.join(",")}",`,
48-
);
25+
process.stdout.write(`${entry.runsSampled},`);
26+
process.stdout.write(`"${entry.plugins.join(",")}",`);
4927

5028
// For test compatibility, format min/max in microseconds
51-
const minInUs = result.histogram.min / 1000;
52-
const maxInUs = result.histogram.max / 1000;
29+
const minInUs = entry.min / 1000;
30+
const maxInUs = entry.max / 1000;
5331
process.stdout.write(`${minInUs.toFixed(2)}us,`);
5432
process.stdout.write(`${maxInUs.toFixed(2)}us\n`);
5533
}

lib/reporter/html.js

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const { platform, arch, availableParallelism, totalmem } = require("node:os");
22
const fs = require("node:fs");
33
const path = require("node:path");
4+
const { summarize } = require("../utils/analyze");
45

56
const formatter = Intl.NumberFormat(undefined, {
67
notation: "standard",
@@ -12,22 +13,6 @@ const timer = Intl.NumberFormat(undefined, {
1213
maximumFractionDigits: 3,
1314
});
1415

15-
const formatTime = (time) => {
16-
if (time < 0.000001) {
17-
// Less than 1 microsecond, show in nanoseconds
18-
return `${(time * 1000000000).toFixed(2)} ns`;
19-
} else if (time < 0.001) {
20-
// Less than 1 millisecond, show in microseconds
21-
return `${(time * 1000000).toFixed(2)} µs`;
22-
} else if (time < 1) {
23-
// Less than 1 second, show in milliseconds
24-
return `${(time * 1000).toFixed(2)} ms`;
25-
} else {
26-
// 1 second or more, show in seconds
27-
return `${time.toFixed(2)} s`;
28-
}
29-
};
30-
3116
const valueToDuration = (maxValue, value, isTimeBased, scalingFactor = 10) => {
3217
const normalizedValue = isTimeBased ? maxValue / value : value / maxValue;
3318
const baseSpeed = (1 / normalizedValue) * scalingFactor;
@@ -92,30 +77,31 @@ const templatePath = path.join(__dirname, "template.html");
9277
const template = fs.readFileSync(templatePath, "utf8");
9378

9479
function htmlReport(results) {
80+
const summary = summarize(results);
9581
const primaryMetric =
9682
results[0]?.opsSec !== undefined ? "opsSec" : "totalTime";
9783
let durations;
9884

9985
if (primaryMetric === "opsSec") {
100-
const maxOpsSec = Math.max(...results.map((b) => b.opsSec));
101-
durations = results.map((r) => ({
86+
const maxOpsSec = Math.max(...summary.map((b) => b.opsSec));
87+
durations = summary.map((r) => ({
10288
name: r.name.replaceAll(" ", "-"),
10389
duration: valueToDuration(maxOpsSec, r.opsSec, false),
10490
metricValueFormatted: formatter.format(r.opsSec),
10591
metricUnit: "ops/sec",
106-
minFormatted: timer.format(r.histogram.min), // Use timer for ns format
107-
maxFormatted: timer.format(r.histogram.max),
92+
minFormatted: timer.format(r.min), // Use timer for ns format
93+
maxFormatted: timer.format(r.max),
10894
}));
10995
} else {
11096
// metric === 'totalTime'
111-
const maxTotalTime = Math.max(...results.map((b) => b.totalTime));
112-
durations = results.map((r) => ({
97+
const maxTotalTime = Math.max(...summary.map((b) => b.totalTime));
98+
durations = summary.map((r) => ({
11399
name: r.name.replaceAll(" ", "-"),
114100
duration: valueToDuration(maxTotalTime, r.totalTime, true),
115-
metricValueFormatted: formatTime(r.totalTime),
101+
metricValueFormatted: r.totalTimeFormatted,
116102
metricUnit: "total time",
117-
minFormatted: timer.format(r.histogram.min), // Use timer for ns format
118-
maxFormatted: timer.format(r.histogram.max),
103+
minFormatted: timer.format(r.min), // Use timer for ns format
104+
maxFormatted: timer.format(r.max),
119105
}));
120106
}
121107

lib/reporter/json.js

Lines changed: 11 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,16 @@
1-
const { timer } = require("../clock");
2-
3-
// Helper function to format time in appropriate units
4-
const formatTime = (time) => {
5-
if (time < 0.000001) {
6-
// Less than 1 microsecond, show in nanoseconds
7-
return `${(time * 1000000000).toFixed(2)} ns`;
8-
} else if (time < 0.001) {
9-
// Less than 1 millisecond, show in microseconds
10-
return `${(time * 1000000).toFixed(2)} µs`;
11-
} else if (time < 1) {
12-
// Less than 1 second, show in milliseconds
13-
return `${(time * 1000).toFixed(2)} ms`;
14-
} else {
15-
// 1 second or more, show in seconds
16-
return `${time.toFixed(2)} s`;
17-
}
18-
};
1+
const { summarize } = require("../utils/analyze");
192

203
function jsonReport(results) {
21-
const output = results.map((result) => {
22-
const baseResult = {
23-
name: result.name,
24-
runsSampled: result.histogram.samples,
25-
min: timer.format(result.histogram.min),
26-
max: timer.format(result.histogram.max),
27-
// Report anything the plugins returned
28-
plugins: result.plugins.map((p) => p.report).filter(Boolean),
29-
};
30-
31-
if (result.opsSec !== undefined) {
32-
const opsSecReported =
33-
result.opsSec < 100
34-
? result.opsSec.toFixed(2)
35-
: result.opsSec.toFixed(0);
36-
baseResult.opsSec = Number(opsSecReported);
37-
} else if (result.totalTime !== undefined) {
38-
baseResult.totalTime = result.totalTime; // Total time in seconds
39-
baseResult.totalTimeFormatted = formatTime(result.totalTime);
40-
}
41-
42-
return baseResult;
43-
});
4+
const output = summarize(results).map((result) => ({
5+
name: result.name,
6+
runsSampled: result.runsSampled,
7+
min: result.minFormatted,
8+
max: result.maxFormatted,
9+
plugins: result.plugins.map((p) => p.report).filter(Boolean),
10+
opsSec: result.opsSec,
11+
totalTime: result.totalTime,
12+
totalTimeFormatted: result.totalTimeFormatted,
13+
}));
4414

4515
console.log(JSON.stringify(output, null, 2));
4616
}

lib/utils/analyze.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const { timer } = require("../clock");
2+
13
function analyze(results, sorted = true) {
24
const baselineResult = results.find((result) => result.baseline);
35

@@ -59,6 +61,57 @@ function analyze(results, sorted = true) {
5961
return output;
6062
}
6163

64+
const formatter = Intl.NumberFormat(undefined, {
65+
notation: "standard",
66+
maximumFractionDigits: 2,
67+
});
68+
69+
// Helper function to format time in appropriate units
70+
const formatTime = (time) => {
71+
if (time < 0.000001) {
72+
// Less than 1 microsecond, show in nanoseconds
73+
return `${(time * 1000000000).toFixed(2)} ns`;
74+
} else if (time < 0.001) {
75+
// Less than 1 millisecond, show in microseconds
76+
return `${(time * 1000000).toFixed(2)} µs`;
77+
} else if (time < 1) {
78+
// Less than 1 second, show in milliseconds
79+
return `${(time * 1000).toFixed(2)} ms`;
80+
} else {
81+
// 1 second or more, show in seconds
82+
return `${time.toFixed(2)} s`;
83+
}
84+
};
85+
86+
function summarize(results) {
87+
return results.map((result) => {
88+
const baseResult = {
89+
name: result.name,
90+
runsSampled: result.histogram.samples,
91+
min: result.histogram.min,
92+
max: result.histogram.max,
93+
minFormatted: timer.format(result.histogram.min), // Use timer for ns format
94+
maxFormatted: timer.format(result.histogram.max),
95+
// Report anything the plugins returned
96+
plugins: result.plugins.map((p) => p.report).filter(Boolean),
97+
};
98+
99+
if (result.opsSec !== undefined) {
100+
const opsSecReported =
101+
result.opsSec < 100
102+
? result.opsSec.toFixed(2)
103+
: result.opsSec.toFixed(0);
104+
baseResult.opsSec = Number(opsSecReported);
105+
} else if (result.totalTime !== undefined) {
106+
baseResult.totalTime = result.totalTime; // Total time in seconds
107+
baseResult.totalTimeFormatted = formatTime(result.totalTime);
108+
}
109+
110+
return baseResult;
111+
});
112+
}
113+
62114
module.exports = {
63115
analyze,
116+
summarize,
64117
};

test/reporter.js

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const {
1212
textReport,
1313
} = require("../lib");
1414

15-
const { analyze } = require("../lib/utils/analyze.js");
15+
const { analyze, summarize } = require("../lib/utils/analyze.js");
1616

1717
describe("chartReport outputs benchmark results as a bar chart", async (t) => {
1818
let output = "";
@@ -287,7 +287,7 @@ describe("prettyReport outputs a beautiful report", async (t) => {
287287
});
288288
});
289289

290-
describe("analyse", async (t) => {
290+
describe("analyze", async (t) => {
291291
let analysis;
292292

293293
before(async () => {
@@ -323,6 +323,61 @@ describe("analyse", async (t) => {
323323
});
324324
});
325325

326+
describe("summarize", async (t) => {
327+
let results;
328+
329+
before(async () => {
330+
// Create a new Suite with the pretty reporter
331+
const suite = new Suite({});
332+
333+
// Add benchmarks with one being the baseline
334+
suite
335+
.add("baseline-test", { baseline: true }, () => {
336+
// Medium-speed operation
337+
for (let i = 0; i < 10000; i++) {}
338+
})
339+
.add("other-test", () => {
340+
// Faster operation
341+
for (let i = 0; i < 1000; i++) {}
342+
})
343+
.add("faster-test", () => {
344+
// Slower operation
345+
for (let i = 0; i < 100; i++) {}
346+
});
347+
348+
// Run the suite
349+
results = await suite.run();
350+
});
351+
352+
it("should contain the required benchmark fields", () => {
353+
const data = summarize(results);
354+
355+
// We expect the two benchmarks we added: 'single with matcher' and 'Multiple replaces'
356+
assert.strictEqual(data.length, 3, "Should have results for 3 benchmarks");
357+
358+
for (const entry of data) {
359+
// Ensure each entry has expected keys
360+
assert.ok(typeof entry.name === "string", "name should be a string");
361+
assert.ok(typeof entry.opsSec === "number", "opsSec should be a number");
362+
assert.ok(
363+
typeof entry.runsSampled === "number",
364+
"runsSampled should be a number",
365+
);
366+
assert.ok(typeof entry.min === "number", "min should be a number");
367+
assert.ok(typeof entry.max === "number", "max should be a number");
368+
assert.ok(
369+
typeof entry.minFormatted === "string",
370+
"minFormatted should be a string (formatted time)",
371+
);
372+
assert.ok(
373+
typeof entry.maxFormatted === "string",
374+
"maxFormatted should be a string (formatted time)",
375+
);
376+
assert.ok(Array.isArray(entry.plugins), "plugins should be an array");
377+
}
378+
});
379+
});
380+
326381
describe("baseline comparisons", async (t) => {
327382
let results;
328383

0 commit comments

Comments
 (0)