server-stress.js 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import autocannon from 'autocannon';
  2. import { execaCommand } from 'execa';
  3. import { markdownTable } from 'markdown-table';
  4. import fs from 'node:fs/promises';
  5. import { fileURLToPath } from 'node:url';
  6. import { waitUntilBusy } from 'port-authority';
  7. import pb from 'pretty-bytes';
  8. import { astroBin } from './_util.js';
  9. const port = 4321;
  10. export const defaultProject = 'server-stress-default';
  11. /**
  12. * @param {URL} projectDir
  13. * @param {URL} outputFile
  14. */
  15. export async function run(projectDir, outputFile) {
  16. const root = fileURLToPath(projectDir);
  17. console.log('Building...');
  18. await execaCommand(`${astroBin} build`, {
  19. cwd: root,
  20. stdio: 'inherit',
  21. });
  22. console.log('Previewing...');
  23. const previewProcess = execaCommand(`${astroBin} preview --port ${port}`, {
  24. cwd: root,
  25. stdio: 'inherit',
  26. });
  27. console.log('Waiting for server ready...');
  28. await waitUntilBusy(port, { timeout: 5000 });
  29. console.log('Running benchmark...');
  30. const result = await benchmarkCannon();
  31. console.log('Killing server...');
  32. if (!previewProcess.kill('SIGTERM')) {
  33. console.warn('Failed to kill server process id:', previewProcess.pid);
  34. }
  35. console.log('Writing results to', fileURLToPath(outputFile));
  36. await fs.writeFile(outputFile, JSON.stringify(result, null, 2));
  37. console.log('Result preview:');
  38. console.log('='.repeat(10));
  39. console.log(`#### Server stress\n\n`);
  40. console.log(printResult(result));
  41. console.log('='.repeat(10));
  42. console.log('Done!');
  43. }
  44. /**
  45. * @returns {Promise<import('autocannon').Result>}
  46. */
  47. async function benchmarkCannon() {
  48. return new Promise((resolve, reject) => {
  49. const instance = autocannon(
  50. {
  51. url: `http://localhost:${port}`,
  52. connections: 100,
  53. duration: 30,
  54. pipelining: 10,
  55. },
  56. (err, result) => {
  57. if (err) {
  58. reject(err);
  59. } else {
  60. // @ts-expect-error untyped but documented
  61. instance.stop();
  62. resolve(result);
  63. }
  64. }
  65. );
  66. autocannon.track(instance, { renderResultsTable: false });
  67. });
  68. }
  69. /**
  70. * @param {import('autocannon').Result} output
  71. */
  72. function printResult(output) {
  73. const { latency: l, requests: r, throughput: t } = output;
  74. const latencyTable = markdownTable(
  75. [
  76. ['', 'Avg', 'Stdev', 'Max'],
  77. ['Latency', `${l.average} ms`, `${l.stddev} ms`, `${l.max} ms`],
  78. ],
  79. {
  80. align: ['l', 'r', 'r', 'r'],
  81. }
  82. );
  83. const reqAndBytesTable = markdownTable(
  84. [
  85. ['', 'Avg', 'Stdev', 'Min', 'Total in 30s'],
  86. ['Req/Sec', r.average, r.stddev, r.min, `${(r.total / 1000).toFixed(1)}k requests`],
  87. ['Bytes/Sec', pb(t.average), pb(t.stddev), pb(t.min), `${pb(t.total)} read`],
  88. ],
  89. {
  90. align: ['l', 'r', 'r', 'r', 'r'],
  91. }
  92. );
  93. return `${latencyTable}\n\n${reqAndBytesTable}`;
  94. }