• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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