• 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/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