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