1'use strict'; 2 3const child_process = require('child_process'); 4const http_benchmarkers = require('./_http-benchmarkers.js'); 5 6class Benchmark { 7 // Used to make sure a benchmark only start a timer once 8 #started = false; 9 10 // Indicate that the benchmark ended 11 #ended = false; 12 13 // Holds process.hrtime value 14 #time = [0, 0]; 15 16 // Use the file name as the name of the benchmark 17 name = require.main.filename.slice(__dirname.length + 1); 18 19 // Execution arguments i.e. flags used to run the jobs 20 flags = process.env.NODE_BENCHMARK_FLAGS ? 21 process.env.NODE_BENCHMARK_FLAGS.split(/\s+/) : 22 []; 23 24 constructor(fn, configs, options = {}) { 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(); 222 } 223 224 end(operations) { 225 // Get elapsed time now and do error checking later for accuracy. 226 const elapsed = process.hrtime(this.#time); 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 if (elapsed[0] === 0 && elapsed[1] === 0) { 241 if (!process.env.NODEJS_BENCHMARK_ZERO_ALLOWED) 242 throw new Error('insufficient clock precision for short benchmark'); 243 // Avoid dividing by zero 244 elapsed[1] = 1; 245 } 246 247 this.#ended = true; 248 const time = elapsed[0] + elapsed[1] / 1e9; 249 const rate = operations / time; 250 this.report(rate, elapsed); 251 } 252 253 report(rate, elapsed) { 254 sendResult({ 255 name: this.name, 256 conf: this.config, 257 rate, 258 time: elapsed[0] + elapsed[1] / 1e9, 259 type: 'report', 260 }); 261 } 262} 263 264function formatResult(data) { 265 // Construct configuration string, " A=a, B=b, ..." 266 let conf = ''; 267 for (const key of Object.keys(data.conf)) { 268 conf += ` ${key}=${JSON.stringify(data.conf[key])}`; 269 } 270 271 let rate = data.rate.toString().split('.'); 272 rate[0] = rate[0].replace(/(\d)(?=(?:\d\d\d)+(?!\d))/g, '$1,'); 273 rate = (rate[1] ? rate.join('.') : rate[0]); 274 return `${data.name}${conf}: ${rate}`; 275} 276 277function sendResult(data) { 278 if (process.send) { 279 // If forked, report by process send 280 process.send(data); 281 } else { 282 // Otherwise report by stdout 283 console.log(formatResult(data)); 284 } 285} 286 287const urls = { 288 long: 'http://nodejs.org:89/docs/latest/api/foo/bar/qua/13949281/0f28b/' + 289 '/5d49/b3020/url.html#test?payload1=true&payload2=false&test=1' + 290 '&benchmark=3&foo=38.38.011.293&bar=1234834910480&test=19299&3992&' + 291 'key=f5c65e1e98fe07e648249ad41e1cfdb0', 292 short: 'https://nodejs.org/en/blog/', 293 idn: 'http://你好你好.在线', 294 auth: 'https://user:pass@example.com/path?search=1', 295 file: 'file:///foo/bar/test/node.js', 296 ws: 'ws://localhost:9229/f46db715-70df-43ad-a359-7f9949f39868', 297 javascript: 'javascript:alert("node is awesome");', 298 percent: 'https://%E4%BD%A0/foo', 299 dot: 'https://example.org/./a/../b/./c', 300}; 301 302const searchParams = { 303 noencode: 'foo=bar&baz=quux&xyzzy=thud', 304 multicharsep: 'foo=bar&&&&&&&&&&baz=quux&&&&&&&&&&xyzzy=thud', 305 encodefake: 'foo=%©ar&baz=%A©uux&xyzzy=%©ud', 306 encodemany: '%66%6F%6F=bar&%62%61%7A=quux&xyzzy=%74h%75d', 307 encodelast: 'foo=bar&baz=quux&xyzzy=thu%64', 308 multivalue: 'foo=bar&foo=baz&foo=quux&quuy=quuz', 309 multivaluemany: 'foo=bar&foo=baz&foo=quux&quuy=quuz&foo=abc&foo=def&' + 310 'foo=ghi&foo=jkl&foo=mno&foo=pqr&foo=stu&foo=vwxyz', 311 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', 312 manyblankpairs: '&&&&&&&&&&&&&&&&&&&&&&&&', 313 altspaces: 'foo+bar=baz+quux&xyzzy+thud=quuy+quuz&abc=def+ghi', 314}; 315 316function getUrlData(withBase) { 317 const data = require('../test/fixtures/wpt/url/resources/urltestdata.json'); 318 const result = []; 319 for (const item of data) { 320 if (item.failure || !item.input) continue; 321 if (withBase) { 322 result.push([item.input, item.base]); 323 } else if (item.base !== 'about:blank') { 324 result.push(item.base); 325 } 326 } 327 return result; 328} 329 330/** 331 * Generate an array of data for URL benchmarks to use. 332 * The size of the resulting data set is the original data size * 2 ** `e`. 333 * The 'wpt' type contains about 400 data points when `withBase` is true, 334 * and 200 data points when `withBase` is false. 335 * Other types contain 200 data points with or without base. 336 * 337 * @param {string} type Type of the data, 'wpt' or a key of `urls` 338 * @param {number} e The repetition of the data, as exponent of 2 339 * @param {boolean} withBase Whether to include a base URL 340 * @param {boolean} asUrl Whether to return the results as URL objects 341 * @return {string[] | string[][] | URL[]} 342 */ 343function bakeUrlData(type, e = 0, withBase = false, asUrl = false) { 344 let result = []; 345 if (type === 'wpt') { 346 result = getUrlData(withBase); 347 } else if (urls[type]) { 348 const input = urls[type]; 349 const item = withBase ? [input, 'about:blank'] : input; 350 // Roughly the size of WPT URL test data 351 result = new Array(200).fill(item); 352 } else { 353 throw new Error(`Unknown url data type ${type}`); 354 } 355 356 if (typeof e !== 'number') { 357 throw new Error(`e must be a number, received ${e}`); 358 } 359 360 for (let i = 0; i < e; ++i) { 361 result = result.concat(result); 362 } 363 364 if (asUrl) { 365 if (withBase) { 366 result = result.map(([input, base]) => new URL(input, base)); 367 } else { 368 result = result.map((input) => new URL(input)); 369 } 370 } 371 return result; 372} 373 374module.exports = { 375 Benchmark, 376 PORT: http_benchmarkers.PORT, 377 bakeUrlData, 378 binding(bindingName) { 379 try { 380 const { internalBinding } = require('internal/test/binding'); 381 382 return internalBinding(bindingName); 383 } catch { 384 return process.binding(bindingName); 385 } 386 }, 387 buildType: process.features.debug ? 'Debug' : 'Release', 388 createBenchmark(fn, configs, options) { 389 return new Benchmark(fn, configs, options); 390 }, 391 sendResult, 392 searchParams, 393 urlDataTypes: Object.keys(urls).concat(['wpt']), 394 urls, 395}; 396