Complete new bench

This commit is contained in:
Wilson Lin 2021-08-08 23:11:05 +10:00
parent 21297d053a
commit 38f186f73e
25 changed files with 204 additions and 351 deletions

View File

@ -47,9 +47,7 @@ jobs:
- name: Build bench - name: Build bench
working-directory: ./bench working-directory: ./bench
run: | run: |
sudo apt install -y build-essential ./build
npm install
./build.sh
- name: Set up Backblaze B2 CLI - name: Set up Backblaze B2 CLI
uses: wilsonzlin/setup-b2@v3 uses: wilsonzlin/setup-b2@v3
@ -58,7 +56,7 @@ jobs:
working-directory: ./bench working-directory: ./bench
run: | run: |
b2 authorize-account ${{ secrets.CICD_CLI_B2_KEY_ID }} ${{ secrets.CICD_CLI_B2_APPLICATION_KEY }} b2 authorize-account ${{ secrets.CICD_CLI_B2_KEY_ID }} ${{ secrets.CICD_CLI_B2_APPLICATION_KEY }}
./bench.sh ./run
b2 sync ./results/ b2://${{ secrets.CICD_CLI_B2_BUCKET_NAME }}/minify-html/bench/${{ steps.version.outputs.VERSION }}/js/ b2 sync ./graphs/ b2://${{ secrets.CICD_CLI_B2_BUCKET_NAME }}/minify-html/bench/${{ steps.version.outputs.VERSION }}/js/
HTML_ONLY=1 ./bench.sh MHB_HTML_ONLY=1 ./run
b2 sync ./results/ b2://${{ secrets.CICD_CLI_B2_BUCKET_NAME }}/minify-html/bench/${{ steps.version.outputs.VERSION }}/core/ b2 sync ./graphs/ b2://${{ secrets.CICD_CLI_B2_BUCKET_NAME }}/minify-html/bench/${{ steps.version.outputs.VERSION }}/core/

View File

