diff --git a/bench/.gitignore b/bench/.gitignore index e9dc357..bafe521 100644 --- a/bench/.gitignore +++ b/bench/.gitignore @@ -1,2 +1,4 @@ node_modules/ min/ +hyperbuild-bench/Cargo.lock +hyperbuild-bench/target/ diff --git a/bench/bench.js b/bench/bench.js index a59d7e9..beb0c3a 100644 --- a/bench/bench.js +++ b/bench/bench.js @@ -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)); +})(); diff --git a/bench/fetch.js b/bench/fetch.js index e7f711a..0bb1147 100644 --- a/bench/fetch.js +++ b/bench/fetch.js @@ -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/", diff --git a/bench/graph.js b/bench/graph.js new file mode 100644 index 0000000..977b2e2 --- /dev/null +++ b/bench/graph.js @@ -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}%`), + }); +})(); diff --git a/bench/hyperbuild-bench/Cargo.toml b/bench/hyperbuild-bench/Cargo.toml new file mode 100644 index 0000000..76b466d --- /dev/null +++ b/bench/hyperbuild-bench/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "hyperbuild-bench" +publish = false +version = "0.0.1" +authors = ["Wilson Lin "] +edition = "2018" + +[dependencies] +hyperbuild = { path = "../.." } +structopt = "0.3.5" +serde = { version = "1.0.104", features = ["derive"] } +serde_json = "1.0.44" diff --git a/bench/hyperbuild-bench/src/main.rs b/bench/hyperbuild-bench/src/main.rs new file mode 100644 index 0000000..a81ba11 --- /dev/null +++ b/bench/hyperbuild-bench/src/main.rs @@ -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(); +} diff --git a/bench/install.sh b/bench/install.sh new file mode 100755 index 0000000..08f8a54 --- /dev/null +++ b/bench/install.sh @@ -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 diff --git a/bench/minification.json b/bench/minification.json index ce1b201..67cebe6 100644 --- a/bench/minification.json +++ b/bench/minification.json @@ -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, diff --git a/bench/minification.png b/bench/minification.png index 9a7223d..5f92585 100644 Binary files a/bench/minification.png and b/bench/minification.png differ diff --git a/bench/minifiers.js b/bench/minifiers.js index 941ccca..7561f4f 100644 --- a/bench/minifiers.js +++ b/bench/minifiers.js @@ -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), diff --git a/bench/package.json b/bench/package.json index 0d10df1..07de493 100644 --- a/bench/package.json +++ b/bench/package.json @@ -12,6 +12,9 @@ "request": "^2.88.0", "request-promise-native": "^1.0.8" }, + "engines": { + "node": "10.x" + }, "scripts": { "start": "node bench.js" } diff --git a/bench/speed.json b/bench/speed.json index 9937937..a5f26d4 100644 --- a/bench/speed.json +++ b/bench/speed.json @@ -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 } -] \ No newline at end of file +} \ No newline at end of file diff --git a/bench/speed.png b/bench/speed.png index 0463a3d..80e8dee 100644 Binary files a/bench/speed.png and b/bench/speed.png differ diff --git a/bench/tests.js b/bench/tests.js index f39beca..3385756 100644 --- a/bench/tests.js +++ b/bench/tests.js @@ -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)); diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 0d9e6fc..427d7d1 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "hyperbuild-fuzz-target" +publish = false version = "0.0.1" authors = ["Wilson Lin "] edition = "2018" diff --git a/src/err.rs b/src/err.rs index 92de9cd..12b8a8b 100644 --- a/src/err.rs +++ b/src/err.rs @@ -1,3 +1,5 @@ +// Implement debug to allow .unwrap(). +#[derive(Debug)] pub enum ErrorType { EntityFollowingMalformedEntity, NoSpaceBeforeAttr,