Generify bench

This commit is contained in:
Wilson Lin 2021-08-08 21:01:37 +10:00
parent 4fc9496829
commit a28e69ddb0
55 changed files with 618 additions and 396 deletions

8
bench/.gitignore vendored
View File

@ -1,7 +1 @@
/minify-html-bench/Cargo.lock
/minify-html-bench/target/
/min/
/results*/
node_modules/
package-lock.json
perf.data*
/results/

View File

@ -1 +0,0 @@
10

View File

@ -7,10 +7,14 @@ for i in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
echo performance | sudo dd status=none of="$i"
done
node sizes.js
# We need sudo to use `nice` but want to keep using current `node`, so use explicit path in case sudo decides to ignore PATH.
node_path="$(which node)"
echo "Using Node.js at $node_path"
sudo --preserve-env=HTML_ONLY nice -n -20 taskset -c 1 "$node_path" speeds.js
sudo chown -R "$USER:$USER" results
node graph.js
results_dir="$PWD/results"
input_dir="$PWD/inputs"
iterations=100
pushd runners
for r in *; do
pushd "$r"
./build
sudo --preserve-env=MHB_HTML_ONLY,PATH MHB_ITERATIONS=$iterations MHB_INPUT_DIR="$input_dir" nice -n -20 taskset -c 1 ./run | tee "$results_dir/$r.json"
popd
done

View File

@ -1,15 +0,0 @@
#!/usr/bin/env bash
set -e
pushd "$(dirname "$0")"
pushd ../nodejs
npm run build
popd
pushd minify-html-bench
cargo build --release
popd
popd

View File

@ -1,54 +1,71 @@
const {promises: fs} = require('fs');
const request = require('request-promise-native');
const path = require('path');
const { promises: fs } = require("fs");
const childProcess = require("child_process");
const path = require("path");
const tests = {
"Amazon": "https://www.amazon.com/",
"BBC": "https://www.bbc.co.uk/",
"Bootstrap": "https://getbootstrap.com/docs/3.4/css/",
"Bing": "https://www.bing.com/",
Amazon: "https://www.amazon.com/",
BBC: "https://www.bbc.co.uk/",
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/",
Google: "https://www.google.com/",
"Hacker News": "https://news.ycombinator.com/",
"NY Times": "https://www.nytimes.com/",
"Reddit": "https://www.reddit.com/",
Reddit: "https://www.reddit.com/",
"Stack Overflow": "https://www.stackoverflow.com/",
"Twitter": "https://twitter.com/",
"Wikipedia": "https://en.wikipedia.org/wiki/Soil",
Twitter: "https://twitter.com/",
Wikipedia: "https://en.wikipedia.org/wiki/Soil",
};
const fetchTest = async (name, url) => {
const html = await request({
url,
gzip: true,
headers: {
'Accept': '*/*',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; rv:71.0) Gecko/20100101 Firefox/71.0',
},
const fetchTest = (name, url) =>
new Promise((resolve, reject) => {
// Use curl to follow redirects without needing a Node.js library.
childProcess.execFile(
"curl",
[
"-H",
`User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; rv:71.0) Gecko/20100101 Firefox/71.0`,
"-H",
"Accept: */*",
"-fLSs",
url,
],
(error, stdout, stderr) => {
if (error) {
return reject(error);
}
if (stderr) {
return reject(new Error(`stderr: ${stderr}`));
}
resolve([name, stdout]);
}
);
});
console.log(`Fetched ${name}`);
return [name, html];
};
(async () => {
const existing = await fs.readdir(path.join(__dirname, 'tests'));
await Promise.all(existing.map(e => fs.unlink(path.join(__dirname, 'tests', e))));
const existing = await fs.readdir(path.join(__dirname, "tests"));
await Promise.all(
existing.map((e) => fs.unlink(path.join(__dirname, "tests", e)))
);
// Format after fetching as formatting is synchronous and can take so long that connections get dropped by server due to inactivity.
for (const [name, html] of await Promise.all(Object.entries(tests).map(([name, url]) => fetchTest(name, url)))) {
for (const [name, html] of await Promise.all(
Object.entries(tests).map(([name, url]) => fetchTest(name, url))
)) {
// Apply some fixes to HTML.
const fixed = html
// Fix early termination of conditional comment in Amazon.
.replace('--></style>\n<![endif]-->', '</style>\n<![endif]-->')
.replace("--></style>\n<![endif]-->", "</style>\n<![endif]-->")
// Fix closing of void tag in Amazon.
.replace(/><\/hr>/g, '/>')
.replace(/><\/hr>/g, "/>")
// Fix extra '</div>' in BBC.
.replace('</a></span></small></div></div></div></footer>', '</a></span></small></div></div></footer>')
.replace(
"</a></span></small></div></div></div></footer>",
"</a></span></small></div></div></footer>"
)
// Fix broken attribute value in Stack Overflow.
.replace('height=151"', 'height="151"')
;
await fs.writeFile(path.join(__dirname, 'tests', name), fixed);
.replace('height=151"', 'height="151"');
await fs.writeFile(path.join(__dirname, "tests", name), fixed);
}
})()
.catch(console.error);
})().catch(console.error);

