Last active
April 19, 2024 14:29
-
-
Save cometkim/a3622d373461dddc24c1eb0f39c92543 to your computer and use it in GitHub Desktop.
HTTP protocol version test via cURL (not a good way)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | |
}); | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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