built-worker.ts 3.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. /** Plain-Node launcher for compiled benchmark workers. */
  2. import { spawn } from 'node:child_process'
  3. /** Process outcome and optional JSON report from one compiled benchmark worker. */
  4. export interface BuiltBenchmarkWorkerRun<Report> {
  5. readonly report: Report | undefined
  6. readonly exitCode: number | null
  7. readonly signal: NodeJS.Signals | null
  8. readonly timedOut: boolean
  9. readonly stderr: string
  10. }
  11. /** Options for one isolated compiled benchmark process. */
  12. export interface BuiltBenchmarkWorkerOptions {
  13. readonly worker: string
  14. readonly args?: readonly string[]
  15. readonly timeoutMs: number
  16. readonly exposeGc?: boolean
  17. readonly heapLimitMb?: number
  18. }
  19. /**
  20. * Run one built JavaScript worker without a TypeScript runtime loader.
  21. * @param options - worker path, arguments, deadline, and optional V8 limits.
  22. * @returns child exit details and its final JSON-line report when successful.
  23. */
  24. export function runBuiltBenchmarkWorker<Report>(
  25. options: BuiltBenchmarkWorkerOptions,
  26. ): Promise<BuiltBenchmarkWorkerRun<Report>> {
  27. if (!options.worker.endsWith('.js') && !options.worker.endsWith('.cjs')) {
  28. throw new Error(`benchmark worker must be compiled JavaScript: ${options.worker}`)
  29. }
  30. const env = { ...process.env }
  31. delete env['NODE_OPTIONS']
  32. delete env['TSX_TSCONFIG_PATH']
  33. return new Promise((resolve, reject) => {
  34. const child = spawn(process.execPath, [
  35. ...options.exposeGc === true ? ['--expose-gc'] : [],
  36. ...options.heapLimitMb === undefined
  37. ? []
  38. : [`--max-old-space-size=${String(options.heapLimitMb)}`],
  39. options.worker,
  40. ...options.args ?? [],
  41. ], {
  42. cwd: process.cwd(),
  43. env,
  44. stdio: ['ignore', 'pipe', 'pipe'],
  45. })
  46. let stdout = ''
  47. let stderr = ''
  48. let timedOut = false
  49. const timeout = setTimeout(() => {
  50. timedOut = true
  51. child.kill('SIGKILL')
  52. }, options.timeoutMs)
  53. child.stdout.setEncoding('utf8').on('data', (chunk: string) => { stdout += chunk })
  54. child.stderr.setEncoding('utf8').on('data', (chunk: string) => { stderr += chunk })
  55. child.once('error', (error) => {
  56. clearTimeout(timeout)
  57. reject(error)
  58. })
  59. child.once('close', (exitCode, signal) => {
  60. clearTimeout(timeout)
  61. const line = stdout.trim().split('\n').findLast(candidate => candidate.startsWith('{'))
  62. try {
  63. const report = exitCode === 0 && line !== undefined
  64. ? JSON.parse(line) as Report
  65. : undefined
  66. resolve({ report, exitCode, signal, timedOut, stderr })
  67. } catch (error: unknown) {
  68. reject(error)
  69. }
  70. })
  71. })
  72. }
  73. /**
  74. * Reject a benchmark worker reached through source execution or a TypeScript loader.
  75. * @param moduleUrl - `import.meta.url` from the worker entry.
  76. * @param packageEntries - resolved production package entries used by the measured path.
  77. */
  78. export function assertBuiltBenchmarkRuntime(
  79. moduleUrl: string,
  80. packageEntries: Readonly<Record<string, string>>,
  81. ): void {
  82. if (!moduleUrl.endsWith('.js') || !moduleUrl.includes('/benchmarks/.dsh-build/')) {
  83. throw new Error(`benchmark worker is not running from benchmarks/.dsh-build: ${moduleUrl}`)
  84. }
  85. const tsRuntime = process.execArgv.find(argument => /(?:^|[/\\])tsx(?:[/\\]|$)|tsx\/esm|tsx\/cjs/.test(argument))
  86. if (tsRuntime !== undefined) throw new Error(`benchmark worker received a TypeScript loader: ${tsRuntime}`)
  87. for (const [specifier, entry] of Object.entries(packageEntries)) {
  88. if (!/\/lib\/(?:[^/]+\/)*[^/]+\.js$/.test(entry)) {
  89. throw new Error(`benchmark package ${specifier} did not resolve to lib JavaScript: ${entry}`)
  90. }
  91. }
  92. }