1'use strict'; 2 3const child_process = require('child_process'); 4const path = require('path'); 5const fs = require('fs'); 6 7const requirementsURL = 8 'https://github.com/nodejs/node/blob/HEAD/benchmark/writing-and-running-benchmarks.md#http-benchmark-requirements'; 9 10// The port used by servers and wrk 11exports.PORT = Number(process.env.PORT) || 12346; 12 13class AutocannonBenchmarker { 14 constructor() { 15 this.name = 'autocannon'; 16 this.executable = 17 process.platform === 'win32' ? 'autocannon.cmd' : 'autocannon'; 18 const result = child_process.spawnSync(this.executable, ['-h']); 19 this.present = !(result.error && result.error.code === 'ENOENT'); 20 } 21 22 create(options) { 23 const args = [ 24 '-d', options.duration, 25 '-c', options.connections, 26 '-j', 27 '-n', 28 ]; 29 for (const field in options.headers) { 30 args.push('-H', `${field}=${options.headers[field]}`); 31 } 32 const scheme = options.scheme || 'http'; 33 args.push(`${scheme}://127.0.0.1:${options.port}${options.path}`); 34 const child = child_process.spawn(this.executable, args); 35 return child; 36 } 37 38 processResults(output) { 39 let result; 40 try { 41 result = JSON.parse(output); 42 } catch { 43 return undefined; 44 } 45 if (!result || !result.requests || !result.requests.average) { 46 return undefined; 47 } 48 return result.requests.average; 49 } 50} 51 52class WrkBenchmarker { 53 constructor() { 54 this.name = 'wrk'; 55 this.executable = 'wrk'; 56 const result = child_process.spawnSync(this.executable, ['-h']); 57 this.present = !(result.error && result.error.code === 'ENOENT'); 58 } 59 60 create(options) { 61 const duration = typeof options.duration === 'number' ? 62 Math.max(options.duration, 1) : 63 options.duration; 64 const scheme = options.scheme || 'http'; 65 const args = [ 66 '-d', duration, 67 '-c', options.connections, 68 '-t', Math.min(options.connections, require('os').cpus().length || 8), 69 `${scheme}://127.0.0.1:${options.port}${options.path}`, 70 ]; 71 for (const field in options.headers) { 72 args.push('-H', `${field}: ${options.headers[field]}`); 73 } 74 const child = child_process.spawn(this.executable, args); 75 return child; 76 } 77 78 processResults(output) { 79 const throughputRe = /Requests\/sec:[ \t]+([0-9.]+)/; 80 const match = output.match(throughputRe); 81 const throughput = match && +match[1]; 82 if (!isFinite(throughput)) { 83 return undefined; 84 } 85 return throughput; 86 } 87} 88 89/** 90 * Simple, single-threaded benchmarker for testing if the benchmark 91 * works 92 */ 93class TestDoubleBenchmarker { 94 constructor(type) { 95 // `type` is the type of benchmarker. Possible values are 'http', 'https', 96 // and 'http2'. 97 this.name = `test-double-${type}`; 98 this.executable = path.resolve(__dirname, '_test-double-benchmarker.js'); 99 this.present = fs.existsSync(this.executable); 100 this.type = type; 101 } 102 103 create(options) { 104 process.env.duration = process.env.duration || options.duration || 5; 105 106 const scheme = options.scheme || 'http'; 107 const env = { 108 test_url: `${scheme}://127.0.0.1:${options.port}${options.path}`, 109 ...process.env 110 }; 111 112 const child = child_process.fork(this.executable, 113 [this.type], 114 { silent: true, env }); 115 return child; 116 } 117 118 processResults(output) { 119 let result; 120 try { 121 result = JSON.parse(output); 122 } catch { 123 return undefined; 124 } 125 return result.throughput; 126 } 127} 128 129/** 130 * HTTP/2 Benchmarker 131 */ 132class H2LoadBenchmarker { 133 constructor() { 134 this.name = 'h2load'; 135 this.executable = 'h2load'; 136 const result = child_process.spawnSync(this.executable, ['-h']); 137 this.present = !(result.error && result.error.code === 'ENOENT'); 138 } 139 140 create(options) { 141 const args = []; 142 if (typeof options.requests === 'number') 143 args.push('-n', options.requests); 144 if (typeof options.clients === 'number') 145 args.push('-c', options.clients); 146 if (typeof options.threads === 'number') 147 args.push('-t', options.threads); 148 if (typeof options.maxConcurrentStreams === 'number') 149 args.push('-m', options.maxConcurrentStreams); 150 if (typeof options.initialWindowSize === 'number') 151 args.push('-w', options.initialWindowSize); 152 if (typeof options.sessionInitialWindowSize === 'number') 153 args.push('-W', options.sessionInitialWindowSize); 154 if (typeof options.rate === 'number') 155 args.push('-r', options.rate); 156 if (typeof options.ratePeriod === 'number') 157 args.push(`--rate-period=${options.ratePeriod}`); 158 if (typeof options.duration === 'number') 159 args.push('-T', options.duration); 160 if (typeof options.timeout === 'number') 161 args.push('-N', options.timeout); 162 if (typeof options.headerTableSize === 'number') 163 args.push(`--header-table-size=${options.headerTableSize}`); 164 if (typeof options.encoderHeaderTableSize === 'number') { 165 args.push( 166 `--encoder-header-table-size=${options.encoderHeaderTableSize}`); 167 } 168 const scheme = options.scheme || 'http'; 169 const host = options.host || '127.0.0.1'; 170 args.push(`${scheme}://${host}:${options.port}${options.path}`); 171 const child = child_process.spawn(this.executable, args); 172 return child; 173 } 174 175 processResults(output) { 176 const rex = /(\d+(?:\.\d+)) req\/s/; 177 return rex.exec(output)[1]; 178 } 179} 180 181const http_benchmarkers = [ 182 new WrkBenchmarker(), 183 new AutocannonBenchmarker(), 184 new TestDoubleBenchmarker('http'), 185 new TestDoubleBenchmarker('https'), 186 new TestDoubleBenchmarker('http2'), 187 new H2LoadBenchmarker(), 188]; 189 190const benchmarkers = {}; 191 192http_benchmarkers.forEach((benchmarker) => { 193 benchmarkers[benchmarker.name] = benchmarker; 194 if (!exports.default_http_benchmarker && benchmarker.present) { 195 exports.default_http_benchmarker = benchmarker.name; 196 } 197}); 198 199exports.run = function(options, callback) { 200 options = { 201 port: exports.PORT, 202 path: '/', 203 connections: 100, 204 duration: 5, 205 benchmarker: exports.default_http_benchmarker, 206 ...options 207 }; 208 if (!options.benchmarker) { 209 callback(new Error('Could not locate required http benchmarker. See ' + 210 `${requirementsURL} for further instructions.`)); 211 return; 212 } 213 const benchmarker = benchmarkers[options.benchmarker]; 214 if (!benchmarker) { 215 callback(new Error(`Requested benchmarker '${options.benchmarker}' ` + 216 'is not supported')); 217 return; 218 } 219 if (!benchmarker.present) { 220 callback(new Error(`Requested benchmarker '${options.benchmarker}' ` + 221 'is not installed')); 222 return; 223 } 224 225 const benchmarker_start = process.hrtime.bigint(); 226 227 const child = benchmarker.create(options); 228 229 child.stderr.pipe(process.stderr); 230 231 let stdout = ''; 232 child.stdout.setEncoding('utf8'); 233 child.stdout.on('data', (chunk) => stdout += chunk); 234 235 child.once('close', (code) => { 236 const benchmark_end = process.hrtime.bigint(); 237 if (code) { 238 let error_message = `${options.benchmarker} failed with ${code}.`; 239 if (stdout !== '') { 240 error_message += ` Output: ${stdout}`; 241 } 242 callback(new Error(error_message), code); 243 return; 244 } 245 246 const result = benchmarker.processResults(stdout); 247 if (result === undefined) { 248 callback(new Error( 249 `${options.benchmarker} produced strange output: ${stdout}`), code); 250 return; 251 } 252 253 const elapsed = benchmark_end - benchmarker_start; 254 callback(null, code, options.benchmarker, result, elapsed); 255 }); 256 257}; 258