2
0

benchmark-builds.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. #!/usr/bin/env node
  2. import { spawnSync } from 'node:child_process'
  3. const args = process.argv.slice(2)
  4. const runsArgIndex = args.findIndex(arg => arg === '--runs' || arg === '-n')
  5. const warmupArgIndex = args.findIndex(arg => arg === '--warmup' || arg === '-w')
  6. const runs =
  7. runsArgIndex >= 0
  8. ? Number(args[runsArgIndex + 1])
  9. : Number(process.env.RUNS || 10)
  10. const warmup =
  11. warmupArgIndex >= 0
  12. ? Number(args[warmupArgIndex + 1])
  13. : Number(process.env.WARMUP || 5)
  14. const quiet = args.includes('--quiet')
  15. if (!Number.isFinite(runs) || runs <= 0) {
  16. console.error('Invalid runs value. Use --runs <number> or set RUNS.')
  17. process.exit(1)
  18. }
  19. if (!Number.isFinite(warmup) || warmup < 0) {
  20. console.error('Invalid warmup value. Use --warmup <number> or set WARMUP.')
  21. process.exit(1)
  22. }
  23. const cases = [
  24. {
  25. title: 'build vs build-rollup',
  26. a: {
  27. label: 'build',
  28. cmd: 'node',
  29. args: ['scripts/build.js'],
  30. },
  31. b: {
  32. label: 'build-rollup',
  33. cmd: 'node',
  34. args: ['scripts/build-with-rollup.js'],
  35. },
  36. },
  37. {
  38. title: 'build-dts vs build-dts-tsc',
  39. a: { label: 'build-dts', cmd: 'node', args: ['scripts/build-types.js'] },
  40. b: {
  41. label: 'build-dts-tsc',
  42. cmd: 'node',
  43. args: ['scripts/build-dts-tsc.js'],
  44. },
  45. },
  46. ]
  47. function extractBuiltInMs(output) {
  48. const cleaned = output.replace(/\u001b\[[0-9;]*m/g, '').replace(/\s+/g, ' ')
  49. const matches = [...cleaned.matchAll(/built in\s+([\d.]+)ms/gi)]
  50. if (!matches.length) return null
  51. return matches.reduce((sum, match) => sum + Number(match[1]), 0)
  52. }
  53. function runOnce(command, commandArgs, label, index) {
  54. const result = spawnSync(command, commandArgs, {
  55. encoding: 'utf8',
  56. stdio: 'pipe',
  57. shell: false,
  58. })
  59. if (!quiet) {
  60. if (result.stdout) process.stdout.write(result.stdout)
  61. if (result.stderr) process.stderr.write(result.stderr)
  62. }
  63. if (result.error) throw result.error
  64. if (result.status !== 0) {
  65. throw new Error(
  66. `${label} failed with exit code ${result.status} on run ${index + 1}`,
  67. )
  68. }
  69. const output = `${result.stdout ?? ''}${result.stderr ?? ''}`
  70. const builtInMs = extractBuiltInMs(output)
  71. if (!Number.isFinite(builtInMs)) {
  72. const lines = output.trim().split(/\r?\n/)
  73. const tail = lines.slice(-20).join('\n')
  74. throw new Error(
  75. `${label} did not emit a "built in <ms>" timing line on run ${
  76. index + 1
  77. }.\nLast output:\n${tail}`,
  78. )
  79. }
  80. return builtInMs
  81. }
  82. function summarize(times) {
  83. const sorted = [...times].sort((a, b) => a - b)
  84. const sum = times.reduce((acc, t) => acc + t, 0)
  85. const avg = sum / times.length
  86. const min = sorted[0]
  87. const max = sorted[sorted.length - 1]
  88. const mid = Math.floor(sorted.length / 2)
  89. const median =
  90. sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]
  91. const variance =
  92. times.reduce((acc, t) => acc + (t - avg) ** 2, 0) / times.length
  93. const stddev = Math.sqrt(variance)
  94. return { avg, min, max, median, stddev }
  95. }
  96. function formatMs(ms) {
  97. return `${(ms / 1000).toFixed(2)}s`
  98. }
  99. function buildTable(rows) {
  100. const widths = rows[0].map((_, col) =>
  101. Math.max(...rows.map(row => String(row[col]).length)),
  102. )
  103. const formatRow = row =>
  104. `| ${row.map((cell, i) => String(cell).padEnd(widths[i], ' ')).join(' | ')} |`
  105. const separator = `| ${widths.map(w => '-'.repeat(w)).join(' | ')} |`
  106. return [formatRow(rows[0]), separator, ...rows.slice(1).map(formatRow)].join(
  107. '\n',
  108. )
  109. }
  110. function renderStatsTable(labelA, labelB, statsA, statsB) {
  111. const diff = statsB.avg - statsA.avg
  112. const ratio = statsB.avg / statsA.avg
  113. const sign = diff >= 0 ? '+' : '-'
  114. const table = buildTable([
  115. ['metric', labelA, labelB],
  116. ['avg', formatMs(statsA.avg), formatMs(statsB.avg)],
  117. ['median', formatMs(statsA.median), formatMs(statsB.median)],
  118. ['min', formatMs(statsA.min), formatMs(statsB.min)],
  119. ['max', formatMs(statsA.max), formatMs(statsB.max)],
  120. ['stddev', formatMs(statsA.stddev), formatMs(statsB.stddev)],
  121. ['avg diff', '-', `${sign}${formatMs(Math.abs(diff))}`],
  122. ['avg ratio', '-', `${ratio.toFixed(2)}x`],
  123. ])
  124. console.log(table)
  125. }
  126. for (const testCase of cases) {
  127. console.log(`\n== ${testCase.title} ==`)
  128. const timesA = []
  129. const timesB = []
  130. if (warmup > 0) {
  131. console.log(`\nWarmup x${warmup} - ${testCase.a.label}`)
  132. for (let i = 0; i < warmup; i += 1) {
  133. runOnce(testCase.a.cmd, testCase.a.args, testCase.a.label, i)
  134. }
  135. console.log(`\nWarmup x${warmup} - ${testCase.b.label}`)
  136. for (let i = 0; i < warmup; i += 1) {
  137. runOnce(testCase.b.cmd, testCase.b.args, testCase.b.label, i)
  138. }
  139. }
  140. for (let i = 0; i < runs; i += 1) {
  141. console.log(`\nRun ${i + 1}/${runs} - ${testCase.a.label}`)
  142. const timeA = runOnce(testCase.a.cmd, testCase.a.args, testCase.a.label, i)
  143. timesA.push(timeA)
  144. console.log(`time: ${formatMs(timeA)}`)
  145. }
  146. for (let i = 0; i < runs; i += 1) {
  147. console.log(`\nRun ${i + 1}/${runs} - ${testCase.b.label}`)
  148. const timeB = runOnce(testCase.b.cmd, testCase.b.args, testCase.b.label, i)
  149. timesB.push(timeB)
  150. console.log(`time: ${formatMs(timeB)}`)
  151. }
  152. const statsA = summarize(timesA)
  153. const statsB = summarize(timesB)
  154. console.log('')
  155. renderStatsTable(testCase.a.label, testCase.b.label, statsA, statsB)
  156. }
  157. console.log('\nDone.')