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