Skip to content

Instantly share code, notes, and snippets.

@cometkim
Last active April 19, 2024 14:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cometkim/a3622d373461dddc24c1eb0f39c92543 to your computer and use it in GitHub Desktop.
Save cometkim/a3622d373461dddc24c1eb0f39c92543 to your computer and use it in GitHub Desktop.
HTTP protocol version test via cURL (not a good way)
import { parseArgs } from 'node:util';
import { spawn } from 'node:child_process';
import { setTimeout } from 'node:timers/promises';
import prettyBytes from 'pretty-bytes';
import prettyMilliseconds from 'pretty-ms';
let { values, positionals } = parseArgs({
args: process.argv.slice(2),
allowPositionals: true,
options: {
runs: {
type: 'string',
default: '1000',
},
extraOpts: {
type: 'string',
default: '',
},
throttle: {
type: 'string',
default: '10',
},
},
});
let [targetUrlString] = positionals;
let targetUrl = new URL(targetUrlString);
let runs = parseInt(values.runs) || 5000;
let throttle = parseInt(values.throttle);
let ctx = { i: 0, interval: null };
let initCtx = () => {
ctx.i = 0;
ctx.interval = null;
};
let tick = () => {
console.log(`${ctx.i} times tested, ${runs - ctx.i} remaining`);
ctx.interval = globalThis.setTimeout(tick, 5000);
};
process.on('SIGINT', onExit);
process.on('SIGUSR1', onExit);
process.on('SIGUSR2', onExit);
process.on('SIGTERM', onExit);
process.on('SIGHUP', onExit);
let results = {
'http1.1': {
preTransferResults: [],
downloadSpeedResults: [],
},
'http2': {
preTransferResults: [],
downloadSpeedResults: [],
},
'http3-only': {
preTransferResults: [],
downloadSpeedResults: [],
},
};
for (let proto of Object.keys(results)) {
console.group(`testing ${proto} to ${targetUrl} ${runs} times...`);
initCtx();
for (; ctx.i < runs; ctx.i++) {
ctx.interval ||= globalThis.setTimeout(tick, 1000);
if (ctx.i !== 0) { await setTimeout(throttle); }
let { code, stdout, stderr } = await exec('curl', [
`--${proto}`,
'-s',
'-o',
'/dev/null',
'-w',
'%{time_pretransfer} %{speed_download}',
...values.extraOpts.split(' ').filter(Boolean),
targetUrl.toString(),
]);
if (code !== 0) {
console.error('Failed to request.', stderr);
process.exit(code);
}
let { timePreTransfer, speedDownload } = parseFormat(stdout);
results[proto].preTransferResults.push(timePreTransfer);
results[proto].downloadSpeedResults.push(speedDownload);
}
console.groupEnd();
}
function onExit() {
globalThis.clearTimeout(ctx.interval);
let reports = {};
for (let [key, value] of Object.entries(results)) {
let asc = (a, b) => a - b;
let preTransferResults = value.preTransferResults.toSorted(asc);
let downloadSpeedResults = value.downloadSpeedResults.toSorted(asc);
if (preTransferResults.length && downloadSpeedResults.length) {
reports[key] = {
'preTransfer': `${subMillis(avg(preTransferResults))} (${subMillis(preTransferResults[0])} ~ ${subMillis(preTransferResults.at(-1))})`,
'preTransfer (p50)': subMillis(pN(50, preTransferResults)),
'preTransfer (p90)': subMillis(pN(90, preTransferResults)),
'preTransfer (p99)': subMillis(pN(99, preTransferResults)),
'downloadSpeed': `${bytesPerSec(avg(downloadSpeedResults))} (${bytesPerSec(downloadSpeedResults[0])} ~ ${bytesPerSec(downloadSpeedResults.at(-1))})`,
'downloadSpeed (p50)': bytesPerSec(pN(50, downloadSpeedResults)),
'downloadSpeed (p90)': bytesPerSec(pN(90, downloadSpeedResults)),
'downloadSpeed (p99)': bytesPerSec(pN(99, downloadSpeedResults)),
};
}
}
console.table(reports);
process.exit(0);
};
onExit();
function parseFormat(line) {
let [timePreTransfer, speedDownload] = line.split(' ');
return {
timePreTransfer: parseFloat(timePreTransfer),
speedDownload: parseInt(speedDownload),
};
}
function avg(arr) {
return arr.reduce((acc, cur) => acc + cur, 0) / arr.length;
}
function pN(n, sorted) {
return sorted.at(sorted.length / (100 / n) | 0);
}
function subMillis(ms, unit = true) {
let pretty = prettyMilliseconds(ms, { formatSubMilliseconds: true })
let [major, sub] = [...pretty.matchAll(/(?<n>\d+(\.\d+)?)(?<unit>[^ ]+)/g)].map(match => match.groups);
return `${major.n}${sub ? `.${sub.n.padStart(0, 3)}` : ''}${unit ? major.unit : ''}`;
}
function bytesPerSec(bytes, unit = true) {
let pretty = prettyBytes(bytes);
let [major, sub] = [...pretty.matchAll(/(?<n>\d+(\.\d+)?) (?<unit>[^ ]+)/g)].map(match => match.groups);
return `${major.n}${sub ? `.${sub.n.padStart(0, 3)}` : ''}${unit ? `${major.unit}/s` : ''}`;
}
async function exec(command, args, options) {
const signals = {
SIGINT: 2,
SIGQUIT: 3,
SIGKILL: 9,
SIGTERM: 15,
};
const stdoutChunks = [];
const stderrChunks = [];
const subprocess = spawn(command, args, {
stdio: ["ignore", "pipe", "pipe"],
...options,
});
subprocess.stdout.on("data", chunk => {
stdoutChunks.push(chunk);
});
subprocess.stderr.on("data", chunk => {
stderrChunks.push(chunk);
});
return await new Promise((resolve, reject) => {
subprocess.once("error", err => {
reject(err);
});
subprocess.once("close", (exitCode, signal) => {
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
const stderr = Buffer.concat(stderrChunks).toString("utf8");
let code = exitCode ?? 1;
if (signals[signal]) {
code = signals[signal] + 128;
}
resolve({ code, stdout, stderr });
});
});
}
# It requires HTTP/3-enabled cURL build.
# I used Cloudflare's distribution, you can use your own.
brew install cloudflare/cloudflare/curl
export PATH="$(brew --prefix cloudflare/cloudflare/curl):$PATH"
testing http1.1 to https://www.example.com/ 20 times...
1 times tested, 19 remaining
8 times tested, 12 remaining
16 times tested, 4 remaining
testing http2 to https://www.example.com/ 20 times...
1 times tested, 19 remaining
3 times tested, 17 remaining
8 times tested, 12 remaining
10 times tested, 10 remaining
15 times tested, 5 remaining
17 times tested, 3 remaining
testing http3-only to https://www.example.com/ 20 times...
1 times tested, 19 remaining
3 times tested, 17 remaining
5 times tested, 15 remaining
10 times tested, 10 remaining
12 times tested, 8 remaining
14 times tested, 6 remaining
┌────────────┬─────────────────────────────────────┬───────────────────┬───────────────────┬───────────────────┬──────────────────────────────────┬─────────────────────┬─────────────────────┬─────────────────────┐
│ (index) │ preTransfer │ preTransfer (p50) │ preTransfer (p90) │ preTransfer (p99) │ downloadSpeed │ downloadSpeed (p50) │ downloadSpeed (p90) │ downloadSpeed (p99) │
├────────────┼─────────────────────────────────────┼───────────────────┼───────────────────┼───────────────────┼──────────────────────────────────┼─────────────────────┼─────────────────────┼─────────────────────┤
│ http1.1 │ '460.184µs (432.765µs ~ 478.106µs)' │ '462.949µs' │ '477.113µs' │ '478.106µs' │ '2.05kB/s (1.97kB/s ~ 2.18kB/s)' │ '2.05kB/s' │ '2.14kB/s' │ '2.18kB/s' │
│ http2 │ '483.386µs (435.294µs ~ 613.541µs)' │ '476.104µs' │ '569.276µs' │ '613.541µs' │ '1.95kB/s (1.63kB/s ~ 2.17kB/s)' │ '2kB/s' │ '2.15kB/s' │ '2.17kB/s' │
│ http3-only │ '320.10µs (294.802µs ~ 341.143µs)' │ '326.694µs' │ '339.559µs' │ '341.143µs' │ '2.65kB/s (2.48kB/s ~ 2.86kB/s)' │ '2.64kB/s' │ '2.82kB/s' │ '2.86kB/s' │
└────────────┴─────────────────────────────────────┴───────────────────┴───────────────────┴───────────────────┴──────────────────────────────────┴─────────────────────┴─────────────────────┴─────────────────────┘
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment