• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const child_process = require('child_process');
4const http_benchmarkers = require('./_http-benchmarkers.js');
5
6class Benchmark {
7  constructor(fn, configs, options = {}) {
8    // Used to make sure a benchmark only start a timer once
9    this._started = false;
10
11    // Indicate that the benchmark ended
12    this._ended = false;
13
14    // Holds process.hrtime value
15    this._time = 0n;
16
17    // Use the file name as the name of the benchmark
18    this.name = require.main.filename.slice(__dirname.length + 1);
19
20    // Execution arguments i.e. flags used to run the jobs
21    this.flags = process.env.NODE_BENCHMARK_FLAGS ?
22      process.env.NODE_BENCHMARK_FLAGS.split(/\s+/) :
23      [];
24
25    // Parse job-specific configuration from the command line arguments
26    const argv = process.argv.slice(2);
27    const parsed_args = this._parseArgs(argv, configs, options);
28    this.options = parsed_args.cli;
29    this.extra_options = parsed_args.extra;
30    if (options.flags) {
31      this.flags = this.flags.concat(options.flags);
32    }
33
34    // The configuration list as a queue of jobs
35    this.queue = this._queue(this.options);
36
37    // The configuration of the current job, head of the queue
38    this.config = this.queue[0];
39
40    process.nextTick(() => {
41      if (process.env.hasOwnProperty('NODE_RUN_BENCHMARK_FN')) {
42        fn(this.config);
43      } else {
44        // _run will use fork() to create a new process for each configuration
45        // combination.
46        this._run();
47      }
48    });
49  }
50
51  _parseArgs(argv, configs, options) {
52    const cliOptions = {};
53
54    // Check for the test mode first.
55    const testIndex = argv.indexOf('--test');
56    if (testIndex !== -1) {
57      for (const [key, rawValue] of Object.entries(configs)) {
58        let value = Array.isArray(rawValue) ? rawValue[0] : rawValue;
59        // Set numbers to one by default to reduce the runtime.
60        if (typeof value === 'number') {
61          if (key === 'dur' || key === 'duration') {
62            value = 0.05;
63          } else if (value > 1) {
64            value = 1;
65          }
66        }
67        cliOptions[key] = [value];
68      }
69      // Override specific test options.
70      if (options.test) {
71        for (const [key, value] of Object.entries(options.test)) {
72          cliOptions[key] = Array.isArray(value) ? value : [value];
73        }
74      }
75      argv.splice(testIndex, 1);
76    } else {
77      // Accept single values instead of arrays.
78      for (const [key, value] of Object.entries(configs)) {
79        if (!Array.isArray(value))
80          configs[key] = [value];
81      }
82    }
83
84    const extraOptions = {};
85    const validArgRE = /^(.+?)=([\s\S]*)$/;
86    // Parse configuration arguments
87    for (const arg of argv) {
88      const match = arg.match(validArgRE);
89      if (!match) {
90        console.error(`bad argument: ${arg}`);
91        process.exit(1);
92      }
93      const [, key, value] = match;
94      if (Object.prototype.hasOwnProperty.call(configs, key)) {
95        if (!cliOptions[key])
96          cliOptions[key] = [];
97        cliOptions[key].push(
98          // Infer the type from the config object and parse accordingly
99          typeof configs[key][0] === 'number' ? +value : value
100        );
101      } else {
102        extraOptions[key] = value;
103      }
104    }
105    return { cli: { ...configs, ...cliOptions }, extra: extraOptions };
106  }
107
108  _queue(options) {
109    const queue = [];
110    const keys = Object.keys(options);
111
112    // Perform a depth-first walk through all options to generate a
113    // configuration list that contains all combinations.
114    function recursive(keyIndex, prevConfig) {
115      const key = keys[keyIndex];
116      const values = options[key];
117
118      for (const value of values) {
119        if (typeof value !== 'number' && typeof value !== 'string') {
120          throw new TypeError(
121            `configuration "${key}" had type ${typeof value}`);
122        }
123        if (typeof value !== typeof values[0]) {
124          // This is a requirement for being able to consistently and
125          // predictably parse CLI provided configuration values.
126          throw new TypeError(`configuration "${key}" has mixed types`);
127        }
128
129        const currConfig = { [key]: value, ...prevConfig };
130
131        if (keyIndex + 1 < keys.length) {
132          recursive(keyIndex + 1, currConfig);
133        } else {
134          queue.push(currConfig);
135        }
136      }
137    }
138
139    if (keys.length > 0) {
140      recursive(0, {});
141    } else {
142      queue.push({});
143    }
144
145    return queue;
146  }
147
148  http(options, cb) {
149    const http_options = { ...options };
150    http_options.benchmarker = http_options.benchmarker ||
151                               this.config.benchmarker ||
152                               this.extra_options.benchmarker ||
153                               http_benchmarkers.default_http_benchmarker;
154    http_benchmarkers.run(
155      http_options, (error, code, used_benchmarker, result, elapsed) => {
156        if (cb) {
157          cb(code);
158        }
159        if (error) {
160          console.error(error);
161          process.exit(code || 1);
162        }
163        this.config.benchmarker = used_benchmarker;
164        this.report(result, elapsed);
165      }
166    );
167  }
168
169  _run() {
170    // If forked, report to the parent.
171    if (process.send) {
172      process.send({
173        type: 'config',
174        name: this.name,
175        queueLength: this.queue.length,
176      });
177    }
178
179    const recursive = (queueIndex) => {
180      const config = this.queue[queueIndex];
181
182      // Set NODE_RUN_BENCHMARK_FN to indicate that the child shouldn't
183      // construct a configuration queue, but just execute the benchmark
184      // function.
185      const childEnv = { ...process.env };
186      childEnv.NODE_RUN_BENCHMARK_FN = '';
187
188      // Create configuration arguments
189      const childArgs = [];
190      for (const [key, value] of Object.entries(config)) {
191        childArgs.push(`${key}=${value}`);
192      }
193      for (const [key, value] of Object.entries(this.extra_options)) {
194        childArgs.push(`${key}=${value}`);
195      }
196
197      const child = child_process.fork(require.main.filename, childArgs, {
198        env: childEnv,
199        execArgv: this.flags.concat(process.execArgv),
200      });
201      child.on('message', sendResult);
202      child.on('close', (code) => {
203        if (code) {
204          process.exit(code);
205        }
206
207        if (queueIndex + 1 < this.queue.length) {
208          recursive(queueIndex + 1);
209        }
210      });
211    };
212
213    recursive(0);
214  }
215
216  start() {
217    if (this._started) {
218      throw new Error('Called start more than once in a single benchmark');
219    }
220    this._started = true;
221    this._time = process.hrtime.bigint();
222  }
223
224  end(operations) {
225    // Get elapsed time now and do error checking later for accuracy.
226    const time = process.hrtime.bigint();
227
228    if (!this._started) {
229      throw new Error('called end without start');
230    }
231    if (this._ended) {
232      throw new Error('called end multiple times');
233    }
234    if (typeof operations !== 'number') {
235      throw new Error('called end() without specifying operation count');
236    }
237    if (!process.env.NODEJS_BENCHMARK_ZERO_ALLOWED && operations <= 0) {
238      throw new Error('called end() with operation count <= 0');
239    }
240
241    this._ended = true;
242
243    if (time === this._time) {
244      if (!process.env.NODEJS_BENCHMARK_ZERO_ALLOWED)
245        throw new Error('insufficient clock precision for short benchmark');
246      // Avoid dividing by zero
247      this.report(operations && Number.MAX_VALUE, 0n);
248      return;
249    }
250
251    const elapsed = time - this._time;
252    const rate = operations / (Number(elapsed) / 1e9);
253    this.report(rate, elapsed);
254  }
255
256  report(rate, elapsed) {
257    sendResult({
258      name: this.name,
259      conf: this.config,
260      rate,
261      time: nanoSecondsToString(elapsed),
262      type: 'report',
263    });
264  }
265}
266
267function nanoSecondsToString(bigint) {
268  const str = bigint.toString();
269  const decimalPointIndex = str.length - 9;
270  if (decimalPointIndex <= 0) {
271    return `0.${'0'.repeat(-decimalPointIndex)}${str}`;
272  }
273  return `${str.slice(0, decimalPointIndex)}.${str.slice(decimalPointIndex)}`;
274}
275
276function formatResult(data) {
277  // Construct configuration string, " A=a, B=b, ..."
278  let conf = '';
279  for (const key of Object.keys(data.conf)) {
280    conf += ` ${key}=${JSON.stringify(data.conf[key])}`;
281  }
282
283  let rate = data.rate.toString().split('.');
284  rate[0] = rate[0].replace(/(\d)(?=(?:\d\d\d)+(?!\d))/g, '$1,');
285  rate = (rate[1] ? rate.join('.') : rate[0]);
286  return `${data.name}${conf}: ${rate}\n`;
287}
288
289function sendResult(data) {
290  if (process.send) {
291    // If forked, report by process send
292    process.send(data);
293  } else {
294    // Otherwise report by stdout
295    process.stdout.write(formatResult(data));
296  }
297}
298
299const urls = {
300  long: 'http://nodejs.org:89/docs/latest/api/foo/bar/qua/13949281/0f28b/' +
301        '/5d49/b3020/url.html#test?payload1=true&payload2=false&test=1' +
302        '&benchmark=3&foo=38.38.011.293&bar=1234834910480&test=19299&3992&' +
303        'key=f5c65e1e98fe07e648249ad41e1cfdb0',
304  short: 'https://nodejs.org/en/blog/',
305  idn: 'http://你好你好.在线',
306  auth: 'https://user:pass@example.com/path?search=1',
307  file: 'file:///foo/bar/test/node.js',
308  ws: 'ws://localhost:9229/f46db715-70df-43ad-a359-7f9949f39868',
309  javascript: 'javascript:alert("node is awesome");',
310  percent: 'https://%E4%BD%A0/foo',
311  dot: 'https://example.org/./a/../b/./c',
312};
313
314const searchParams = {
315  noencode: 'foo=bar&baz=quux&xyzzy=thud',
316  multicharsep: 'foo=bar&&&&&&&&&&baz=quux&&&&&&&&&&xyzzy=thud',
317  encodefake: 'foo=%©ar&baz=%A©uux&xyzzy=%©ud',
318  encodemany: '%66%6F%6F=bar&%62%61%7A=quux&xyzzy=%74h%75d',
319  encodelast: 'foo=bar&baz=quux&xyzzy=thu%64',
320  multivalue: 'foo=bar&foo=baz&foo=quux&quuy=quuz',
321  multivaluemany: 'foo=bar&foo=baz&foo=quux&quuy=quuz&foo=abc&foo=def&' +
322                  'foo=ghi&foo=jkl&foo=mno&foo=pqr&foo=stu&foo=vwxyz',
323  manypairs: 'a&b&c&d&e&f&g&h&i&j&k&l&m&n&o&p&q&r&s&t&u&v&w&x&y&z',
324  manyblankpairs: '&&&&&&&&&&&&&&&&&&&&&&&&',
325  altspaces: 'foo+bar=baz+quux&xyzzy+thud=quuy+quuz&abc=def+ghi',
326};
327
328function getUrlData(withBase) {
329  const data = require('../test/fixtures/wpt/url/resources/urltestdata.json');
330  const result = [];
331  for (const item of data) {
332    if (item.failure || !item.input) continue;
333    if (withBase) {
334      result.push([item.input, item.base]);
335    } else if (item.base !== 'about:blank') {
336      result.push(item.base);
337    }
338  }
339  return result;
340}
341
342/**
343 * Generate an array of data for URL benchmarks to use.
344 * The size of the resulting data set is the original data size * 2 ** `e`.
345 * The 'wpt' type contains about 400 data points when `withBase` is true,
346 * and 200 data points when `withBase` is false.
347 * Other types contain 200 data points with or without base.
348 *
349 * @param {string} type Type of the data, 'wpt' or a key of `urls`
350 * @param {number} e The repetition of the data, as exponent of 2
351 * @param {boolean} withBase Whether to include a base URL
352 * @param {boolean} asUrl Whether to return the results as URL objects
353 * @return {string[] | string[][] | URL[]}
354 */
355function bakeUrlData(type, e = 0, withBase = false, asUrl = false) {
356  let result = [];
357  if (type === 'wpt') {
358    result = getUrlData(withBase);
359  } else if (urls[type]) {
360    const input = urls[type];
361    const item = withBase ? [input, 'about:blank'] : input;
362    // Roughly the size of WPT URL test data
363    result = new Array(200).fill(item);
364  } else {
365    throw new Error(`Unknown url data type ${type}`);
366  }
367
368  if (typeof e !== 'number') {
369    throw new Error(`e must be a number, received ${e}`);
370  }
371
372  for (let i = 0; i < e; ++i) {
373    result = result.concat(result);
374  }
375
376  if (asUrl) {
377    if (withBase) {
378      result = result.map(([input, base]) => new URL(input, base));
379    } else {
380      result = result.map((input) => new URL(input));
381    }
382  }
383  return result;
384}
385
386module.exports = {
387  Benchmark,
388  PORT: http_benchmarkers.PORT,
389  bakeUrlData,
390  binding(bindingName) {
391    try {
392      const { internalBinding } = require('internal/test/binding');
393
394      return internalBinding(bindingName);
395    } catch {
396      return process.binding(bindingName);
397    }
398  },
399  buildType: process.features.debug ? 'Debug' : 'Release',
400  createBenchmark(fn, configs, options) {
401    return new Benchmark(fn, configs, options);
402  },
403  sendResult,
404  searchParams,
405  urlDataTypes: Object.keys(urls).concat(['wpt']),
406  urls,
407};
408