Skip to content

Commit

Permalink
feat: add html reporter
Browse files Browse the repository at this point in the history
This commit introduces a new `reporter` to bench-node htmlReporter,
which will create an HTML file with benchmark results animated by
a circle visualizer.
  • Loading branch information
RafaelGSS committed Dec 5, 2024
1 parent 08f4b16 commit 071f79d
Show file tree
Hide file tree
Showing 7 changed files with 362 additions and 2 deletions.
21 changes: 21 additions & 0 deletions examples/html/node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const { Suite, htmlReport } = require('../../lib');
const assert = require('node:assert');

const suite = new Suite({
reporter: htmlReport,
});

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();
115 changes: 115 additions & 0 deletions examples/html/result.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Benchmark Visualizer</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: #f4f4f4;
font-family: Arial, sans-serif;
}

.container {
position: relative;
width: 400px; /* Reduced width */
height: 200px;
border: 2px solid #000;
background: #fff;
}

.label {
position: absolute;
left: -120px; /* Place labels outside the box */
width: 100px;
text-align: right;
font-size: 14px;
}

.circle {
width: 40px;
height: 40px;
border-radius: 50%;
position: absolute;
}


#label-single-with-matcher {
top: 20px;
}

#circle-single-with-matcher {
background-color: red;
top: 20px;
}

#label-multiple-replaces {
top: 100px;
}

#circle-multiple-replaces {
background-color: blue;
top: 100px;
}

</style>
</head>
<body>
<div class="container">

<div id="circle-single-with-matcher" class="circle"></div>

<div id="circle-multiple-replaces" class="circle"></div>



<div id="label-single-with-matcher" class="label">single-with-matcher

(504,104.31 ops/sec)</div>

<div id="label-multiple-replaces" class="label">multiple-replaces

(358,487.94 ops/sec)</div>

</div>

<script>
const durations = [{"name":"single-with-matcher","duration":10,"opsSecFormatted":"504,104.31"},{"name":"multiple-replaces","duration":14.061959795114847,"opsSecFormatted":"358,487.94"}];

const animateCircles = () => {
const boxWidth = 400; // Width of the container box
const circles = durations.map((d) => ({
id: "circle-" + d.name.toLowerCase(),
duration: d.duration,
position: 0,
direction: 1,
}));

const update = () => {
circles.forEach(circle => {
const element = document.getElementById(circle.id);

circle.position += circle.direction * (boxWidth / circle.duration) * 0.5;

if (circle.position >= boxWidth - 20 || circle.position <= 0) {
circle.direction *= -1; // Reverse direction on collision
}

element.style.transform = `translateX(${circle.position}px)`;
});

requestAnimationFrame(update);
};

update();
};

animateCircles();
</script>
</body>
</html>
3 changes: 2 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { textReport, chartReport } = require('./report');
const { textReport, chartReport, htmlReport } = require('./report');
const { getInitialIterations, runBenchmark, runWarmup } = require('./lifecycle');
const { debugBench, timer } = require('./clock');
const {
Expand Down Expand Up @@ -131,4 +131,5 @@ module.exports = {
V8OptimizeOnNextCallPlugin,
chartReport,
textReport,
htmlReport,
};
2 changes: 2 additions & 0 deletions lib/report.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
const { textReport } = require('./reporter/text');
const { chartReport } = require('./reporter/chart');
const { htmlReport } = require('./reporter/html');

module.exports = {
chartReport,
textReport,
htmlReport,
};
72 changes: 72 additions & 0 deletions lib/reporter/html.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
const fs = require('node:fs');
const path = require('node:path');

const formatter = Intl.NumberFormat(undefined, {
notation: 'standard',
maximumFractionDigits: 2,
});

const opsToDuration = (maxOps, ops, scalingFactor = 10) => {
const baseSpeed = (maxOps / ops) * scalingFactor;
return Math.max(baseSpeed, 2); // Normalize speed with a minimum of 2 seconds
};

const generateHTML = (template, durations) => {
let css = ''
let circleDiv = ''
let labelDiv = ''
let position = 20;
const colors = ['red', 'blue', 'green', 'pink', 'grey', 'purple']
for (const d of durations) {
css += `
#label-${d.name} {
top: ${position}px;
}
#circle-${d.name} {
background-color: ${colors.shift()};
top: ${position}px;
}
`
circleDiv += `
<div id="label-${d.name}" class="label">${d.name}
(${d.opsSecFormatted} ops/sec)</div>
`
labelDiv += `
<div id="circle-${d.name}" class="circle"></div>
`

position += 80;
}

return template
.replaceAll('{{DURATIONS}}', JSON.stringify(durations))
.replaceAll('{{CSS}}', css)
.replaceAll('{{LABEL_DIV}}', labelDiv)
.replaceAll('{{CIRCLE_DIV}}', circleDiv)
.replaceAll('{{CONTAINER_HEIGHT}}', `${durations.length * 100}px;`);
};

const templatePath = path.join(__dirname, 'template.html');
const template = fs.readFileSync(templatePath, 'utf8');

function htmlReport(results) {
const maxOpsSec = Math.max(...results.map(b => b.opsSec));

const durations = results.map((r) => ({
name: r.name.replaceAll(' ', '-'),
duration: opsToDuration(maxOpsSec, r.opsSec),
opsSecFormatted: formatter.format(r.opsSec)
}));

const htmlContent = generateHTML(template, durations);
fs.writeFileSync('result.html', htmlContent, 'utf8');
process.stdout.write('HTML file has been generated: result.html');
}


module.exports = {
htmlReport,
}

85 changes: 85 additions & 0 deletions lib/reporter/template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Benchmark Visualizer</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: #f4f4f4;
font-family: Arial, sans-serif;
}

.container {
position: relative;
width: 400px; /* Reduced width */
height: {{CONTAINER_HEIGHT}}
border: 2px solid #000;
background: #fff;
}

.label {
position: absolute;
left: -120px; /* Place labels outside the box */
width: 100px;
text-align: right;
font-size: 14px;
}

.circle {
width: 40px;
height: 40px;
border-radius: 50%;
position: absolute;
}

{{CSS}}
</style>
</head>
<body>
<div class="container">
{{LABEL_DIV}}

{{CIRCLE_DIV}}
</div>

<script>
const durations = {{DURATIONS}};

const animateCircles = () => {
const boxWidth = 400; // Width of the container box
const circles = durations.map((d) => ({
id: "circle-" + d.name.toLowerCase(),
duration: d.duration,
position: 0,
direction: 1,
}));

const update = () => {
circles.forEach(circle => {
const element = document.getElementById(circle.id);

circle.position += circle.direction * (boxWidth / circle.duration) * 0.5;

if (circle.position >= boxWidth - 20 || circle.position <= 0) {
circle.direction *= -1; // Reverse direction on collision
}

element.style.transform = `translateX(${circle.position}px)`;
});

requestAnimationFrame(update);
};

update();
};

animateCircles();
</script>
</body>
</html>
Loading

0 comments on commit 071f79d

Please sign in to comment.