mirror of
https://github.com/nodejs/node.git
synced 2024-11-24 12:10:08 +01:00
71785889c8
PR-URL: https://github.com/nodejs/node/pull/55044 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: LiviaMedeiros <livia@cirno.name>
444 lines
13 KiB
JavaScript
444 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
const child_process = require('child_process');
|
|
const http_benchmarkers = require('./_http-benchmarkers.js');
|
|
|
|
function allow() {
|
|
return true;
|
|
}
|
|
|
|
class Benchmark {
|
|
constructor(fn, configs, options = {}) {
|
|
// Used to make sure a benchmark only start a timer once
|
|
this._started = false;
|
|
|
|
// Indicate that the benchmark ended
|
|
this._ended = false;
|
|
|
|
// Holds process.hrtime value
|
|
this._time = 0n;
|
|
|
|
// Use the file name as the name of the benchmark
|
|
this.name = require.main.filename.slice(__dirname.length + 1);
|
|
|
|
// Execution arguments i.e. flags used to run the jobs
|
|
this.flags = process.env.NODE_BENCHMARK_FLAGS?.split(/\s+/) ?? [];
|
|
|
|
// Parse job-specific configuration from the command line arguments
|
|
const argv = process.argv.slice(2);
|
|
const parsed_args = this._parseArgs(argv, configs, options);
|
|
|
|
this.options = parsed_args.cli;
|
|
this.extra_options = parsed_args.extra;
|
|
this.combinationFilter = typeof options.combinationFilter === 'function' ? options.combinationFilter : allow;
|
|
|
|
if (options.byGroups) {
|
|
this.queue = [];
|
|
const groupNames = process.env.NODE_RUN_BENCHMARK_GROUPS?.split(',') ?? Object.keys(configs);
|
|
|
|
for (const groupName of groupNames) {
|
|
const config = { ...configs[groupName][0], group: groupName };
|
|
const parsed_args = this._parseArgs(argv, config, options);
|
|
|
|
this.options = parsed_args.cli;
|
|
this.extra_options = parsed_args.extra;
|
|
this.queue = this.queue.concat(this._queue(this.options));
|
|
}
|
|
} else {
|
|
this.queue = this._queue(this.options);
|
|
}
|
|
|
|
if (options.flags) {
|
|
this.flags = this.flags.concat(options.flags);
|
|
}
|
|
|
|
if (this.queue.length === 0)
|
|
return;
|
|
|
|
// The configuration of the current job, head of the queue
|
|
this.config = this.queue[0];
|
|
|
|
process.nextTick(() => {
|
|
if (process.env.NODE_RUN_BENCHMARK_FN !== undefined) {
|
|
fn(this.config);
|
|
} else {
|
|
// _run will use fork() to create a new process for each configuration
|
|
// combination.
|
|
this._run();
|
|
}
|
|
});
|
|
}
|
|
|
|
_parseArgs(argv, configs, options) {
|
|
const cliOptions = {};
|
|
|
|
// Check for the test mode first.
|
|
const testIndex = argv.indexOf('--test');
|
|
if (testIndex !== -1) {
|
|
for (const [key, rawValue] of Object.entries(configs)) {
|
|
let value = Array.isArray(rawValue) ? rawValue[0] : rawValue;
|
|
// Set numbers to one by default to reduce the runtime.
|
|
if (typeof value === 'number') {
|
|
if (key === 'dur' || key === 'duration') {
|
|
value = 0.05;
|
|
} else if (value > 1) {
|
|
value = 1;
|
|
}
|
|
}
|
|
cliOptions[key] = [value];
|
|
}
|
|
// Override specific test options.
|
|
if (options.test) {
|
|
for (const [key, value] of Object.entries(options.test)) {
|
|
cliOptions[key] = Array.isArray(value) ? value : [value];
|
|
}
|
|
}
|
|
argv.splice(testIndex, 1);
|
|
} else {
|
|
// Accept single values instead of arrays.
|
|
for (const [key, value] of Object.entries(configs)) {
|
|
if (!Array.isArray(value))
|
|
configs[key] = [value];
|
|
}
|
|
}
|
|
|
|
const extraOptions = {};
|
|
const validArgRE = /^(.+?)=([\s\S]*)$/;
|
|
// Parse configuration arguments
|
|
for (const arg of argv) {
|
|
const match = arg.match(validArgRE);
|
|
if (!match) {
|
|
console.error(`bad argument: ${arg}`);
|
|
process.exit(1);
|
|
}
|
|
const [, key, value] = match;
|
|
if (configs[key] !== undefined) {
|
|
cliOptions[key] ||= [];
|
|
cliOptions[key].push(
|
|
// Infer the type from the config object and parse accordingly
|
|
typeof configs[key][0] === 'number' ? +value : value,
|
|
);
|
|
} else {
|
|
extraOptions[key] = value;
|
|
}
|
|
}
|
|
return { cli: { ...configs, ...cliOptions }, extra: extraOptions };
|
|
}
|
|
|
|
_queue(options) {
|
|
const queue = [];
|
|
const keys = Object.keys(options);
|
|
const { combinationFilter } = this;
|
|
|
|
// Perform a depth-first walk through all options to generate a
|
|
// configuration list that contains all combinations.
|
|
function recursive(keyIndex, prevConfig) {
|
|
const key = keys[keyIndex];
|
|
const values = options[key];
|
|
|
|
for (const value of values) {
|
|
if (typeof value !== 'number' && typeof value !== 'string') {
|
|
throw new TypeError(
|
|
`configuration "${key}" had type ${typeof value}`);
|
|
}
|
|
if (typeof value !== typeof values[0]) {
|
|
// This is a requirement for being able to consistently and
|
|
// predictably parse CLI provided configuration values.
|
|
throw new TypeError(`configuration "${key}" has mixed types`);
|
|
}
|
|
|
|
const currConfig = { [key]: value, ...prevConfig };
|
|
|
|
if (keyIndex + 1 < keys.length) {
|
|
recursive(keyIndex + 1, currConfig);
|
|
} else {
|
|
// Check if we should allow the current combination
|
|
const allowed = combinationFilter({ ...currConfig });
|
|
if (typeof allowed !== 'boolean') {
|
|
throw new TypeError(
|
|
'Combination filter must always return a boolean',
|
|
);
|
|
}
|
|
if (allowed)
|
|
queue.push(currConfig);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (keys.length > 0) {
|
|
recursive(0, {});
|
|
} else {
|
|
queue.push({});
|
|
}
|
|
|
|
return queue;
|
|
}
|
|
|
|
http(options, cb) {
|
|
const http_options = { ...options };
|
|
http_options.benchmarker ||= this.config.benchmarker ||
|
|
this.extra_options.benchmarker ||
|
|
http_benchmarkers.default_http_benchmarker;
|
|
http_benchmarkers.run(
|
|
http_options, (error, code, used_benchmarker, result, elapsed) => {
|
|
if (cb) {
|
|
cb(code);
|
|
}
|
|
if (error) {
|
|
console.error(error);
|
|
process.exit(code || 1);
|
|
}
|
|
this.config.benchmarker = used_benchmarker;
|
|
this.report(result, elapsed);
|
|
},
|
|
);
|
|
}
|
|
|
|
_run() {
|
|
// If forked, report to the parent.
|
|
if (process.send) {
|
|
process.send({
|
|
type: 'config',
|
|
name: this.name,
|
|
queueLength: this.queue.length,
|
|
});
|
|
}
|
|
|
|
const recursive = (queueIndex) => {
|
|
const config = this.queue[queueIndex];
|
|
|
|
// Set NODE_RUN_BENCHMARK_FN to indicate that the child shouldn't
|
|
// construct a configuration queue, but just execute the benchmark
|
|
// function.
|
|
const childEnv = { ...process.env };
|
|
childEnv.NODE_RUN_BENCHMARK_FN = '';
|
|
|
|
// Create configuration arguments
|
|
const childArgs = [];
|
|
for (const [key, value] of Object.entries(config)) {
|
|
childArgs.push(`${key}=${value}`);
|
|
}
|
|
for (const [key, value] of Object.entries(this.extra_options)) {
|
|
childArgs.push(`${key}=${value}`);
|
|
}
|
|
|
|
const child = child_process.fork(require.main.filename, childArgs, {
|
|
env: childEnv,
|
|
execArgv: this.flags.concat(process.execArgv),
|
|
});
|
|
child.on('message', sendResult);
|
|
child.on('close', (code) => {
|
|
if (code) {
|
|
process.exit(code);
|
|
}
|
|
|
|
if (queueIndex + 1 < this.queue.length) {
|
|
recursive(queueIndex + 1);
|
|
}
|
|
});
|
|
};
|
|
|
|
recursive(0);
|
|
}
|
|
|
|
start() {
|
|
if (this._started) {
|
|
throw new Error('Called start more than once in a single benchmark');
|
|
}
|
|
this._started = true;
|
|
this._time = process.hrtime.bigint();
|
|
}
|
|
|
|
end(operations) {
|
|
// Get elapsed time now and do error checking later for accuracy.
|
|
const time = process.hrtime.bigint();
|
|
|
|
if (!this._started) {
|
|
throw new Error('called end without start');
|
|
}
|
|
if (this._ended) {
|
|
throw new Error('called end multiple times');
|
|
}
|
|
if (typeof operations !== 'number') {
|
|
throw new Error('called end() without specifying operation count');
|
|
}
|
|
if (!process.env.NODEJS_BENCHMARK_ZERO_ALLOWED && operations <= 0) {
|
|
throw new Error('called end() with operation count <= 0');
|
|
}
|
|
|
|
this._ended = true;
|
|
|
|
if (time === this._time) {
|
|
if (!process.env.NODEJS_BENCHMARK_ZERO_ALLOWED)
|
|
throw new Error('insufficient clock precision for short benchmark');
|
|
// Avoid dividing by zero
|
|
this.report(operations && Number.MAX_VALUE, 0n);
|
|
return;
|
|
}
|
|
|
|
const elapsed = time - this._time;
|
|
const rate = operations / (Number(elapsed) / 1e9);
|
|
this.report(rate, elapsed);
|
|
}
|
|
|
|
report(rate, elapsed) {
|
|
sendResult({
|
|
name: this.name,
|
|
conf: this.config,
|
|
rate,
|
|
time: nanoSecondsToString(elapsed),
|
|
type: 'report',
|
|
});
|
|
}
|
|
}
|
|
|
|
function nanoSecondsToString(bigint) {
|
|
const str = bigint.toString();
|
|
const decimalPointIndex = str.length - 9;
|
|
if (decimalPointIndex <= 0) {
|
|
return `0.${'0'.repeat(-decimalPointIndex)}${str}`;
|
|
}
|
|
return `${str.slice(0, decimalPointIndex)}.${str.slice(decimalPointIndex)}`;
|
|
}
|
|
|
|
function formatResult(data) {
|
|
// Construct configuration string, " A=a, B=b, ..."
|
|
let conf = '';
|
|
for (const key of Object.keys(data.conf)) {
|
|
conf += ` ${key}=${JSON.stringify(data.conf[key])}`;
|
|
}
|
|
|
|
let rate = data.rate.toString().split('.');
|
|
rate[0] = rate[0].replace(/(\d)(?=(?:\d\d\d)+(?!\d))/g, '$1,');
|
|
rate = (rate[1] ? rate.join('.') : rate[0]);
|
|
return `${data.name}${conf}: ${rate}\n`;
|
|
}
|
|
|
|
function sendResult(data) {
|
|
if (process.send) {
|
|
// If forked, report by process send
|
|
process.send(data, () => {
|
|
if (process.env.NODE_RUN_BENCHMARK_FN !== undefined) {
|
|
// If, for any reason, the process is unable to self close within
|
|
// a second after completing, forcefully close it.
|
|
require('timers').setTimeout(() => {
|
|
process.exit(0);
|
|
}, 5000).unref();
|
|
}
|
|
});
|
|
} else {
|
|
// Otherwise report by stdout
|
|
process.stdout.write(formatResult(data));
|
|
}
|
|
}
|
|
|
|
const urls = {
|
|
long: 'http://nodejs.org:89/docs/latest/api/foo/bar/qua/13949281/0f28b/' +
|
|
'/5d49/b3020/url.html#test?payload1=true&payload2=false&test=1' +
|
|
'&benchmark=3&foo=38.38.011.293&bar=1234834910480&test=19299&3992&' +
|
|
'key=f5c65e1e98fe07e648249ad41e1cfdb0',
|
|
short: 'https://nodejs.org/en/blog/',
|
|
idn: 'http://你好你好.在线',
|
|
auth: 'https://user:pass@example.com/path?search=1',
|
|
file: 'file:///foo/bar/test/node.js',
|
|
ws: 'ws://localhost:9229/f46db715-70df-43ad-a359-7f9949f39868',
|
|
javascript: 'javascript:alert("node is awesome");',
|
|
percent: 'https://%E4%BD%A0/foo',
|
|
dot: 'https://example.org/./a/../b/./c',
|
|
};
|
|
|
|
const searchParams = {
|
|
noencode: 'foo=bar&baz=quux&xyzzy=thud',
|
|
multicharsep: 'foo=bar&&&&&&&&&&baz=quux&&&&&&&&&&xyzzy=thud',
|
|
encodefake: 'foo=%©ar&baz=%A©uux&xyzzy=%©ud',
|
|
encodemany: '%66%6F%6F=bar&%62%61%7A=quux&xyzzy=%74h%75d',
|
|
encodelast: 'foo=bar&baz=quux&xyzzy=thu%64',
|
|
multivalue: 'foo=bar&foo=baz&foo=quux&quuy=quuz',
|
|
multivaluemany: 'foo=bar&foo=baz&foo=quux&quuy=quuz&foo=abc&foo=def&' +
|
|
'foo=ghi&foo=jkl&foo=mno&foo=pqr&foo=stu&foo=vwxyz',
|
|
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',
|
|
manyblankpairs: '&&&&&&&&&&&&&&&&&&&&&&&&',
|
|
altspaces: 'foo+bar=baz+quux&xyzzy+thud=quuy+quuz&abc=def+ghi',
|
|
};
|
|
|
|
function getUrlData(withBase) {
|
|
const data = require('../test/fixtures/wpt/url/resources/urltestdata.json');
|
|
const result = [];
|
|
for (const item of data) {
|
|
if (item.failure || !item.input) continue;
|
|
if (withBase) {
|
|
// item.base might be null. It should be converted into `undefined`.
|
|
result.push([item.input, item.base ?? undefined]);
|
|
} else if (item.base !== null) {
|
|
result.push(item.base);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Generate an array of data for URL benchmarks to use.
|
|
* The size of the resulting data set is the original data size * 2 ** `e`.
|
|
* The 'wpt' type contains about 400 data points when `withBase` is true,
|
|
* and 200 data points when `withBase` is false.
|
|
* Other types contain 200 data points with or without base.
|
|
* @param {string} type Type of the data, 'wpt' or a key of `urls`
|
|
* @param {number} e The repetition of the data, as exponent of 2
|
|
* @param {boolean} withBase Whether to include a base URL
|
|
* @param {boolean} asUrl Whether to return the results as URL objects
|
|
* @return {string[] | string[][] | URL[]}
|
|
*/
|
|
function bakeUrlData(type, e = 0, withBase = false, asUrl = false) {
|
|
let result = [];
|
|
if (type === 'wpt') {
|
|
result = getUrlData(withBase);
|
|
} else if (urls[type]) {
|
|
const input = urls[type];
|
|
const item = withBase ? [input, 'about:blank'] : input;
|
|
// Roughly the size of WPT URL test data
|
|
result = new Array(200).fill(item);
|
|
} else {
|
|
throw new Error(`Unknown url data type ${type}`);
|
|
}
|
|
|
|
if (typeof e !== 'number') {
|
|
throw new Error(`e must be a number, received ${e}`);
|
|
}
|
|
|
|
for (let i = 0; i < e; ++i) {
|
|
result = result.concat(result);
|
|
}
|
|
|
|
if (asUrl) {
|
|
if (withBase) {
|
|
result = result.map(([input, base]) => new URL(input, base));
|
|
} else {
|
|
result = result.map((input) => new URL(input));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
module.exports = {
|
|
Benchmark,
|
|
PORT: http_benchmarkers.PORT,
|
|
bakeUrlData,
|
|
binding(bindingName) {
|
|
try {
|
|
const { internalBinding } = require('internal/test/binding');
|
|
|
|
return internalBinding(bindingName);
|
|
} catch {
|
|
return process.binding(bindingName);
|
|
}
|
|
},
|
|
buildType: process.features.debug ? 'Debug' : 'Release',
|
|
createBenchmark(fn, configs, options) {
|
|
return new Benchmark(fn, configs, options);
|
|
},
|
|
sendResult,
|
|
searchParams,
|
|
urlDataTypes: Object.keys(urls).concat(['wpt']),
|
|
urls,
|
|
};
|