Generify bench
This commit is contained in:
parent
4fc9496829
commit
a28e69ddb0
|
@ -1,7 +1 @@
|
||||||
/minify-html-bench/Cargo.lock
|
/results/
|
||||||
/minify-html-bench/target/
|
|
||||||
/min/
|
|
||||||
/results*/
|
|
||||||
node_modules/
|
|
||||||
package-lock.json
|
|
||||||
perf.data*
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
10
|
|
|
@ -7,10 +7,14 @@ for i in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
|
||||||
echo performance | sudo dd status=none of="$i"
|
echo performance | sudo dd status=none of="$i"
|
||||||
done
|
done
|
||||||
|
|
||||||
node sizes.js
|
results_dir="$PWD/results"
|
||||||
# We need sudo to use `nice` but want to keep using current `node`, so use explicit path in case sudo decides to ignore PATH.
|
input_dir="$PWD/inputs"
|
||||||
node_path="$(which node)"
|
iterations=100
|
||||||
echo "Using Node.js at $node_path"
|
|
||||||
sudo --preserve-env=HTML_ONLY nice -n -20 taskset -c 1 "$node_path" speeds.js
|
pushd runners
|
||||||
sudo chown -R "$USER:$USER" results
|
for r in *; do
|
||||||
node graph.js
|
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
|
||||||
|
|
|
@ -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
|
|
|
@ -1,54 +1,71 @@
|
||||||
const {promises: fs} = require('fs');
|
const { promises: fs } = require("fs");
|
||||||
const request = require('request-promise-native');
|
const childProcess = require("child_process");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
|
|
||||||
const tests = {
|
const tests = {
|
||||||
"Amazon": "https://www.amazon.com/",
|
Amazon: "https://www.amazon.com/",
|
||||||
"BBC": "https://www.bbc.co.uk/",
|
BBC: "https://www.bbc.co.uk/",
|
||||||
"Bootstrap": "https://getbootstrap.com/docs/3.4/css/",
|
Bootstrap: "https://getbootstrap.com/docs/3.4/css/",
|
||||||
"Bing": "https://www.bing.com/",
|
Bing: "https://www.bing.com/",
|
||||||
"Coding Horror": "https://blog.codinghorror.com/",
|
"Coding Horror": "https://blog.codinghorror.com/",
|
||||||
"ECMA-262": "https://www.ecma-international.org/ecma-262/10.0/index.html",
|
"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/",
|
"Hacker News": "https://news.ycombinator.com/",
|
||||||
"NY Times": "https://www.nytimes.com/",
|
"NY Times": "https://www.nytimes.com/",
|
||||||
"Reddit": "https://www.reddit.com/",
|
Reddit: "https://www.reddit.com/",
|
||||||
"Stack Overflow": "https://www.stackoverflow.com/",
|
"Stack Overflow": "https://www.stackoverflow.com/",
|
||||||
"Twitter": "https://twitter.com/",
|
Twitter: "https://twitter.com/",
|
||||||
"Wikipedia": "https://en.wikipedia.org/wiki/Soil",
|
Wikipedia: "https://en.wikipedia.org/wiki/Soil",
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchTest = async (name, url) => {
|
const fetchTest = (name, url) =>
|
||||||
const html = await request({
|
new Promise((resolve, reject) => {
|
||||||
url,
|
// Use curl to follow redirects without needing a Node.js library.
|
||||||
gzip: true,
|
childProcess.execFile(
|
||||||
headers: {
|
"curl",
|
||||||
'Accept': '*/*',
|
[
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; rv:71.0) Gecko/20100101 Firefox/71.0',
|
"-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 () => {
|
(async () => {
|
||||||
const existing = await fs.readdir(path.join(__dirname, 'tests'));
|
const existing = await fs.readdir(path.join(__dirname, "tests"));
|
||||||
await Promise.all(existing.map(e => fs.unlink(path.join(__dirname, 'tests', e))));
|
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.
|
// 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.
|
// Apply some fixes to HTML.
|
||||||
const fixed = html
|
const fixed = html
|
||||||
// Fix early termination of conditional comment in Amazon.
|
// 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.
|
// Fix closing of void tag in Amazon.
|
||||||
.replace(/><\/hr>/g, '/>')
|
.replace(/><\/hr>/g, "/>")
|
||||||
// Fix extra '</div>' in BBC.
|
// 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.
|
// Fix broken attribute value in Stack Overflow.
|
||||||
.replace('height=151"', 'height="151"')
|
.replace('height=151"', 'height="151"');
|
||||||
;
|
await fs.writeFile(path.join(__dirname, "tests", name), fixed);
|
||||||
await fs.writeFile(path.join(__dirname, 'tests', name), fixed);
|
|
||||||
}
|
}
|
||||||
})()
|
})().catch(console.error);
|
||||||
.catch(console.error);
|
|
||||||
|
|
224
bench/graph.js
224
bench/graph.js
|
@ -1,30 +1,30 @@
|
||||||
const results = require('./results');
|
const results = require("./results");
|
||||||
const https = require('https');
|
const https = require("https");
|
||||||
|
|
||||||
const colours = {
|
const colours = {
|
||||||
'minify-html': '#041f60',
|
"minify-html": "#041f60",
|
||||||
'@minify-html/js': '#1f77b4',
|
"@minify-html/js": "#1f77b4",
|
||||||
'minimize': '#ff7f0e',
|
minimize: "#ff7f0e",
|
||||||
'html-minifier': '#2ca02c',
|
"html-minifier": "#2ca02c",
|
||||||
};
|
};
|
||||||
|
|
||||||
const COLOUR_SPEED_PRIMARY = '#2e61bd';
|
const COLOUR_SPEED_PRIMARY = "#2e61bd";
|
||||||
const COLOUR_SPEED_SECONDARY = 'rgb(188, 188, 188)';
|
const COLOUR_SPEED_SECONDARY = "rgb(188, 188, 188)";
|
||||||
const COLOUR_SIZE_PRIMARY = '#64acce';
|
const COLOUR_SIZE_PRIMARY = "#64acce";
|
||||||
const COLOUR_SIZE_SECONDARY = 'rgb(224, 224, 224)';
|
const COLOUR_SIZE_SECONDARY = "rgb(224, 224, 224)";
|
||||||
|
|
||||||
const breakdownChartOptions = (title) => ({
|
const breakdownChartOptions = (title) => ({
|
||||||
options: {
|
options: {
|
||||||
legend: {
|
legend: {
|
||||||
display: true,
|
display: true,
|
||||||
labels: {
|
labels: {
|
||||||
fontColor: '#000',
|
fontColor: "#000",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: title,
|
text: title,
|
||||||
fontColor: '#333',
|
fontColor: "#333",
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
|
@ -32,10 +32,10 @@ const breakdownChartOptions = (title) => ({
|
||||||
{
|
{
|
||||||
barPercentage: 0.25,
|
barPercentage: 0.25,
|
||||||
gridLines: {
|
gridLines: {
|
||||||
color: '#e2e2e2',
|
color: "#e2e2e2",
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
fontColor: '#666',
|
fontColor: "#666",
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -43,10 +43,10 @@ const breakdownChartOptions = (title) => ({
|
||||||
yAxes: [
|
yAxes: [
|
||||||
{
|
{
|
||||||
gridLines: {
|
gridLines: {
|
||||||
color: '#ccc',
|
color: "#ccc",
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
fontColor: '#666',
|
fontColor: "#666",
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -59,7 +59,7 @@ const axisLabel = (fontColor, labelString) => ({
|
||||||
display: true,
|
display: true,
|
||||||
fontColor,
|
fontColor,
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontStyle: 'bold',
|
fontStyle: "bold",
|
||||||
labelString,
|
labelString,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
});
|
});
|
||||||
|
@ -76,31 +76,31 @@ const combinedChartOptions = () => ({
|
||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
fontColor: '#555',
|
fontColor: "#555",
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
yAxes: [
|
yAxes: [
|
||||||
{
|
{
|
||||||
id: 'y1',
|
id: "y1",
|
||||||
type: 'linear',
|
type: "linear",
|
||||||
scaleLabel: axisLabel(COLOUR_SPEED_PRIMARY, 'Performance'),
|
scaleLabel: axisLabel(COLOUR_SPEED_PRIMARY, "Performance"),
|
||||||
position: 'left',
|
position: "left",
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: "$$$_____REPLACE_WITH_TICK_CALLBACK_____$$$",
|
callback: "$$$_____REPLACE_WITH_TICK_CALLBACK_____$$$",
|
||||||
fontColor: COLOUR_SPEED_PRIMARY,
|
fontColor: COLOUR_SPEED_PRIMARY,
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
},
|
},
|
||||||
gridLines: {
|
gridLines: {
|
||||||
color: '#eee',
|
color: "#eee",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'y2',
|
id: "y2",
|
||||||
type: 'linear',
|
type: "linear",
|
||||||
scaleLabel: axisLabel(COLOUR_SIZE_PRIMARY, 'Average size reduction'),
|
scaleLabel: axisLabel(COLOUR_SIZE_PRIMARY, "Average size reduction"),
|
||||||
position: 'right',
|
position: "right",
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: "$$$_____REPLACE_WITH_TICK_CALLBACK_____$$$",
|
callback: "$$$_____REPLACE_WITH_TICK_CALLBACK_____$$$",
|
||||||
fontColor: COLOUR_SIZE_PRIMARY,
|
fontColor: COLOUR_SIZE_PRIMARY,
|
||||||
|
@ -108,90 +108,116 @@ const combinedChartOptions = () => ({
|
||||||
},
|
},
|
||||||
gridLines: {
|
gridLines: {
|
||||||
display: false,
|
display: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderChart = (cfg) => new Promise((resolve, reject) => {
|
const renderChart = (cfg) =>
|
||||||
const req = https.request('https://quickchart.io/chart', {
|
new Promise((resolve, reject) => {
|
||||||
method: 'POST',
|
const req = https.request("https://quickchart.io/chart", {
|
||||||
headers: {
|
method: "POST",
|
||||||
'Content-Type': 'application/json',
|
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 () => {
|
(async () => {
|
||||||
const averageSpeeds = results.getSpeedResults().getAverageRelativeSpeedPerMinifier('@minify-html/js');
|
const averageSpeeds = results
|
||||||
const averageSizes = results.getSizeResults().getAverageRelativeSizePerMinifier();
|
.getSpeedResults()
|
||||||
|
.getAverageRelativeSpeedPerMinifier("@minify-html/js");
|
||||||
|
const averageSizes = results
|
||||||
|
.getSizeResults()
|
||||||
|
.getAverageRelativeSizePerMinifier();
|
||||||
const averageLabels = ["minimize", "html-minifier", "@minify-html/js"];
|
const averageLabels = ["minimize", "html-minifier", "@minify-html/js"];
|
||||||
|
|
||||||
results.writeAverageCombinedGraph(await renderChart({
|
results.writeAverageCombinedGraph(
|
||||||
type: 'bar',
|
await renderChart({
|
||||||
data: {
|
type: "bar",
|
||||||
labels: averageLabels,
|
data: {
|
||||||
datasets: [
|
labels: averageLabels,
|
||||||
{
|
datasets: [
|
||||||
yAxisID: 'y1',
|
{
|
||||||
backgroundColor: averageLabels.map((n) => n === "@minify-html/js" ? COLOUR_SPEED_PRIMARY : COLOUR_SPEED_SECONDARY),
|
yAxisID: "y1",
|
||||||
data: averageLabels.map((n) => averageSpeeds.get(n)),
|
backgroundColor: averageLabels.map((n) =>
|
||||||
},
|
n === "@minify-html/js"
|
||||||
{
|
? COLOUR_SPEED_PRIMARY
|
||||||
yAxisID: 'y2',
|
: COLOUR_SPEED_SECONDARY
|
||||||
backgroundColor: averageLabels.map((n) => n === "@minify-html/js" ? COLOUR_SIZE_PRIMARY : COLOUR_SIZE_SECONDARY),
|
),
|
||||||
data: averageLabels.map((n) => 1 - averageSizes.get(n)),
|
data: averageLabels.map((n) => averageSpeeds.get(n)),
|
||||||
},
|
},
|
||||||
],
|
{
|
||||||
},
|
yAxisID: "y2",
|
||||||
...combinedChartOptions(),
|
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');
|
const speeds = results
|
||||||
results.writeSpeedsGraph(await renderChart({
|
.getSpeedResults()
|
||||||
type: 'bar',
|
.getRelativeFileSpeedsPerMinifier("@minify-html/js");
|
||||||
data: {
|
results.writeSpeedsGraph(
|
||||||
labels: speeds[0][1].map(([n]) => n),
|
await renderChart({
|
||||||
datasets: speeds.map(([minifier, fileSpeeds]) => ({
|
type: "bar",
|
||||||
label: minifier,
|
data: {
|
||||||
backgroundColor: colours[minifier],
|
labels: speeds[0][1].map(([n]) => n),
|
||||||
data: fileSpeeds.map(([_, speed]) => speed),
|
datasets: speeds.map(([minifier, fileSpeeds]) => ({
|
||||||
})),
|
label: minifier,
|
||||||
},
|
backgroundColor: colours[minifier],
|
||||||
...breakdownChartOptions('Operations per second (higher is better)'),
|
data: fileSpeeds.map(([_, speed]) => speed),
|
||||||
}));
|
})),
|
||||||
|
},
|
||||||
|
...breakdownChartOptions("Operations per second (higher is better)"),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const sizes = results.getSizeResults().getRelativeFileSizesPerMinifier();
|
const sizes = results.getSizeResults().getRelativeFileSizesPerMinifier();
|
||||||
results.writeSizesGraph(await renderChart({
|
results.writeSizesGraph(
|
||||||
type: 'bar',
|
await renderChart({
|
||||||
data: {
|
type: "bar",
|
||||||
labels: sizes[0][1].map(([n]) => n),
|
data: {
|
||||||
datasets: sizes.map(([minifier, fileSizes]) => ({
|
labels: sizes[0][1].map(([n]) => n),
|
||||||
label: minifier,
|
datasets: sizes.map(([minifier, fileSizes]) => ({
|
||||||
backgroundColor: colours[minifier],
|
label: minifier,
|
||||||
data: fileSizes.map(([_, size]) => size),
|
backgroundColor: colours[minifier],
|
||||||
})),
|
data: fileSizes.map(([_, size]) => size),
|
||||||
},
|
})),
|
||||||
...breakdownChartOptions('Minified size (lower is better)'),
|
},
|
||||||
}));
|
...breakdownChartOptions("Minified size (lower is better)"),
|
||||||
|
})
|
||||||
|
);
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -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),
|
|
||||||
};
|
|
|
@ -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();
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +1,21 @@
|
||||||
const minifiers = require('./minifiers');
|
const minifiers = require("./minifiers");
|
||||||
const tests = require('./tests');
|
const tests = require("./tests");
|
||||||
const {join} = require('path');
|
const { join } = require("path");
|
||||||
const {mkdirSync, readFileSync, writeFileSync} = require('fs');
|
const { mkdirSync, readFileSync, writeFileSync } = require("fs");
|
||||||
|
|
||||||
const RESULTS_DIR = join(__dirname, 'results');
|
const RESULTS_DIR = join(__dirname, "results");
|
||||||
const SPEEDS_JSON = join(RESULTS_DIR, 'speeds.json');
|
const SPEEDS_JSON = join(RESULTS_DIR, "speeds.json");
|
||||||
const SPEEDS_GRAPH = join(RESULTS_DIR, 'speeds.png');
|
const SPEEDS_GRAPH = join(RESULTS_DIR, "speeds.png");
|
||||||
const AVERAGE_COMBINED_GRAPH = join(RESULTS_DIR, 'average-combined.png');
|
const AVERAGE_COMBINED_GRAPH = join(RESULTS_DIR, "average-combined.png");
|
||||||
const AVERAGE_SPEEDS_GRAPH = join(RESULTS_DIR, 'average-speeds.png');
|
const AVERAGE_SPEEDS_GRAPH = join(RESULTS_DIR, "average-speeds.png");
|
||||||
const SIZES_JSON = join(RESULTS_DIR, 'sizes.json');
|
const SIZES_JSON = join(RESULTS_DIR, "sizes.json");
|
||||||
const SIZES_GRAPH = join(RESULTS_DIR, 'sizes.png');
|
const SIZES_GRAPH = join(RESULTS_DIR, "sizes.png");
|
||||||
const AVERAGE_SIZES_GRAPH = join(RESULTS_DIR, 'average-sizes.png');
|
const AVERAGE_SIZES_GRAPH = join(RESULTS_DIR, "average-sizes.png");
|
||||||
|
|
||||||
const minifierNames = Object.keys(minifiers);
|
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 = {
|
module.exports = {
|
||||||
writeSpeedResults(speeds) {
|
writeSpeedResults(speeds) {
|
||||||
|
@ -40,50 +40,58 @@ module.exports = {
|
||||||
writeFileSync(SIZES_GRAPH, data);
|
writeFileSync(SIZES_GRAPH, data);
|
||||||
},
|
},
|
||||||
getSpeedResults() {
|
getSpeedResults() {
|
||||||
const data = JSON.parse(readFileSync(SPEEDS_JSON, 'utf8'));
|
const data = JSON.parse(readFileSync(SPEEDS_JSON, "utf8"));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Get minifier-speed pairs.
|
// Get minifier-speed pairs.
|
||||||
getAverageRelativeSpeedPerMinifier(baselineMinifier) {
|
getAverageRelativeSpeedPerMinifier(baselineMinifier) {
|
||||||
return new Map(minifierNames.map(minifier => [
|
return new Map(
|
||||||
minifier,
|
minifierNames.map((minifier) => [
|
||||||
testNames
|
minifier,
|
||||||
// Get operations per second for each test.
|
testNames
|
||||||
.map(test => data[test][minifier] / data[test][baselineMinifier])
|
// Get operations per second for each test.
|
||||||
// Sum all test operations per second.
|
.map(
|
||||||
.reduce((sum, c) => sum + c)
|
(test) => data[test][minifier] / data[test][baselineMinifier]
|
||||||
// Divide by tests count to get average operations per second.
|
)
|
||||||
/ testNames.length,
|
// 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.
|
// Get minifier-speeds pairs.
|
||||||
getRelativeFileSpeedsPerMinifier(baselineMinifier) {
|
getRelativeFileSpeedsPerMinifier(baselineMinifier) {
|
||||||
return minifierNames.map(minifier => [
|
return minifierNames.map((minifier) => [
|
||||||
minifier,
|
minifier,
|
||||||
testNames.map(test => [test, data[test][minifier] / data[test][baselineMinifier]]),
|
testNames.map((test) => [
|
||||||
|
test,
|
||||||
|
data[test][minifier] / data[test][baselineMinifier],
|
||||||
|
]),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getSizeResults() {
|
getSizeResults() {
|
||||||
const data = JSON.parse(readFileSync(SIZES_JSON, 'utf8'));
|
const data = JSON.parse(readFileSync(SIZES_JSON, "utf8"));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Get minifier-size pairs.
|
// Get minifier-size pairs.
|
||||||
getAverageRelativeSizePerMinifier() {
|
getAverageRelativeSizePerMinifier() {
|
||||||
return new Map(minifierNames.map(minifier => [
|
return new Map(
|
||||||
minifier,
|
minifierNames.map((minifier) => [
|
||||||
testNames
|
minifier,
|
||||||
.map(test => data[test][minifier].relative)
|
testNames
|
||||||
.reduce((sum, c) => sum + c)
|
.map((test) => data[test][minifier].relative)
|
||||||
/ testNames.length,
|
.reduce((sum, c) => sum + c) / testNames.length,
|
||||||
]));
|
])
|
||||||
|
);
|
||||||
},
|
},
|
||||||
// Get minifier-sizes pairs.
|
// Get minifier-sizes pairs.
|
||||||
getRelativeFileSizesPerMinifier() {
|
getRelativeFileSizesPerMinifier() {
|
||||||
return minifierNames.map(minifier => [
|
return minifierNames.map((minifier) => [
|
||||||
minifier,
|
minifier,
|
||||||
testNames.map(test => [test, data[test][minifier].relative]),
|
testNames.map((test) => [test, data[test][minifier].relative]),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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.
|
|
@ -0,0 +1,2 @@
|
||||||
|
/package-lock.json
|
||||||
|
node_modules/
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -Eeuxo pipefail
|
||||||
|
|
||||||
|
npm i
|
|
@ -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));
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "0.12.19",
|
||||||
|
"html-minifier": "4.0.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -Eeuxo pipefail
|
||||||
|
|
||||||
|
node index.js
|
|
@ -0,0 +1,2 @@
|
||||||
|
/package-lock.json
|
||||||
|
node_modules/
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -Eeuxo pipefail
|
||||||
|
|
||||||
|
pushd ../../../nodejs
|
||||||
|
npm run build
|
||||||
|
popd
|
||||||
|
|
||||||
|
npm i
|
|
@ -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));
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@minify-html/js": "file:../../../nodejs"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -Eeuxo pipefail
|
||||||
|
|
||||||
|
node index.js
|
|
@ -0,0 +1,2 @@
|
||||||
|
/Cargo.lock
|
||||||
|
/target/
|
|
@ -6,8 +6,7 @@ authors = ["Wilson Lin <code@wilsonl.in>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
minify-html = { path = "../../rust/main" }
|
minify-html = { path = "../../../rust/main" }
|
||||||
structopt = "0.3.5"
|
|
||||||
serde = { version = "1.0.104", features = ["derive"] }
|
serde = { version = "1.0.104", features = ["derive"] }
|
||||||
serde_json = "1.0.44"
|
serde_json = "1.0.44"
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -Eeuxo pipefail
|
||||||
|
|
||||||
|
cargo build --release
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -Eeuxo pipefail
|
||||||
|
|
||||||
|
cargo run --release
|
|
@ -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();
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
/Cargo.lock
|
||||||
|
/target/
|
|
@ -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"
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -Eeuxo pipefail
|
||||||
|
|
||||||
|
cargo build --release
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -Eeuxo pipefail
|
||||||
|
|
||||||
|
cargo run --release
|
|
@ -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();
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
/package-lock.json
|
||||||
|
node_modules/
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -Eeuxo pipefail
|
||||||
|
|
||||||
|
npm i
|
|
@ -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));
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "0.12.19",
|
||||||
|
"minimize": "2.2.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -Eeuxo pipefail
|
||||||
|
|
||||||
|
node index.js
|
|
@ -1,15 +1,15 @@
|
||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
const minifiers = require('./minifiers');
|
const minifiers = require("./minifiers");
|
||||||
const results = require('./results');
|
const results = require("./results");
|
||||||
const tests = require('./tests');
|
const tests = require("./tests");
|
||||||
|
|
||||||
const sizes = {};
|
const sizes = {};
|
||||||
const setSize = (program, test, result) => {
|
const setSize = (program, test, result) => {
|
||||||
if (!sizes[test]) {
|
if (!sizes[test]) {
|
||||||
sizes[test] = {
|
sizes[test] = {
|
||||||
original: {
|
original: {
|
||||||
absolute: tests.find(t => t.name === test).contentAsString.length,
|
absolute: tests.find((t) => t.name === test).contentAsString.length,
|
||||||
relative: 1,
|
relative: 1,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -28,8 +28,8 @@ const setSize = (program, test, result) => {
|
||||||
const min = await minifiers[m](t.contentAsString, t.contentAsBuffer);
|
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.
|
// If `min` is a Buffer, convert to string (interpret as UTF-8) to get canonical length.
|
||||||
setSize(m, t.name, min.toString().length);
|
setSize(m, t.name, min.toString().length);
|
||||||
const minPath = path.join(__dirname, 'min', m, `${t.name}.html`);
|
const minPath = path.join(__dirname, "min", m, `${t.name}.html`);
|
||||||
fs.mkdirSync(path.dirname(minPath), {recursive: true});
|
fs.mkdirSync(path.dirname(minPath), { recursive: true });
|
||||||
fs.writeFileSync(minPath, min);
|
fs.writeFileSync(minPath, min);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Failed to run ${m} on test ${t.name}:`);
|
console.error(`Failed to run ${m} on test ${t.name}:`);
|
||||||
|
|
|
@ -1,23 +1,27 @@
|
||||||
const benchmark = require('benchmark');
|
const benchmark = require("benchmark");
|
||||||
const childProcess = require('child_process');
|
const childProcess = require("child_process");
|
||||||
const minimist = require('minimist');
|
const minimist = require("minimist");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
const minifiers = require('./minifiers');
|
const minifiers = require("./minifiers");
|
||||||
const results = require('./results');
|
const results = require("./results");
|
||||||
const tests = require('./tests');
|
const tests = require("./tests");
|
||||||
|
|
||||||
const args = minimist(process.argv.slice(2));
|
const args = minimist(process.argv.slice(2));
|
||||||
const shouldRunRust = !!args.rust;
|
const shouldRunRust = !!args.rust;
|
||||||
|
|
||||||
const cmd = (command, ...args) => {
|
const cmd = (command, ...args) => {
|
||||||
const throwErr = msg => {
|
const throwErr = (msg) => {
|
||||||
throw new Error(`${msg}\n ${command} ${args.join(' ')}`);
|
throw new Error(`${msg}\n ${command} ${args.join(" ")}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const {status, signal, error, stdout, stderr} = childProcess.spawnSync(command, args.map(String), {
|
const { status, signal, error, stdout, stderr } = childProcess.spawnSync(
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
command,
|
||||||
encoding: 'utf8',
|
args.map(String),
|
||||||
});
|
{
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
encoding: "utf8",
|
||||||
|
}
|
||||||
|
);
|
||||||
if (error) {
|
if (error) {
|
||||||
throwErr(error.message);
|
throwErr(error.message);
|
||||||
}
|
}
|
||||||
|
@ -33,42 +37,57 @@ const cmd = (command, ...args) => {
|
||||||
return stdout;
|
return stdout;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fromEntries = entries => {
|
const fromEntries = (entries) => {
|
||||||
if (Object.fromEntries) return Object.fromEntries(entries);
|
if (Object.fromEntries) return Object.fromEntries(entries);
|
||||||
const obj = {};
|
const obj = {};
|
||||||
for (const [prop, val] of entries) obj[prop] = val;
|
for (const [prop, val] of entries) obj[prop] = val;
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
const runTest = test => new Promise((resolve, reject) => {
|
const runTest = (test) =>
|
||||||
// Run JS libraries.
|
new Promise((resolve, reject) => {
|
||||||
const suite = new benchmark.Suite();
|
// Run JS libraries.
|
||||||
for (const m of Object.keys(minifiers)) {
|
const suite = new benchmark.Suite();
|
||||||
suite.add(m, {
|
for (const m of Object.keys(minifiers)) {
|
||||||
defer: true,
|
suite.add(m, {
|
||||||
fn (deferred) {
|
defer: true,
|
||||||
Promise.resolve(minifiers[m](test.contentAsString, test.contentAsBuffer)).then(() => deferred.resolve());
|
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)
|
suite
|
||||||
.run({'async': true});
|
.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 () => {
|
(async () => {
|
||||||
const speeds = fromEntries(tests.map(t => [t.name, {}]));
|
const speeds = fromEntries(tests.map((t) => [t.name, {}]));
|
||||||
|
|
||||||
// Run Rust library.
|
// Run Rust library.
|
||||||
if (shouldRunRust) {
|
if (shouldRunRust) {
|
||||||
for (const [testName, testOps] of JSON.parse(cmd(
|
for (const [testName, testOps] of JSON.parse(
|
||||||
path.join(__dirname, 'minify-html-bench', 'target', 'release', 'minify-html-bench'),
|
cmd(
|
||||||
'--iterations', 512,
|
path.join(
|
||||||
'--tests', path.join(__dirname, 'tests'),
|
__dirname,
|
||||||
))) {
|
"minify-html-bench",
|
||||||
Object.assign(speeds[testName], {['minify-html']: testOps});
|
"target",
|
||||||
|
"release",
|
||||||
|
"minify-html-bench"
|
||||||
|
),
|
||||||
|
"--iterations",
|
||||||
|
512,
|
||||||
|
"--tests",
|
||||||
|
path.join(__dirname, "tests")
|
||||||
|
)
|
||||||
|
)) {
|
||||||
|
Object.assign(speeds[testName], { ["minify-html"]: testOps });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,0 +1 @@
|
||||||
|
/perf.data*
|
Loading…
Reference in New Issue