render.js 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import { execaCommand } from 'execa';
  2. import { markdownTable } from 'markdown-table';
  3. import fs from 'node:fs/promises';
  4. import http from 'node:http';
  5. import path from 'node:path';
  6. import { fileURLToPath } from 'node:url';
  7. import { waitUntilBusy } from 'port-authority';
  8. import { calculateStat, astroBin } from './_util.js';
  9. import { renderPages } from '../make-project/render-default.js';
  10. const port = 4322;
  11. export const defaultProject = 'render-default';
  12. /**
  13. * @param {URL} projectDir
  14. * @param {URL} outputFile
  15. */
  16. export async function run(projectDir, outputFile) {
  17. const root = fileURLToPath(projectDir);
  18. console.log('Building...');
  19. await execaCommand(`${astroBin} build`, {
  20. cwd: root,
  21. stdio: 'inherit',
  22. });
  23. console.log('Previewing...');
  24. const previewProcess = execaCommand(`${astroBin} preview --port ${port}`, {
  25. cwd: root,
  26. stdio: 'inherit',
  27. });
  28. console.log('Waiting for server ready...');
  29. await waitUntilBusy(port, { timeout: 5000 });
  30. console.log('Running benchmark...');
  31. const result = await benchmarkRenderTime();
  32. console.log('Killing server...');
  33. if (!previewProcess.kill('SIGTERM')) {
  34. console.warn('Failed to kill server process id:', previewProcess.pid);
  35. }
  36. console.log('Writing results to', fileURLToPath(outputFile));
  37. await fs.writeFile(outputFile, JSON.stringify(result, null, 2));
  38. console.log('Result preview:');
  39. console.log('='.repeat(10));
  40. console.log(`#### Render\n\n`);
  41. console.log(printResult(result));
  42. console.log('='.repeat(10));
  43. console.log('Done!');
  44. }
  45. async function benchmarkRenderTime() {
  46. /** @type {Record<string, number[]>} */
  47. const result = {};
  48. for (const fileName of renderPages) {
  49. // Render each file 100 times and push to an array
  50. for (let i = 0; i < 100; i++) {
  51. const pathname = '/' + fileName.slice(0, -path.extname(fileName).length);
  52. const renderTime = await fetchRenderTime(`http://localhost:${port}${pathname}`);
  53. if (!result[pathname]) result[pathname] = [];
  54. result[pathname].push(renderTime);
  55. }
  56. }
  57. /** @type {Record<string, import('./_util.js').Stat>} */
  58. const processedResult = {};
  59. for (const [pathname, times] of Object.entries(result)) {
  60. // From the 100 results, calculate average, standard deviation, and max value
  61. processedResult[pathname] = calculateStat(times);
  62. }
  63. return processedResult;
  64. }
  65. /**
  66. * @param {Record<string, import('./_util.js').Stat>} result
  67. */
  68. function printResult(result) {
  69. return markdownTable(
  70. [
  71. ['Page', 'Avg (ms)', 'Stdev (ms)', 'Max (ms)'],
  72. ...Object.entries(result).map(([pathname, { avg, stdev, max }]) => [
  73. pathname,
  74. avg.toFixed(2),
  75. stdev.toFixed(2),
  76. max.toFixed(2),
  77. ]),
  78. ],
  79. {
  80. align: ['l', 'r', 'r', 'r'],
  81. }
  82. );
  83. }
  84. /**
  85. * Simple fetch utility to get the render time sent by `@benchmark/timer` in plain text
  86. * @param {string} url
  87. * @returns {Promise<number>}
  88. */
  89. function fetchRenderTime(url) {
  90. return new Promise((resolve, reject) => {
  91. const req = http.request(url, (res) => {
  92. res.setEncoding('utf8');
  93. let data = '';
  94. res.on('data', (chunk) => (data += chunk));
  95. res.on('error', (e) => reject(e));
  96. res.on('end', () => resolve(+data));
  97. });
  98. req.on('error', (e) => reject(e));
  99. req.end();
  100. });
  101. }