aboutsummaryrefslogtreecommitdiff
path: root/benchmark/bench/server-stress.js
diff options
context:
space:
mode:
Diffstat (limited to 'benchmark/bench/server-stress.js')
-rw-r--r--benchmark/bench/server-stress.js115
1 files changed, 115 insertions, 0 deletions
diff --git a/benchmark/bench/server-stress.js b/benchmark/bench/server-stress.js
new file mode 100644
index 000000000..5bcaa6963
--- /dev/null
+++ b/benchmark/bench/server-stress.js
@@ -0,0 +1,115 @@
+import fs from 'node:fs/promises';
+import { fileURLToPath } from 'node:url';
+import autocannon from 'autocannon';
+import { markdownTable } from 'markdown-table';
+import { waitUntilBusy } from 'port-authority';
+import pb from 'pretty-bytes';
+import { exec } from 'tinyexec';
+import { astroBin } from './_util.js';
+
+const port = 4321;
+
+export const defaultProject = 'server-stress-default';
+
+/**
+ * @param {URL} projectDir
+ * @param {URL} outputFile
+ */
+export async function run(projectDir, outputFile) {
+ const root = fileURLToPath(projectDir);
+
+ console.log('Building...');
+ await exec(astroBin, ['build'], {
+ nodeOptions: {
+ cwd: root,
+ stdio: 'inherit',
+ },
+ throwOnError: true,
+ });
+
+ console.log('Previewing...');
+ const previewProcess = await exec(astroBin, ['preview', '--port', port], {
+ nodeOptions: {
+ cwd: root,
+ stdio: 'inherit',
+ },
+ });
+
+ console.log('Waiting for server ready...');
+ await waitUntilBusy(port, { timeout: 5000 });
+
+ console.log('Running benchmark...');
+ const result = await benchmarkCannon();
+
+ console.log('Killing server...');
+ if (!previewProcess.kill('SIGTERM')) {
+ console.warn('Failed to kill server process id:', previewProcess.pid);
+ }
+
+ console.log('Writing results to', fileURLToPath(outputFile));
+ await fs.writeFile(outputFile, JSON.stringify(result, null, 2));
+
+ console.log('Result preview:');
+ console.log('='.repeat(10));
+ console.log(`#### Server stress\n\n`);
+ console.log(printResult(result));
+ console.log('='.repeat(10));
+
+ console.log('Done!');
+}
+
+/**
+ * @returns {Promise<import('autocannon').Result>}
+ */
+export async function benchmarkCannon() {
+ return new Promise((resolve, reject) => {
+ const instance = autocannon(
+ {
+ url: `http://localhost:${port}`,
+ connections: 100,
+ duration: 30,
+ pipelining: 10,
+ },
+ (err, result) => {
+ if (err) {
+ reject(err);
+ } else {
+ // @ts-expect-error untyped but documented
+ instance.stop();
+ resolve(result);
+ }
+ },
+ );
+ autocannon.track(instance, { renderResultsTable: false });
+ });
+}
+
+/**
+ * @param {import('autocannon').Result} output
+ */
+function printResult(output) {
+ const { latency: l, requests: r, throughput: t } = output;
+
+ const latencyTable = markdownTable(
+ [
+ ['', 'Avg', 'Stdev', 'Max'],
+ ['Latency', `${l.average} ms`, `${l.stddev} ms`, `${l.max} ms`],
+ ],
+ {
+ align: ['l', 'r', 'r', 'r'],
+ },
+ );
+
+ const reqAndBytesTable = markdownTable(
+ [
+ ['', 'Avg', 'Stdev', 'Min', 'Total in 30s'],
+ ['Req/Sec', r.average, r.stddev, r.min, `${(r.total / 1000).toFixed(1)}k requests`],
+ ['Bytes/Sec', pb(t.average), pb(t.stddev), pb(t.min), `${pb(t.total)} read`],
+ ],
+ {
+ align: ['l', 'r', 'r', 'r', 'r'],
+ },
+ );
+
+ return `${latencyTable}\n\n${reqAndBytesTable}`;
+}