View File

@ -1,30 +1,30 @@
const results = require('./results');
const https = require('https');
const results = require("./results");
const https = require("https");
const colours = {
'minify-html': '#041f60',
'@minify-html/js': '#1f77b4',
'minimize': '#ff7f0e',
'html-minifier': '#2ca02c',
"minify-html": "#041f60",
"@minify-html/js": "#1f77b4",
minimize: "#ff7f0e",
"html-minifier": "#2ca02c",
};
const COLOUR_SPEED_PRIMARY = '#2e61bd';
const COLOUR_SPEED_SECONDARY = 'rgb(188, 188, 188)';
const COLOUR_SIZE_PRIMARY = '#64acce';
const COLOUR_SIZE_SECONDARY = 'rgb(224, 224, 224)';
const COLOUR_SPEED_PRIMARY = "#2e61bd";
const COLOUR_SPEED_SECONDARY = "rgb(188, 188, 188)";
const COLOUR_SIZE_PRIMARY = "#64acce";
const COLOUR_SIZE_SECONDARY = "rgb(224, 224, 224)";
const breakdownChartOptions = (title) => ({
options: {
legend: {
display: true,
labels: {
fontColor: '#000',
fontColor: "#000",
},
},
title: {
display: true,
text: title,
fontColor: '#333',
fontColor: "#333",
fontSize: 24,
},
scales: {
@ -32,10 +32,10 @@ const breakdownChartOptions = (title) => ({
{
barPercentage: 0.25,
gridLines: {
color: '#e2e2e2',
color: "#e2e2e2",
},
ticks: {
fontColor: '#666',
fontColor: "#666",
fontSize: 20,
},
},
@ -43,10 +43,10 @@ const breakdownChartOptions = (title) => ({
yAxes: [
{
gridLines: {
color: '#ccc',
color: "#ccc",
},
ticks: {
fontColor: '#666',
fontColor: "#666",
fontSize: 20,
},
},
@ -59,7 +59,7 @@ const axisLabel = (fontColor, labelString) => ({
display: true,
fontColor,
fontSize: 24,
fontStyle: 'bold',
fontStyle: "bold",
labelString,
padding: 16,
});
@ -76,31 +76,31 @@ const combinedChartOptions = () => ({
display: false,
},
ticks: {
fontColor: '#555',
fontColor: "#555",
fontSize: 24,
},
},
],
yAxes: [
{
id: 'y1',
type: 'linear',
scaleLabel: axisLabel(COLOUR_SPEED_PRIMARY, 'Performance'),
position: 'left',
id: "y1",
type: "linear",
scaleLabel: axisLabel(COLOUR_SPEED_PRIMARY, "Performance"),
position: "left",
ticks: {
callback: "$$$_____REPLACE_WITH_TICK_CALLBACK_____$$$",
fontColor: COLOUR_SPEED_PRIMARY,
fontSize: 24,
},
gridLines: {
color: '#eee',
color: "#eee",
},
},
{
id: 'y2',
type: 'linear',
scaleLabel: axisLabel(COLOUR_SIZE_PRIMARY, 'Average size reduction'),
position: 'right',
id: "y2",
type: "linear",
scaleLabel: axisLabel(COLOUR_SIZE_PRIMARY, "Average size reduction"),
position: "right",
ticks: {
callback: "$$$_____REPLACE_WITH_TICK_CALLBACK_____$$$",
fontColor: COLOUR_SIZE_PRIMARY,
@ -108,90 +108,116 @@ const combinedChartOptions = () => ({
},
gridLines: {
display: false,
}
},
},
],
},
},
});
const renderChart = (cfg) => new Promise((resolve, reject) => {
const req = https.request('https://quickchart.io/chart', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
const renderChart = (cfg) =>
new Promise((resolve, reject) => {
const req = https.request("https://quickchart.io/chart", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
req.on("error", reject);
req.on("response", (res) => {
const err = res.headers["x-quickchart-error"];
if (res.statusCode < 200 || res.statusCode > 299 || err) {
return reject(new Error(err || `Status ${res.statusCode}`));
}
const chunks = [];
res.on("error", reject);
res.on("data", (c) => chunks.push(c));
res.on("end", () => resolve(Buffer.concat(chunks)));
});
req.end(
JSON.stringify({
backgroundColor: "white",
chart: JSON.stringify(cfg).replaceAll(
'"$$$_____REPLACE_WITH_TICK_CALLBACK_____$$$"',
"function(value) {return Math.round(value * 10000) / 100 + '%';}"
),
width: 1333,
height: 768,
format: "png",
})
);
});
req.on('error', reject);
req.on('response', res => {
const err = res.headers['x-quickchart-error'];
if (res.statusCode < 200 || res.statusCode > 299 || err) {
return reject(new Error(err || `Status ${res.statusCode}`));
}
const chunks = [];
res.on('error', reject);
res.on('data', c => chunks.push(c));
res.on('end', () => resolve(Buffer.concat(chunks)));
});
req.end(JSON.stringify({
backgroundColor: 'white',
chart: JSON.stringify(cfg).replaceAll('"$$$_____REPLACE_WITH_TICK_CALLBACK_____$$$"', "function(value) {return Math.round(value * 10000) / 100 + '%';}"),
width: 1333,
height: 768,
format: 'png',
}));
});
(async () => {
const averageSpeeds = results.getSpeedResults().getAverageRelativeSpeedPerMinifier('@minify-html/js');
const averageSizes = results.getSizeResults().getAverageRelativeSizePerMinifier();
const averageSpeeds = results
.getSpeedResults()
.getAverageRelativeSpeedPerMinifier("@minify-html/js");
const averageSizes = results
.getSizeResults()
.getAverageRelativeSizePerMinifier();
const averageLabels = ["minimize", "html-minifier", "@minify-html/js"];
results.writeAverageCombinedGraph(await renderChart({
type: 'bar',
data: {
labels: averageLabels,
datasets: [
{
yAxisID: 'y1',
backgroundColor: averageLabels.map((n) => n === "@minify-html/js" ? COLOUR_SPEED_PRIMARY : COLOUR_SPEED_SECONDARY),
data: averageLabels.map((n) => averageSpeeds.get(n)),
},
{
yAxisID: 'y2',
backgroundColor: averageLabels.map((n) => n === "@minify-html/js" ? COLOUR_SIZE_PRIMARY : COLOUR_SIZE_SECONDARY),
data: averageLabels.map((n) => 1 - averageSizes.get(n)),
},
],
},
...combinedChartOptions(),
}));
results.writeAverageCombinedGraph(
await renderChart({
type: "bar",
data: {
labels: averageLabels,
datasets: [
{
yAxisID: "y1",
backgroundColor: averageLabels.map((n) =>
n === "@minify-html/js"
? COLOUR_SPEED_PRIMARY
: COLOUR_SPEED_SECONDARY
),
data: averageLabels.map((n) => averageSpeeds.get(n)),
},
{
yAxisID: "y2",
backgroundColor: averageLabels.map((n) =>
n === "@minify-html/js"
? COLOUR_SIZE_PRIMARY
: COLOUR_SIZE_SECONDARY
),
data: averageLabels.map((n) => 1 - averageSizes.get(n)),
},
],
},
...combinedChartOptions(),
})
);
const speeds = results.getSpeedResults().getRelativeFileSpeedsPerMinifier('@minify-html/js');
results.writeSpeedsGraph(await renderChart({
type: 'bar',
data: {
labels: speeds[0][1].map(([n]) => n),
datasets: speeds.map(([minifier, fileSpeeds]) => ({
label: minifier,
backgroundColor: colours[minifier],
data: fileSpeeds.map(([_, speed]) => speed),
})),
},
...breakdownChartOptions('Operations per second (higher is better)'),
}));
const speeds = results
.getSpeedResults()
.getRelativeFileSpeedsPerMinifier("@minify-html/js");
results.writeSpeedsGraph(
await renderChart({
type: "bar",
data: {
labels: speeds[0][1].map(([n]) => n),
datasets: speeds.map(([minifier, fileSpeeds]) => ({
label: minifier,
backgroundColor: colours[minifier],
data: fileSpeeds.map(([_, speed]) => speed),
})),
},
...breakdownChartOptions("Operations per second (higher is better)"),
})
);
const sizes = results.getSizeResults().getRelativeFileSizesPerMinifier();
results.writeSizesGraph(await renderChart({
type: 'bar',
data: {
labels: sizes[0][1].map(([n]) => n),
datasets: sizes.map(([minifier, fileSizes]) => ({
label: minifier,
backgroundColor: colours[minifier],
data: fileSizes.map(([_, size]) => size),
})),
},
...breakdownChartOptions('Minified size (lower is better)'),
}));
results.writeSizesGraph(
await renderChart({
type: "bar",
data: {
labels: sizes[0][1].map(([n]) => n),
datasets: sizes.map(([minifier, fileSizes]) => ({
label: minifier,
backgroundColor: colours[minifier],
data: fileSizes.map(([_, size]) => size),
})),
},
...breakdownChartOptions("Minified size (lower is better)"),
})
);
})();

View File

@ -1,90 +0,0 @@
const esbuild = require('esbuild');
const htmlMinifier = require('html-minifier');
const minifyHtml = require('@minify-html/js');
const minimize = require('minimize');
const testJsAndCssMinification = process.env.HTML_ONLY !== '1';
const jsMime = new Set([
undefined,
'application/ecmascript',
'application/javascript',
'application/x-ecmascript',
'application/x-javascript',
'text/ecmascript',
'text/javascript',
'text/javascript1.0',
'text/javascript1.1',
'text/javascript1.2',
'text/javascript1.3',
'text/javascript1.4',
'text/javascript1.5',
'text/jscript',
'text/livescript',
'text/x-ecmascript',
'text/x-javascript',
]);
const minifyHtmlCfg = minifyHtml.createConfiguration({
minify_js: testJsAndCssMinification,
minify_css: testJsAndCssMinification,
});
const esbuildCss = code => esbuild.transformSync(code, {
loader: "css",
minify: true,
}).code;
const esbuildJs = code => esbuild.transformSync(code, {
loader: "js",
minify: true,
}).code.replace(/<\/script/g, "<\\/script");
const htmlMinifierCfg = {
collapseBooleanAttributes: true,
collapseInlineTagWhitespace: true,
collapseWhitespace: true,
// minify-html can do context-aware whitespace removal, which is safe when configured correctly to match how whitespace is used in the document.
// html-minifier cannot, so whitespace must be collapsed conservatively.
// Alternatively, minify-html can also be made to remove whitespace regardless of context.
conservativeCollapse: true,
customEventAttributes: [],
decodeEntities: true,
ignoreCustomComments: [],
ignoreCustomFragments: [/<\?[\s\S]*?\?>/],
minifyCSS: testJsAndCssMinification && esbuildCss,
minifyJS: testJsAndCssMinification && esbuildJs,
processConditionalComments: true,
removeAttributeQuotes: true,
removeComments: true,
removeEmptyAttributes: true,
removeOptionalTags: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
removeTagWhitespace: true,
useShortDoctype: true,
};
module.exports = {
'@minify-html/js': (_, buffer) => minifyHtml.minify(buffer, minifyHtmlCfg),
'html-minifier': content => htmlMinifier.minify(content, htmlMinifierCfg),
'minimize': testJsAndCssMinification
? (content) => new minimize({
plugins: [
{
id: 'esbuild',
element: (node, next) => {
if (node.type === 'text' && node.parent) {
if (node.parent.type === 'script' && jsMime.has(node.parent.attribs.type)) {
node.data = esbuildJs(node.data);
} else if (node.parent.type === 'style') {
node.data = esbuildCss(node.data);
}
}
next();
},
},
],
}).parse(content)
: content => new minimize().parse(content),
};

View File

@ -1,34 +0,0 @@
use minify_html::{Cfg, minify};
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 data = source.to_vec();
minify(&data, &Cfg::new());
};
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();
}

View File

@ -1,16 +0,0 @@
{
"private": true,
"dependencies": {
"@minify-html/js": "file:../nodejs",
"benchmark": "2.1.4",
"esbuild": "^0.12.18",
"html-minifier": "4.0.0",
"minimize": "2.2.0",
"minimist": "^1.2.5",
"request": "^2.88.2",
"request-promise-native": "^1.0.9"
},
"scripts": {
"start": "node bench.js"
}
}

View File

@ -1,21 +1,21 @@
const minifiers = require('./minifiers');
const tests = require('./tests');
const {join} = require('path');
const {mkdirSync, readFileSync, writeFileSync} = require('fs');
const minifiers = require("./minifiers");
const tests = require("./tests");
const { join } = require("path");
const { mkdirSync, readFileSync, writeFileSync } = require("fs");
const RESULTS_DIR = join(__dirname, 'results');
const SPEEDS_JSON = join(RESULTS_DIR, 'speeds.json');
const SPEEDS_GRAPH = join(RESULTS_DIR, 'speeds.png');
const AVERAGE_COMBINED_GRAPH = join(RESULTS_DIR, 'average-combined.png');
const AVERAGE_SPEEDS_GRAPH = join(RESULTS_DIR, 'average-speeds.png');
const SIZES_JSON = join(RESULTS_DIR, 'sizes.json');
const SIZES_GRAPH = join(RESULTS_DIR, 'sizes.png');
const AVERAGE_SIZES_GRAPH = join(RESULTS_DIR, 'average-sizes.png');
const RESULTS_DIR = join(__dirname, "results");
const SPEEDS_JSON = join(RESULTS_DIR, "speeds.json");
const SPEEDS_GRAPH = join(RESULTS_DIR, "speeds.png");
const AVERAGE_COMBINED_GRAPH = join(RESULTS_DIR, "average-combined.png");
const AVERAGE_SPEEDS_GRAPH = join(RESULTS_DIR, "average-speeds.png");
const SIZES_JSON = join(RESULTS_DIR, "sizes.json");
const SIZES_GRAPH = join(RESULTS_DIR, "sizes.png");
const AVERAGE_SIZES_GRAPH = join(RESULTS_DIR, "average-sizes.png");
const minifierNames = Object.keys(minifiers);
const testNames = tests.map(t => t.name);
const testNames = tests.map((t) => t.name);
mkdirSync(RESULTS_DIR, {recursive: true});
mkdirSync(RESULTS_DIR, { recursive: true });
module.exports = {
writeSpeedResults(speeds) {
@ -40,50 +40,58 @@ module.exports = {
writeFileSync(SIZES_GRAPH, data);
},
getSpeedResults() {
const data = JSON.parse(readFileSync(SPEEDS_JSON, 'utf8'));
const data = JSON.parse(readFileSync(SPEEDS_JSON, "utf8"));
return {
// Get minifier-speed pairs.
getAverageRelativeSpeedPerMinifier(baselineMinifier) {
return new Map(minifierNames.map(minifier => [
minifier,
testNames
// Get operations per second for each test.
.map(test => data[test][minifier] / data[test][baselineMinifier])
// Sum all test operations per second.
.reduce((sum, c) => sum + c)
// Divide by tests count to get average operations per second.
/ testNames.length,
]));
return new Map(
minifierNames.map((minifier) => [
minifier,
testNames
// Get operations per second for each test.
.map(
(test) => data[test][minifier] / data[test][baselineMinifier]
)
// Sum all test operations per second.
.reduce((sum, c) => sum + c) /
// Divide by tests count to get average operations per second.
testNames.length,
])
);
},
// Get minifier-speeds pairs.
getRelativeFileSpeedsPerMinifier(baselineMinifier) {
return minifierNames.map(minifier => [
return minifierNames.map((minifier) => [
minifier,
testNames.map(test => [test, data[test][minifier] / data[test][baselineMinifier]]),
testNames.map((test) => [
test,
data[test][minifier] / data[test][baselineMinifier],
]),
]);
},
};
},
getSizeResults() {
const data = JSON.parse(readFileSync(SIZES_JSON, 'utf8'));
const data = JSON.parse(readFileSync(SIZES_JSON, "utf8"));
return {
// Get minifier-size pairs.
getAverageRelativeSizePerMinifier() {
return new Map(minifierNames.map(minifier => [
minifier,
testNames
.map(test => data[test][minifier].relative)
.reduce((sum, c) => sum + c)
/ testNames.length,
]));
return new Map(
minifierNames.map((minifier) => [
minifier,
testNames
.map((test) => data[test][minifier].relative)
.reduce((sum, c) => sum + c) / testNames.length,
])
);
},
// Get minifier-sizes pairs.
getRelativeFileSizesPerMinifier() {
return minifierNames.map(minifier => [
return minifierNames.map((minifier) => [
minifier,
testNames.map(test => [test, data[test][minifier].relative]),
testNames.map((test) => [test, data[test][minifier].relative]),
]);
},
};

9
bench/runners/README.md Normal file
View File

@ -0,0 +1,9 @@
# Benchmark runners
- Each directory should have an executable `./build` and `./run`.
- The runners should use the following environment variables:
- `MHB_ITERATIONS`: times to run each input.
- `MHB_INPUT_DIR`: path to directory containing inputs. Files should be read from this directory and used as the inputs.
- `MHB_HTML_ONLY`: if set to `1`, `minify_css` and `minify_js` should be disabled.
- The output should be a JSON array of pairs, where each pair represents the input name and execution time in seconds (as a floating point value).
- The execution time should be measured using high-precision monotonic system clocks where possible.

View File

@ -0,0 +1,2 @@
/package-lock.json
node_modules/

View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -Eeuxo pipefail
npm i

View File

@ -0,0 +1,57 @@
const esbuild = require("esbuild");
const fs = require("fs");
const htmlMinifier = require("html-minifier");
const path = require("path");
const iterations = parseInt(process.env.MHB_ITERATIONS, 10);
const inputDir = process.env.MHB_INPUT_DIR;
const htmlOnly = process.env.MHB_HTML_ONLY === "1";
const htmlMinifierCfg = {
collapseBooleanAttributes: true,
collapseInlineTagWhitespace: true,
collapseWhitespace: true,
// minify-html can do context-aware whitespace removal, which is safe when configured correctly to match how whitespace is used in the document.
// html-minifier cannot, so whitespace must be collapsed conservatively.
// Alternatively, minify-html can also be made to remove whitespace regardless of context.
conservativeCollapse: true,
customEventAttributes: [],
decodeEntities: true,
ignoreCustomComments: [],
ignoreCustomFragments: [/<\?[\s\S]*?\?>/],
minifyCSS: !htmlOnly && esbuildCss,
minifyJS: !htmlOnly && esbuildJs,
processConditionalComments: true,
removeAttributeQuotes: true,
removeComments: true,
removeEmptyAttributes: true,
removeOptionalTags: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
removeTagWhitespace: true,
useShortDoctype: true,
};
const esbuildCss = (code) =>
esbuild.transformSync(code, {
loader: "css",
minify: true,
}).code;
const esbuildJs = (code) =>
esbuild.transformSync(code, {
loader: "js",
minify: true,
}).code;
const results = fs.readdirSync(inputDir).map((name) => {
const src = fs.readFileSync(path.join(inputDir, name), "utf8");
const start = process.hrtime.bigint();
for (let i = 0; i < iterations; i++) {
htmlMinifier.minify(src, htmlMinifierCfg);
}
const elapsed = process.hrtime.bigint() - start;
return [name, Number(elapsed) / 1_000_000_000];
});
console.log(JSON.stringify(results));

View File

@ -0,0 +1,7 @@
{
"private": true,
"dependencies": {
"esbuild": "0.12.19",
"html-minifier": "4.0.0"
}
}

View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -Eeuxo pipefail
node index.js

View File

@ -0,0 +1,2 @@
/package-lock.json
node_modules/

View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -Eeuxo pipefail
pushd ../../../nodejs
npm run build
popd
npm i

View File

@ -0,0 +1,23 @@
const fs = require("fs");
const minifyHtml = require("@minify-html/js");
const path = require("path");
const iterations = parseInt(process.env.MHB_ITERATIONS, 10);
const inputDir = process.env.MHB_INPUT_DIR;
const htmlOnly = process.env.MHB_HTML_ONLY === "1";
const minifyHtmlCfg = minifyHtml.createConfiguration({
minify_css: !htmlOnly,
minify_js: !htmlOnly,
});
const results = fs.readdirSync(inputDir).map((name) => {
const src = fs.readFileSync(path.join(inputDir, name));
const start = process.hrtime.bigint();
for (let i = 0; i < iterations; i++) {
minifyHtml.minify(src, minifyHtmlCfg);
}
const elapsed = process.hrtime.bigint() - start;
return [name, Number(elapsed) / 1_000_000_000];
});
console.log(JSON.stringify(results));

View File

@ -0,0 +1,6 @@
{
"private": true,
"dependencies": {
"@minify-html/js": "file:../../../nodejs"
}
}

View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -Eeuxo pipefail
node index.js

View File

@ -0,0 +1,2 @@
/Cargo.lock
/target/

View File

@ -6,8 +6,7 @@ authors = ["Wilson Lin <code@wilsonl.in>"]
edition = "2018"
[dependencies]
minify-html = { path = "../../rust/main" }
structopt = "0.3.5"
minify-html = { path = "../../../rust/main" }
serde = { version = "1.0.104", features = ["derive"] }
serde_json = "1.0.44"

View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -Eeuxo pipefail
cargo build --release

View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -Eeuxo pipefail
cargo run --release

View File

@ -0,0 +1,33 @@
use std::{env, fs};
use std::io::stdout;
use std::time::Instant;
use minify_html::{Cfg, minify};
fn main() {
let iterations = env::var("MHB_ITERATIONS").unwrap().parse::<usize>().unwrap();
let input_dir = env::var("MHB_INPUT_DIR").unwrap();
let html_only = env::var("MHB_HTML_ONLY").unwrap() == "1";
let tests = fs::read_dir(input_dir).unwrap().map(|d| d.unwrap());
let mut results: Vec<(String, f64)> = Vec::new();
let mut cfg = Cfg::new();
if !html_only {
cfg.minify_css = true;
cfg.minify_js = true;
};
for t in tests {
let source = fs::read(t.path()).unwrap();
let start = Instant::now();
for _ in 0..iterations {
let data = source.to_vec();
minify(&source, &cfg);
};
let elapsed = start.elapsed().as_secs_f64();
results.push((t.file_name().into_string().unwrap(), elapsed));
};
serde_json::to_writer(stdout(), &results).unwrap();
}

View File

@ -0,0 +1,2 @@
/Cargo.lock
/target/

View File

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

View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -Eeuxo pipefail
cargo build --release

View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -Eeuxo pipefail
cargo run --release

View File

@ -0,0 +1,32 @@
use std::{env, fs};
use std::io::stdout;
use std::time::Instant;
use minify_html_onepass::{Cfg, in_place};
fn main() {
let iterations = env::var("MHB_ITERATIONS").unwrap().parse::<usize>().unwrap();
let input_dir = env::var("MHB_INPUT_DIR").unwrap();
let html_only = env::var("MHB_HTML_ONLY").unwrap() == "1";
let tests = fs::read_dir(input_dir).unwrap().map(|d| d.unwrap());
let mut results: Vec<(String, f64)> = Vec::new();
let mut cfg = Cfg {
minify_css: !html_only,
minify_js: !html_only,
};
for t in tests {
let source = fs::read(t.path()).unwrap();
let start = Instant::now();
for _ in 0..iterations {
let mut data = source.to_vec();
let _ = in_place(&mut data, &cfg).unwrap();
};
let elapsed = start.elapsed().as_secs_f64();
results.push((t.file_name().into_string().unwrap(), elapsed));
};
serde_json::to_writer(stdout(), &results).unwrap();
}

2
bench/runners/minimize/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/package-lock.json
node_modules/

5
bench/runners/minimize/build Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -Eeuxo pipefail
npm i

View File

@ -0,0 +1,70 @@
const esbuild = require("esbuild");
const fs = require("fs");
const minimize = require("minimize");
const path = require("path");
const iterations = parseInt(process.env.MHB_ITERATIONS, 10);
const inputDir = process.env.MHB_INPUT_DIR;
const htmlOnly = process.env.MHB_HTML_ONLY === "1";
const jsMime = new Set([
undefined,
"application/ecmascript",
"application/javascript",
"application/x-ecmascript",
"application/x-javascript",
"text/ecmascript",
"text/javascript",
"text/javascript1.0",
"text/javascript1.1",
"text/javascript1.2",
"text/javascript1.3",
"text/javascript1.4",
"text/javascript1.5",
"text/jscript",
"text/livescript",
"text/x-ecmascript",
"text/x-javascript",
]);
const esbuildCss = (code) =>
esbuild.transformSync(code, {
loader: "css",
minify: true,
}).code;
const esbuildJs = (code) =>
esbuild.transformSync(code, {
loader: "js",
minify: true,
}).code;
const jsCssPlugin = {
id: "esbuild",
element: (node, next) => {
if (node.type === "text" && node.parent) {
if (
node.parent.type === "script" &&
jsMime.has(node.parent.attribs.type)
) {
node.data = esbuildJs(node.data);
} else if (node.parent.type === "style") {
node.data = esbuildCss(node.data);
}
}
next();
},
};
const plugins = htmlOnly ? [] : [jsCssPlugin];
const results = fs.readdirSync(inputDir).map((name) => {
const src = fs.readFileSync(path.join(inputDir, name), "utf8");
const start = process.hrtime.bigint();
for (let i = 0; i < iterations; i++) {
new minimize({ plugins }).parse(src);
}
const elapsed = process.hrtime.bigint() - start;
return [name, Number(elapsed) / 1_000_000_000];
});
console.log(JSON.stringify(results));

View File

@ -0,0 +1,7 @@
{
"private": true,
"dependencies": {
"esbuild": "0.12.19",
"minimize": "2.2.0"
}
}

5
bench/runners/minimize/run Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -Eeuxo pipefail
node index.js

View File

@ -1,15 +1,15 @@
const fs = require('fs');
const path = require('path');
const minifiers = require('./minifiers');
const results = require('./results');
const tests = require('./tests');
const fs = require("fs");
const path = require("path");
const minifiers = require("./minifiers");
const results = require("./results");
const tests = require("./tests");
const sizes = {};
const setSize = (program, test, result) => {
if (!sizes[test]) {
sizes[test] = {
original: {
absolute: tests.find(t => t.name === test).contentAsString.length,
absolute: tests.find((t) => t.name === test).contentAsString.length,
relative: 1,
},
};
@ -28,8 +28,8 @@ const setSize = (program, test, result) => {
const min = await minifiers[m](t.contentAsString, t.contentAsBuffer);
// If `min` is a Buffer, convert to string (interpret as UTF-8) to get canonical length.
setSize(m, t.name, min.toString().length);
const minPath = path.join(__dirname, 'min', m, `${t.name}.html`);
fs.mkdirSync(path.dirname(minPath), {recursive: true});
const minPath = path.join(__dirname, "min", m, `${t.name}.html`);
fs.mkdirSync(path.dirname(minPath), { recursive: true });
fs.writeFileSync(minPath, min);
} catch (err) {
console.error(`Failed to run ${m} on test ${t.name}:`);

View File

@ -1,23 +1,27 @@
const benchmark = require('benchmark');
const childProcess = require('child_process');
const minimist = require('minimist');
const path = require('path');
const minifiers = require('./minifiers');
const results = require('./results');
const tests = require('./tests');
const benchmark = require("benchmark");
const childProcess = require("child_process");
const minimist = require("minimist");
const path = require("path");
const minifiers = require("./minifiers");
const results = require("./results");
const tests = require("./tests");
const args = minimist(process.argv.slice(2));
const shouldRunRust = !!args.rust;
const cmd = (command, ...args) => {
const throwErr = msg => {
throw new Error(`${msg}\n ${command} ${args.join(' ')}`);
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',
});
const { status, signal, error, stdout, stderr } = childProcess.spawnSync(
command,
args.map(String),
{
stdio: ["ignore", "pipe", "pipe"],
encoding: "utf8",
}
);
if (error) {
throwErr(error.message);
}
@ -33,42 +37,57 @@ const cmd = (command, ...args) => {
return stdout;
};
const fromEntries = entries => {
const fromEntries = (entries) => {
if (Object.fromEntries) return Object.fromEntries(entries);
const obj = {};
for (const [prop, val] of entries) obj[prop] = val;
return obj;
};
const runTest = test => new Promise((resolve, reject) => {
// Run JS libraries.
const suite = new benchmark.Suite();
for (const m of Object.keys(minifiers)) {
suite.add(m, {
defer: true,
fn (deferred) {
Promise.resolve(minifiers[m](test.contentAsString, test.contentAsBuffer)).then(() => deferred.resolve());
},
});
}
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 runTest = (test) =>
new Promise((resolve, reject) => {
// Run JS libraries.
const suite = new benchmark.Suite();
for (const m of Object.keys(minifiers)) {
suite.add(m, {
defer: true,
fn(deferred) {
Promise.resolve(
minifiers[m](test.contentAsString, test.contentAsBuffer)
).then(() => deferred.resolve());
},
});
}
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 });
});
(async () => {
const speeds = fromEntries(tests.map(t => [t.name, {}]));
const speeds = fromEntries(tests.map((t) => [t.name, {}]));
// Run Rust library.
if (shouldRunRust) {
for (const [testName, testOps] of JSON.parse(cmd(
path.join(__dirname, 'minify-html-bench', 'target', 'release', 'minify-html-bench'),
'--iterations', 512,
'--tests', path.join(__dirname, 'tests'),
))) {
Object.assign(speeds[testName], {['minify-html']: testOps});
for (const [testName, testOps] of JSON.parse(
cmd(
path.join(
__dirname,
"minify-html-bench",
"target",
"release",
"minify-html-bench"
),
"--iterations",
512,
"--tests",
path.join(__dirname, "tests")
)
)) {
Object.assign(speeds[testName], { ["minify-html"]: testOps });
}
}

View File

@ -1,9 +0,0 @@
const fs = require('fs');
const path = require('path');
const testsDir = path.join(__dirname, 'tests');
module.exports = fs.readdirSync(testsDir).filter(f => !/^\./.test(f)).map(name => ({
name,
contentAsString: fs.readFileSync(path.join(testsDir, name), 'utf8'),
contentAsBuffer: fs.readFileSync(path.join(testsDir, name)),
})).sort((a, b) => a.name.localeCompare(b.name));

0
bench/compare.sh → debug/diff/compare.sh Executable file → Normal file
View File

1
debug/prof/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/perf.data*

0
bench/profile.sh → debug/prof/profile.sh Executable file → Normal file
View File