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