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