@ -19,7 +19,8 @@ A Rust HTML minifier meticulously optimised for speed and effectiveness, with bi
Comparison with [html-minfier](https://github.com/kangax/html-minifier) and [minimize](https://github.com/Swaagie/minimize), run on the top web pages. [See the breakdown here.](./bench) Comparison with [html-minfier](https://github.com/kangax/html-minifier) and [minimize](https://github.com/Swaagie/minimize), run on the top web pages. [See the breakdown here.](./bench)
<img alt="Chart showing speed and compression of HTML minifiers" src="https://wilsonl.in/minify-html/bench/0.6.0/core/average-combined.png"> <img alt="Chart showing speed of HTML minifiers" src="https://wilsonl.in/minify-html/bench/0.6.0/core/average-speeds.png">
<img alt="Chart showing compression of HTML minifiers" src="https://wilsonl.in/minify-html/bench/0.6.0/core/average-sizes.png">
Need even faster performance? Check the [one](https://github.com/wilsonzlin/minify-html/tree/one) branch. Need even faster performance? Check the [one](https://github.com/wilsonzlin/minify-html/tree/one) branch.

1
bench/.gitignore vendored
View File

@ -1 +1,2 @@
/graphs/
/results/ /results/

View File

@ -1,17 +1,25 @@
const results = require("./results"); const results = require("./results");
const https = require("https"); const https = require("https");
const path = require("path");
const fs = require("fs/promises");
const colours = { const GRAPHS_DIR = path.join(__dirname, "graphs");
"minify-html": "#041f60", const SPEEDS_GRAPH = path.join(GRAPHS_DIR, "speeds.png");
"@minify-html/js": "#1f77b4", const SIZES_GRAPH = path.join(GRAPHS_DIR, "sizes.png");
minimize: "#ff7f0e", const AVERAGE_SPEEDS_GRAPH = path.join(GRAPHS_DIR, "average-speeds.png");
"html-minifier": "#2ca02c", const AVERAGE_SIZES_GRAPH = path.join(GRAPHS_DIR, "average-sizes.png");
const speedColours = {
"@minify-html/js": "#2e61bd",
"minify-html": "#2e61bd",
"minify-html-onepass": "#222",
}; };
const defaultSpeedColour = "rgb(188, 188, 188)";
const COLOUR_SPEED_PRIMARY = "#2e61bd"; const sizeColours = {
const COLOUR_SPEED_SECONDARY = "rgb(188, 188, 188)"; "minify-html": "#2e61bd",
const COLOUR_SIZE_PRIMARY = "#64acce"; };
const COLOUR_SIZE_SECONDARY = "rgb(224, 224, 224)"; const defaultSizeColour = "rgb(188, 188, 188)";
const breakdownChartOptions = (title) => ({ const breakdownChartOptions = (title) => ({
options: { options: {
@ -30,20 +38,21 @@ const breakdownChartOptions = (title) => ({
scales: { scales: {
xAxes: [ xAxes: [
{ {
barPercentage: 0.25,
gridLines: { gridLines: {
color: "#e2e2e2", color: "#f2f2f2",
}, },
ticks: { ticks: {
fontColor: "#666", callback: "$$$_____REPLACE_WITH_TICK_CALLBACK_____$$$",
fontColor: "#999",
fontSize: 20, fontSize: 20,
}, },
}, },
], ],
yAxes: [ yAxes: [
{ {
barPercentage: 0.5,
gridLines: { gridLines: {
color: "#ccc", color: "#aaa",
}, },
ticks: { ticks: {
fontColor: "#666", fontColor: "#666",
@ -61,10 +70,10 @@ const axisLabel = (fontColor, labelString) => ({
fontSize: 24, fontSize: 24,
fontStyle: "bold", fontStyle: "bold",
labelString, labelString,
padding: 16, padding: 12,
}); });
const combinedChartOptions = () => ({ const averageChartOptions = (label) => ({
options: { options: {
legend: { legend: {
display: false, display: false,
@ -77,45 +86,30 @@ const combinedChartOptions = () => ({
}, },
ticks: { ticks: {
fontColor: "#555", fontColor: "#555",
fontSize: 24, fontSize: 16,
}, },
}, },
], ],
yAxes: [ yAxes: [
{ {
id: "y1",
type: "linear", type: "linear",
scaleLabel: axisLabel(COLOUR_SPEED_PRIMARY, "Performance"), scaleLabel: axisLabel("#222", label),
position: "left", position: "left",
ticks: { ticks: {
callback: "$$$_____REPLACE_WITH_TICK_CALLBACK_____$$$", callback: "$$$_____REPLACE_WITH_TICK_CALLBACK_____$$$",
fontColor: COLOUR_SPEED_PRIMARY, fontColor: "#222",
fontSize: 24, fontSize: 16,
}, },
gridLines: { gridLines: {
color: "#eee", color: "#eee",
}, },
}, },
{
id: "y2",
type: "linear",
scaleLabel: axisLabel(COLOUR_SIZE_PRIMARY, "Average size reduction"),
position: "right",
ticks: {
callback: "$$$_____REPLACE_WITH_TICK_CALLBACK_____$$$",
fontColor: COLOUR_SIZE_PRIMARY,
fontSize: 24,
},
gridLines: {
display: false,
},
},
], ],
}, },
}, },
}); });
const renderChart = (cfg) => const renderChart = (cfg, width, height) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const req = https.request("https://quickchart.io/chart", { const req = https.request("https://quickchart.io/chart", {
method: "POST", method: "POST",
@ -141,83 +135,109 @@ const renderChart = (cfg) =>
'"$$$_____REPLACE_WITH_TICK_CALLBACK_____$$$"', '"$$$_____REPLACE_WITH_TICK_CALLBACK_____$$$"',
"function(value) {return Math.round(value * 10000) / 100 + '%';}" "function(value) {return Math.round(value * 10000) / 100 + '%';}"
), ),
width: 1333, width,
height: 768, height,
format: "png", format: "png",
}) })
); );
}); });
(async () => { (async () => {
const averageSpeeds = results await fs.mkdir(GRAPHS_DIR, { recursive: true });
.getSpeedResults()
.getAverageRelativeSpeedPerMinifier("@minify-html/js");
const averageSizes = results
.getSizeResults()
.getAverageRelativeSizePerMinifier();
const averageLabels = ["minimize", "html-minifier", "@minify-html/js"];
results.writeAverageCombinedGraph( const res = results.calculate();
await renderChart({ const speedMinifiers = [...res.minifiers].sort(
type: "bar", (a, b) => res.minifierAvgOps[a] - res.minifierAvgOps[b]
data: { );
labels: averageLabels, const sizeMinifiers = ["minimize", "html-minifier", "minify-html"];
datasets: [ const inputs = Object.keys(res.inputSizes).sort();
{
yAxisID: "y1", await fs.writeFile(
backgroundColor: averageLabels.map((n) => AVERAGE_SPEEDS_GRAPH,
n === "@minify-html/js" await renderChart(
? COLOUR_SPEED_PRIMARY {
: COLOUR_SPEED_SECONDARY type: "bar",
), data: {
data: averageLabels.map((n) => averageSpeeds.get(n)), labels: speedMinifiers.map(m => m.replace(" (", "\n(")),
}, datasets: [
{ {
yAxisID: "y2", backgroundColor: speedMinifiers.map(
backgroundColor: averageLabels.map((n) => (n) => speedColours[n] ?? defaultSpeedColour
n === "@minify-html/js" ),
? COLOUR_SIZE_PRIMARY data: speedMinifiers.map(
: COLOUR_SIZE_SECONDARY (m) => res.minifierAvgOps[m] / res.maxMinifierAvgOps
), ),
data: averageLabels.map((n) => 1 - averageSizes.get(n)), },
}, ],
], },
...averageChartOptions("Performance"),
}, },
...combinedChartOptions(), 1024,
}) 768
)
); );
const speeds = results await fs.writeFile(
.getSpeedResults() AVERAGE_SIZES_GRAPH,
.getRelativeFileSpeedsPerMinifier("@minify-html/js"); await renderChart(
results.writeSpeedsGraph( {
await renderChart({ type: "bar",
type: "bar", data: {
data: { labels: sizeMinifiers.map(m => m.replace(" (", "\n(")),
labels: speeds[0][1].map(([n]) => n), datasets: [
datasets: speeds.map(([minifier, fileSpeeds]) => ({ {
label: minifier, backgroundColor: sizeMinifiers.map(
backgroundColor: colours[minifier], (n) => sizeColours[n] ?? defaultSizeColour
data: fileSpeeds.map(([_, speed]) => speed), ),
})), data: sizeMinifiers.map((m) => res.minifierAvgReduction[m]),
},
],
},
...averageChartOptions("Reduction"),
}, },
...breakdownChartOptions("Operations per second (higher is better)"), 1024,
}) 768
)
); );
const sizes = results.getSizeResults().getRelativeFileSizesPerMinifier(); await fs.writeFile(
results.writeSizesGraph( SPEEDS_GRAPH,
await renderChart({ await renderChart(
type: "bar", {
data: { type: "horizontalBar",
labels: sizes[0][1].map(([n]) => n), data: {
datasets: sizes.map(([minifier, fileSizes]) => ({ labels: inputs,
label: minifier, datasets: speedMinifiers.map((minifier) => ({
backgroundColor: colours[minifier], label: minifier,
data: fileSizes.map(([_, size]) => size), data: inputs.map(
})), (input) =>
res.perInputOps[minifier][input] / res.maxInputOps[input]
),
})),
},
...breakdownChartOptions("Operations per second (higher is better)"),
}, },
...breakdownChartOptions("Minified size (lower is better)"), 900,
}) 1000
)
);
await fs.writeFile(
SIZES_GRAPH,
await renderChart(
{
type: "horizontalBar",
data: {
labels: inputs,
datasets: sizeMinifiers.map((minifier) => ({
label: minifier,
data: inputs.map((input) => res.perInputReduction[minifier][input]),
})),
},
...breakdownChartOptions("Size reduction (higher is better)"),
},
900,
1000
)
); );
})(); })();

View File

@ -1,99 +1,67 @@
const minifiers = require("./minifiers"); const fs = require("fs");
const tests = require("./tests"); const path = require("path");
const { join } = require("path");
const { mkdirSync, readFileSync, writeFileSync } = require("fs");
const RESULTS_DIR = join(__dirname, "results"); const RESULTS_DIR = path.join(__dirname, "results");
const SPEEDS_JSON = join(RESULTS_DIR, "speeds.json"); const INPUTS_DIR = path.join(__dirname, "inputs");
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);
mkdirSync(RESULTS_DIR, { recursive: true });
module.exports = { module.exports = {
writeSpeedResults(speeds) { calculate: () => {
writeFileSync(SPEEDS_JSON, JSON.stringify(speeds, null, 2)); // minifier => avg(ops).
}, const minifierAvgOps = {};
writeSizeResults(sizes) { // minifier => avg(1 - output / original).
writeFileSync(SIZES_JSON, JSON.stringify(sizes, null, 2)); const minifierAvgReduction = {};
}, let maxMinifierAvgOps = 0;
writeAverageCombinedGraph(data) { // minifier => input => ops.
writeFileSync(AVERAGE_COMBINED_GRAPH, data); const perInputOps = {};
}, // minifier => input => (1 - output / original).
writeAverageSpeedsGraph(data) { const perInputReduction = {};
writeFileSync(AVERAGE_SPEEDS_GRAPH, data); // input => max(ops).
}, const maxInputOps = {};
writeSpeedsGraph(data) { const inputSizes = Object.fromEntries(
writeFileSync(SPEEDS_GRAPH, data); fs.readdirSync(INPUTS_DIR).map((f) => {
}, const name = path.basename(f, ".json");
writeAverageSizesGraph(data) { const stats = fs.statSync(path.join(INPUTS_DIR, f));
writeFileSync(AVERAGE_SIZES_GRAPH, data); return [name, stats.size];
}, })
writeSizesGraph(data) { );
writeFileSync(SIZES_GRAPH, data);
}, for (const f of fs.readdirSync(RESULTS_DIR)) {
getSpeedResults() { const minifier = decodeURIComponent(path.basename(f, ".json"));
const data = JSON.parse(readFileSync(SPEEDS_JSON, "utf8")); const data = JSON.parse(
fs.readFileSync(path.join(RESULTS_DIR, f), "utf8")
);
for (const [input, size, iterations, seconds] of data) {
const originalSize = inputSizes[input];
const ops = 1 / (seconds / iterations);
const reduction = 1 - size / originalSize;
(minifierAvgOps[minifier] ??= []).push(ops);
(minifierAvgReduction[minifier] ??= []).push(reduction);
(perInputOps[minifier] ??= {})[input] = ops;
(perInputReduction[minifier] ??= {})[input] = reduction;
maxInputOps[input] = Math.max(maxInputOps[input] ?? 0, ops);
}
}
const minifiers = Object.keys(minifierAvgOps);
for (const m of minifiers) {
minifierAvgOps[m] =
minifierAvgOps[m].reduce((sum, ops) => sum + ops, 0) /
minifierAvgOps[m].length;
maxMinifierAvgOps = Math.max(maxMinifierAvgOps, minifierAvgOps[m]);
minifierAvgReduction[m] =
minifierAvgReduction[m].reduce((sum, ops) => sum + ops, 0) /
minifierAvgReduction[m].length;
}
return { return {
// Get minifier-speed pairs. minifierAvgReduction,
getAverageRelativeSpeedPerMinifier(baselineMinifier) { minifierAvgOps,
return new Map( maxMinifierAvgOps,
minifierNames.map((minifier) => [ perInputOps,
minifier, perInputReduction,
testNames maxInputOps,
// Get operations per second for each test. inputSizes,
.map( minifiers,
(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) => [
minifier,
testNames.map((test) => [
test,
data[test][minifier] / data[test][baselineMinifier],
]),
]);
},
};
},
getSizeResults() {
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,
])
);
},
// Get minifier-sizes pairs.
getRelativeFileSizesPerMinifier() {
return minifierNames.map((minifier) => [
minifier,
testNames.map((test) => [test, data[test][minifier].relative]),
]);
},
}; };
}, },
}; };

View File

@ -14,10 +14,11 @@ const minifyHtmlCfg = minifyHtml.createConfiguration({
const results = fs.readdirSync(inputDir).map((name) => { const results = fs.readdirSync(inputDir).map((name) => {
const src = fs.readFileSync(path.join(inputDir, name)); const src = fs.readFileSync(path.join(inputDir, name));
const start = process.hrtime.bigint(); const start = process.hrtime.bigint();
let len;
for (let i = 0; i < iterations; i++) { for (let i = 0; i < iterations; i++) {
minifyHtml.minify(src, minifyHtmlCfg); len = minifyHtml.minify(src, minifyHtmlCfg).byteLength;
} }
const elapsed = process.hrtime.bigint() - start; const elapsed = process.hrtime.bigint() - start;
return [name, Number(elapsed) / 1_000_000_000]; return [name, len, iterations, Number(elapsed) / 1_000_000_000];
}); });
console.log(JSON.stringify(results)); console.log(JSON.stringify(results));

View File

@ -5,5 +5,5 @@
- `MHB_ITERATIONS`: times to run each input. - `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_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. - `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 output should be a JSON array of tuples, where each tuples contains the input name, output size, iterations, and execution time in seconds (as a floating point value).
- The execution time should be measured using high-precision monotonic system clocks where possible. - The execution time should be measured using high-precision monotonic system clocks where possible.

View File

@ -48,10 +48,11 @@ const htmlMinifierCfg = {
const results = fs.readdirSync(inputDir).map((name) => { const results = fs.readdirSync(inputDir).map((name) => {
const src = fs.readFileSync(path.join(inputDir, name), "utf8"); const src = fs.readFileSync(path.join(inputDir, name), "utf8");
const start = process.hrtime.bigint(); const start = process.hrtime.bigint();
let len;
for (let i = 0; i < iterations; i++) { for (let i = 0; i < iterations; i++) {
htmlMinifier.minify(src, htmlMinifierCfg); len = htmlMinifier.minify(src, htmlMinifierCfg).length;
} }
const elapsed = process.hrtime.bigint() - start; const elapsed = process.hrtime.bigint() - start;
return [name, Number(elapsed) / 1_000_000_000]; return [name, len, iterations, Number(elapsed) / 1_000_000_000];
}); });
console.log(JSON.stringify(results)); console.log(JSON.stringify(results));

View File

@ -11,7 +11,7 @@ fn main() {
let tests = fs::read_dir(input_dir).unwrap().map(|d| d.unwrap()); let tests = fs::read_dir(input_dir).unwrap().map(|d| d.unwrap());
let mut results: Vec<(String, f64)> = Vec::new(); let mut results: Vec<(String, usize, usize, f64)> = Vec::new();
let cfg = Cfg { let cfg = Cfg {
minify_css: !html_only, minify_css: !html_only,
minify_js: !html_only, minify_js: !html_only,
@ -20,12 +20,13 @@ fn main() {
for t in tests { for t in tests {
let source = fs::read(t.path()).unwrap(); let source = fs::read(t.path()).unwrap();
let start = Instant::now(); let start = Instant::now();
let mut len = 0;
for _ in 0..iterations { for _ in 0..iterations {
let mut data = source.to_vec(); let mut data = source.to_vec();
let _ = in_place(&mut data, &cfg).expect("failed to minify"); len = in_place(&mut data, &cfg).expect("failed to minify");
}; };
let elapsed = start.elapsed().as_secs_f64(); let elapsed = start.elapsed().as_secs_f64();
results.push((t.file_name().into_string().unwrap(), elapsed)); results.push((t.file_name().into_string().unwrap(), len, iterations, elapsed));
}; };
serde_json::to_writer(stdout(), &results).unwrap(); serde_json::to_writer(stdout(), &results).unwrap();

View File

@ -11,7 +11,7 @@ fn main() {
let tests = fs::read_dir(input_dir).unwrap().map(|d| d.unwrap()); let tests = fs::read_dir(input_dir).unwrap().map(|d| d.unwrap());
let mut results: Vec<(String, f64)> = Vec::new(); let mut results: Vec<(String, usize, usize, f64)> = Vec::new();
let mut cfg = Cfg::new(); let mut cfg = Cfg::new();
if !html_only { if !html_only {
cfg.minify_css = true; cfg.minify_css = true;
@ -21,11 +21,12 @@ fn main() {
for t in tests { for t in tests {
let source = fs::read(t.path()).unwrap(); let source = fs::read(t.path()).unwrap();
let start = Instant::now(); let start = Instant::now();
let mut len = 0;
for _ in 0..iterations { for _ in 0..iterations {
let _ = minify(&source, &cfg); len = minify(&source, &cfg).len();
}; };
let elapsed = start.elapsed().as_secs_f64(); let elapsed = start.elapsed().as_secs_f64();
results.push((t.file_name().into_string().unwrap(), elapsed)); results.push((t.file_name().into_string().unwrap(), len, iterations, elapsed));
}; };
serde_json::to_writer(stdout(), &results).unwrap(); serde_json::to_writer(stdout(), &results).unwrap();

View File

@ -61,10 +61,11 @@ const plugins = htmlOnly ? [] : [jsCssPlugin];
const results = fs.readdirSync(inputDir).map((name) => { const results = fs.readdirSync(inputDir).map((name) => {
const src = fs.readFileSync(path.join(inputDir, name), "utf8"); const src = fs.readFileSync(path.join(inputDir, name), "utf8");
const start = process.hrtime.bigint(); const start = process.hrtime.bigint();
let len;
for (let i = 0; i < iterations; i++) { for (let i = 0; i < iterations; i++) {
new minimize({ plugins }).parse(src); len = new minimize({ plugins }).parse(src).length;
} }
const elapsed = process.hrtime.bigint() - start; const elapsed = process.hrtime.bigint() - start;
return [name, Number(elapsed) / 1_000_000_000]; return [name, len, iterations, Number(elapsed) / 1_000_000_000];
}); });
console.log(JSON.stringify(results)); console.log(JSON.stringify(results));

View File

@ -1,42 +0,0 @@
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,
relative: 1,
},
};
}
const original = sizes[test].original.absolute;
sizes[test][program] = {
absolute: result,
relative: result / original,
};
};
(async () => {
for (const t of tests) {
for (const m of Object.keys(minifiers)) {
try {
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 });
fs.writeFileSync(minPath, min);
} catch (err) {
console.error(`Failed to run ${m} on test ${t.name}:`);
console.error(err);
process.exit(1);
}
}
}
results.writeSizeResults(sizes);
})();

View File

@ -1,98 +0,0 @@
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 { 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 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, {}]));
// 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 t of tests) {
Object.assign(speeds[t.name], await runTest(t));
}
results.writeSpeedResults(speeds);
})();