Also bench hyperbuild Rust; improve graphs; remove default values for html-minifier config

This commit is contained in:
Wilson Lin 2020-01-09 22:40:04 +11:00
parent 1cb9bf9817
commit 68c43ab486
16 changed files with 307 additions and 153 deletions

2
bench/.gitignore vendored
View File

@ -1,2 +1,4 @@
node_modules/
min/
hyperbuild-bench/Cargo.lock
hyperbuild-bench/target/

View File

@ -1,65 +1,39 @@
"use strict";
const benchmark = require('benchmark');
const chartjs = require('chartjs-node');
const childProcess = require('child_process');
const fs = require('fs');
const path = require('path');
const programs = require('./minifiers');
const tests = require('./tests');
const colours = [
{
backgroundColor: '#9ad0f5',
borderColor: '#47aaec',
},
{
backgroundColor: '#ffb0c1',
borderColor: '#ff87a1',
},
{
backgroundColor: '#a4dfdf',
borderColor: '#4bc0c0',
},
];
const chartOptions = (title, displayLegend, yTick = t => t) => ({
options: {
title: {
display: true,
text: title,
},
scales: {
xAxes: [{
barPercentage: 0.5,
gridLines: {
color: '#ccc',
},
ticks: {
fontColor: '#222',
},
}],
yAxes: [{
gridLines: {
color: '#666',
},
ticks: {
callback: yTick,
fontColor: '#222',
},
}],
},
legend: {
display: displayLegend,
labels: {
fontFamily: 'Ubuntu, sans-serif',
fontColor: '#000',
},
},
},
});
const renderChart = async (file, cfg) => {
const chart = new chartjs(435, 320);
await chart.drawChart(cfg);
await chart.writeImageToFile('image/png', path.join(__dirname, `${file}.png`));
const cmd = (command, ...args) => {
const throwErr = msg => {
throw new Error(`${msg}\n ${command} ${args.join(' ')}`);
};
const {status, signal, error, stdout, stderr} = childProcess.spawnSync(command, args.map(String), {
stdio: ['ignore', 'pipe', 'pipe'],
encoding: 'utf8',
});
if (error) {
throwErr(error.message);
}
if (signal) {
throwErr(`Command exited with signal ${signal}`);
}
if (status !== 0) {
throwErr(`Command exited with status ${status}`);
}
if (stderr) {
throwErr(`stderr: ${stderr}`);
}
return stdout;
};
const fromEntries = entries => {
if (Object.fromEntries) return Object.fromEntries(entries);
const obj = {};
for (const [prop, val] of entries) obj[prop] = val;
return obj;
};
const sizes = {};
@ -91,52 +65,37 @@ for (const t of tests) {
}
}
}
fs.writeFileSync(path.join(__dirname, 'minification.json'), JSON.stringify(sizes, null, 2));
const suite = new benchmark.Suite();
for (const p of Object.keys(programs)) {
suite.add(p, () => {
for (const t of tests) {
programs[p](t.content);
}
});
}
suite
.on('cycle', event => {
console.info(event.target.toString());
})
.on('complete', async function () {
const speedResults = this.map(b => ({
name: b.name,
ops: b.hz,
}));
fs.writeFileSync(path.join(__dirname, "speed.json"), JSON.stringify(speedResults, null, 2));
await renderChart('speed', {
type: 'bar',
data: {
labels: speedResults.map(r => r.name),
datasets: [{
...colours[0],
data: speedResults.map(r => r.ops),
}],
},
...chartOptions('Operations per second (higher is better)', false),
const runTest = test => new Promise((resolve, reject) => {
// Run JS libraries.
const suite = new benchmark.Suite();
for (const p of Object.keys(programs)) {
suite.add(p, () => {
programs[p](test.content);
});
}
suite
.on('cycle', event => console.info(test.name, event.target.toString()))
.on('complete', () => resolve(fromEntries(suite.map(b => [b.name, b.hz]))))
.on('error', reject)
.run({'async': true});
});
const testNames = Object.keys(sizes);
const programNames = Object.keys(programs);
fs.writeFileSync(path.join(__dirname, "minification.json"), JSON.stringify(sizes, null, 2));
await renderChart('minification', {
type: 'bar',
scaleFontColor: 'red',
data: {
labels: testNames,
datasets: programNames.map((program, i) => ({
label: program,
...colours[i],
data: testNames.map(test => sizes[test][program].relative * 100),
})),
},
...chartOptions('Relative minified HTML file size (lower is better)', true, tick => `${tick}%`),
});
})
.run({'async': true});
(async () => {
const results = {};
// Run Rust library.
for (const [testName, testOps] of JSON.parse(cmd(
path.join(__dirname, 'hyperbuild-bench', 'target', 'release', 'hyperbuild-bench'),
'--iterations', 100,
'--tests', path.join(__dirname, 'tests'),
))) {
results[testName] = {hyperbuild: testOps};
}
for (const t of tests) {
Object.assign(results[t.name], await runTest(t));
}
fs.writeFileSync(path.join(__dirname, 'speed.json'), JSON.stringify(results, null, 2));
})();

View File

@ -9,6 +9,7 @@ const tests = {
"Bootstrap": "https://getbootstrap.com/docs/3.4/css/",
"Bing": "https://www.bing.com/",
"Coding Horror": "https://blog.codinghorror.com/",
"ECMA-262": "https://www.ecma-international.org/ecma-262/10.0/index.html",
"Google": "https://www.google.com/",
"Hacker News": "https://news.ycombinator.com/",
"NY Times": "https://www.nytimes.com/",

86
bench/graph.js Normal file
View File

@ -0,0 +1,86 @@
const chartjs = require('chartjs-node');
const fs = require('fs');
const path = require('path');
const tests = require('./tests');
const colours = {
'hyperbuild': '#041f60',
'hyperbuild-nodejs': '#0476d0',
'minimize': '#3cacae',
'html-minifier': '#5d6c89',
};
const chartOptions = (title, displayLegend, yTick = t => t) => ({
options: {
title: {
display: true,
text: title,
},
scales: {
xAxes: [{
barPercentage: 0.75,
gridLines: {
color: '#ccc',
},
ticks: {
fontColor: '#222',
},
}],
yAxes: [{
gridLines: {
color: '#666',
},
ticks: {
callback: yTick,
fontColor: '#222',
},
}],
},
legend: {
display: displayLegend,
labels: {
fontFamily: 'Ubuntu, sans-serif',
fontColor: '#000',
},
},
},
});
const renderChart = async (file, cfg) => {
const chart = new chartjs(435, 320);
await chart.drawChart(cfg);
await chart.writeImageToFile('image/png', path.join(__dirname, `${file}.png`));
};
(async () => {
const testNames = tests.map(t => t.name).sort();
const speedResults = JSON.parse(fs.readFileSync(path.join(__dirname, 'speed.json'), 'utf8'));
await renderChart('speed', {
type: 'bar',
data: {
labels: testNames,
datasets: ['hyperbuild', 'hyperbuild-nodejs', 'minimize', 'html-minifier'].map(program => ({
label: program,
backgroundColor: colours[program],
data: testNames.map(test => speedResults[test][program] / speedResults[test]['hyperbuild'] * 100),
})),
},
...chartOptions('Relative operations per second (higher is better)', true, tick => `${tick}%`),
});
const sizes = JSON.parse(fs.readFileSync(path.join(__dirname, 'minification.json'), 'utf8'));
await renderChart('minification', {
type: 'bar',
scaleFontColor: 'red',
data: {
labels: testNames,
datasets: ['hyperbuild-nodejs', 'html-minifier', 'minimize'].map(program => ({
label: program,
backgroundColor: colours[program],
data: testNames.map(test => sizes[test][program].relative * 100),
})),
},
...chartOptions('Relative minified HTML file size (lower is better)', true, tick => `${tick}%`),
});
})();

View File

@ -0,0 +1,12 @@
[package]
name = "hyperbuild-bench"
publish = false
version = "0.0.1"
authors = ["Wilson Lin <code@wilsonl.in>"]
edition = "2018"
[dependencies]
hyperbuild = { path = "../.." }
structopt = "0.3.5"
serde = { version = "1.0.104", features = ["derive"] }
serde_json = "1.0.44"

View File

@ -0,0 +1,34 @@
use hyperbuild::hyperbuild;
use std::fs;
use std::io::{stdout};
use std::time::Instant;
use structopt::StructOpt;
#[derive(StructOpt)]
struct Args {
#[structopt(long, parse(from_os_str))]
tests: std::path::PathBuf,
#[structopt(long)]
iterations: usize,
}
fn main() {
let args = Args::from_args();
let tests = fs::read_dir(args.tests).unwrap().map(|d| d.unwrap());
let mut results: Vec<(String, f64)> = Vec::new();
for t in tests {
let source = fs::read(t.path()).unwrap();
let start = Instant::now();
for _ in 0..args.iterations {
let mut data = source.to_vec();
hyperbuild(&mut data).unwrap();
};
let elapsed = start.elapsed().as_secs_f64();
let ops = args.iterations as f64 / elapsed;
results.push((t.file_name().to_str().unwrap().to_string(), ops));
};
serde_json::to_writer(stdout(), &results).unwrap();
}

9
bench/install.sh Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -e
rm -rf node_modules
HYPERBUILD_NODEJS_SKIP_BIN_DOWNLOAD=1 npm i
pushd hyperbuild-bench
cargo build --release
popd

View File

@ -9,8 +9,8 @@
"relative": 0.6330862904281787
},
"html-minifier": {
"absolute": 488839,
"relative": 0.8534572912574746
"absolute": 488822,
"relative": 0.8534276111911309
},
"minimize": {
"absolute": 495105,
@ -45,8 +45,8 @@
"relative": 0.6222239353466829
},
"html-minifier": {
"absolute": 137061,
"relative": 0.8805095688708154
"absolute": 137026,
"relative": 0.8802847212853573
},
"minimize": {
"absolute": 137358,
@ -63,8 +63,8 @@
"relative": 0.7342292960872684
},
"html-minifier": {
"absolute": 270621,
"relative": 0.7325266961711803
"absolute": 270604,
"relative": 0.732480679957232
},
"minimize": {
"absolute": 279252,
@ -99,8 +99,8 @@
"relative": 0.5906241489629755
},
"html-minifier": {
"absolute": 383586,
"relative": 0.9762867679809012
"absolute": 383578,
"relative": 0.9762664067212518
},
"minimize": {
"absolute": 384579,
@ -117,8 +117,8 @@
"relative": 0.5157900380676639
},
"html-minifier": {
"absolute": 28464,
"relative": 0.5087126695619538
"absolute": 28448,
"relative": 0.5084267152788948
},
"minimize": {
"absolute": 30643,
@ -135,8 +135,8 @@
"relative": 0.6978107321127253
},
"html-minifier": {
"absolute": 1888047,
"relative": 0.9521424184017114
"absolute": 1887947,
"relative": 0.9520919883849586
},
"minimize": {
"absolute": 1888704,
@ -153,8 +153,8 @@
"relative": 0.5387307214917895
},
"html-minifier": {
"absolute": 1115700,
"relative": 0.7231445803045672
"absolute": 1115617,
"relative": 0.7230907835848708
},
"minimize": {
"absolute": 1117526,
@ -171,8 +171,8 @@
"relative": 0.5613076908178878
},
"html-minifier": {
"absolute": 89329,
"relative": 0.5766919089212971
"absolute": 89321,
"relative": 0.5766402623645085
},
"minimize": {
"absolute": 91363,
@ -189,8 +189,8 @@
"relative": 0.9003600363028295
},
"html-minifier": {
"absolute": 273191,
"relative": 0.9082057027356775
"absolute": 273174,
"relative": 0.9081491873418815
},
"minimize": {
"absolute": 273887,
@ -207,8 +207,8 @@
"relative": 0.5520822745589214
},
"html-minifier": {
"absolute": 1307604,
"relative": 0.5359190926945385
"absolute": 1307563,
"relative": 0.5359022889200009
},
"minimize": {
"absolute": 1368203,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -3,39 +3,24 @@ const hyperbuild = require("hyperbuild");
const minimize = require("minimize");
module.exports = {
'hyperbuild-nodejs': content => hyperbuild.minify(Buffer.from(content)),
'hyperbuild-nodejs': content => hyperbuild.minify(content),
'html-minifier': content => htmlMinifier.minify(content, {
caseSensitive: false,
collapseBooleanAttributes: true,
collapseInlineTagWhitespace: true,
collapseWhitespace: true,
conservativeWhitespace: false,
customEventAttributes: [],
decodeEntities: true,
html5: true,
ignoreCustomComments: [],
ignoreCustomFragments: [/<\?[\s\S]*?\?>/],
includeAutoGeneratedTags: true,
keepClosingSlash: false,
minifyCSS: false,
minifyJS: false,
minifyURLs: false,
preserveLineBreaks: false,
preventAttributesEscaping: false,
processConditionalComments: true,
processScripts: [],
removeAttributeQuotes: true,
removeComments: true,
removeEmptyAttributes: false,
removeEmptyElements: false,
removeEmptyAttributes: true,
removeOptionalTags: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
removeTagWhitespace: true,
sortAttributes: false,
sortClassName: false,
trimCustomFragments: false,
useShortDoctype: true,
}),
'minimize': content => new minimize().parse(content),

View File

@ -12,6 +12,9 @@
"request": "^2.88.0",
"request-promise-native": "^1.0.8"
},
"engines": {
"node": "10.x"
},
"scripts": {
"start": "node bench.js"
}

View File

@ -1,14 +1,74 @@
[
{
"name": "hyperbuild-nodejs",
"ops": 10.158435796714862
{
"Amazon.html": {
"hyperbuild": 245.46705260338564,
"hyperbuild-nodejs": 145.21435374237635,
"html-minifier": 16.19830761009811,
"minimize": 95.71966364576267
},
{
"name": "html-minifier",
"ops": 0.9598865156998558
"BBC.html": {
"hyperbuild": 429.2291873495222,
"hyperbuild-nodejs": 251.3721939160052,
"html-minifier": 18.333446052847226,
"minimize": 108.38902861455512
},
{
"name": "minimize",
"ops": 3.6888006698975837
"Bootstrap.html": {
"hyperbuild": 235.08368235051825,
"hyperbuild-nodejs": 156.19542771462898,
"html-minifier": 8.557266916672539,
"minimize": 22.359774537863895
},
"Bing.html": {
"hyperbuild": 1008.1262435363229,
"hyperbuild-nodejs": 585.3489088472239,
"html-minifier": 79.35385186294975,
"minimize": 435.31581246812584
},
"Coding Horror.html": {
"hyperbuild": 1146.867798530376,
"hyperbuild-nodejs": 680.2295027510518,
"html-minifier": 45.63362214760677,
"minimize": 164.51899348138494
},
"Google.html": {
"hyperbuild": 344.0346646025321,
"hyperbuild-nodejs": 317.3708534283478,
"html-minifier": 29.36827883130167,
"minimize": 365.1698468973524
},
"Hacker News.html": {
"hyperbuild": 1804.5683188361834,
"hyperbuild-nodejs": 1259.6432378637871,
"html-minifier": 66.43984413610241,
"minimize": 255.30928557346104
},
"NY Times.html": {
"hyperbuild": 123.84742876588177,
"hyperbuild-nodejs": 51.83081525871115,
"html-minifier": 7.334756953956464,
"minimize": 59.400301132747934
},
"Reddit.html": {
"hyperbuild": 109.45057921629598,
"hyperbuild-nodejs": 66.80243904185947,
"html-minifier": 6.3323721760167695,
"minimize": 44.528247219895
},
"Stack Overflow.html": {
"hyperbuild": 763.6540095978328,
"hyperbuild-nodejs": 496.21357271825997,
"html-minifier": 39.39722290667494,
"minimize": 148.07292819104936
},
"Twitter.html": {
"hyperbuild": 376.9341764747767,
"hyperbuild-nodejs": 208.2611701306221,
"html-minifier": 42.264558908660206,
"minimize": 136.3651156178245
},
"Wikipedia.html": {
"hyperbuild": 52.02792034641937,
"hyperbuild-nodejs": 32.045431164840046,
"html-minifier": 2.35238631274572,
"minimize": 7.878943786969402
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -1,8 +1,8 @@
const fs = require('fs');
const path = require('path');
const testsDir = path.join(__dirname, "tests");
const testsDir = path.join(__dirname, 'tests');
module.exports = fs.readdirSync(testsDir).map(name => ({
name,
content: fs.readFileSync(path.join(testsDir, name), "utf8"),
}));
content: fs.readFileSync(path.join(testsDir, name), 'utf8'),
})).sort((a, b) => a.name.localeCompare(b.name));

View File

@ -1,5 +1,6 @@
[package]
name = "hyperbuild-fuzz-target"
publish = false
version = "0.0.1"
authors = ["Wilson Lin <code@wilsonl.in>"]
edition = "2018"

View File

@ -1,3 +1,5 @@
// Implement debug to allow .unwrap().
#[derive(Debug)]
pub enum ErrorType {
EntityFollowingMalformedEntity,
NoSpaceBeforeAttr,