UNPKG

tedious

Version:

A TDS driver, for connecting to MS SQLServer databases.

288 lines (247 loc) 8.54 kB
'use strict'; const fs = require('fs'); const child_process = require('child_process'); const { PerformanceObserver, constants: { NODE_PERFORMANCE_GC_INCREMENTAL, NODE_PERFORMANCE_GC_MAJOR, NODE_PERFORMANCE_GC_MINOR } } = require('perf_hooks'); const { Connection } = require('tedious'); // The `Benchmark` class is taken from Node.js - see // https://github.com/nodejs/node/blob/0f96dc266fd0cd8c1baa82ce7eb951c11b29a331/benchmark/common.js // Copyright Node.js contributors. All rights reserved. function Benchmark(fn, configs, options) { // Use the file name as the name of the benchmark this.name = require.main.filename.slice(__dirname.length + 1); // Parse job-specific configuration from the command line arguments const parsed_args = this._parseArgs(process.argv.slice(2), configs); this.options = parsed_args.cli; this.extra_options = parsed_args.extra; // The configuration list as a queue of jobs this.queue = this._queue(this.options); // The configuration of the current job, head of the queue this.config = this.queue[0]; // Execution arguments i.e. flags used to run the jobs this.flags = []; if (options && options.flags) { this.flags = this.flags.concat(options.flags); } if (process.env.NODE_BENCHMARK_FLAGS) { const flags = process.env.NODE_BENCHMARK_FLAGS.split(/\s+/); this.flags = this.flags.concat(flags); } // Holds process.hrtime value this._time = [0, 0]; // Used to make sure a benchmark only start a timer once this._started = false; this._gcStats = { [NODE_PERFORMANCE_GC_INCREMENTAL]: { count: 0, totalDuration: 0 }, [NODE_PERFORMANCE_GC_MINOR]: { count: 0, totalDuration: 0 }, [NODE_PERFORMANCE_GC_MAJOR]: { count: 0, totalDuration: 0 } }; this._observer = new PerformanceObserver((list) => { const entries = list.getEntries(); const length = entries.length; for (let i = 0; i < length; i++) { const entry = entries[i]; const stats = this._gcStats[entry.detail.kind]; if (stats) { stats.count += 1; stats.totalDuration += entry.duration; } } }); // this._run will use fork() to create a new process for each configuration // combination. if (process.env.hasOwnProperty('NODE_RUN_BENCHMARK_FN')) { process.nextTick(() => fn(this.config)); } else { process.nextTick(() => this._run()); } } Benchmark.prototype._parseArgs = function(argv, configs) { const cliOptions = {}; 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 config = match[1]; if (configs[config]) { // Infer the type from the config object and parse accordingly const isNumber = typeof configs[config][0] === 'number'; const value = isNumber ? +match[2] : match[2]; if (!cliOptions[config]) cliOptions[config] = []; cliOptions[config].push(value); } else { extraOptions[config] = match[2]; } } return { cli: Object.assign({}, configs, cliOptions), extra: extraOptions }; }; Benchmark.prototype._queue = function(options) { const queue = []; const keys = Object.keys(options); // Perform a depth-first walk though all options to generate a // configuration list that contains all combinations. function recursive(keyIndex, prevConfig) { const key = keys[keyIndex]; const values = options[key]; const type = typeof values[0]; for (const value of values) { if (typeof value !== 'number' && typeof value !== 'string') { throw new TypeError(`configuration "${key}" had type ${typeof value}`); } if (typeof value !== type) { // 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 = Object.assign({ [key]: value }, prevConfig); if (keyIndex + 1 < keys.length) { recursive(keyIndex + 1, currConfig); } else { queue.push(currConfig); } } } if (keys.length > 0) { recursive(0, {}); } else { queue.push({}); } return queue; }; Benchmark.prototype._run = function() { const self = this; // If forked, report to the parent. if (process.send) { process.send({ type: 'config', name: this.name, queueLength: this.queue.length, }); } (function recursive(queueIndex) { const config = self.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 = Object.assign({}, process.env); childEnv.NODE_RUN_BENCHMARK_FN = ''; // Create configuration arguments const childArgs = []; for (const key of Object.keys(config)) { childArgs.push(`${key}=${config[key]}`); } for (const key of Object.keys(self.extra_options)) { childArgs.push(`${key}=${self.extra_options[key]}`); } const child = child_process.fork(require.main.filename, childArgs, { env: childEnv, execArgv: self.flags.concat(process.execArgv), }); child.on('message', sendResult); child.on('close', (code) => { if (code) { process.exit(code); } if (queueIndex + 1 < self.queue.length) { recursive(queueIndex + 1); } }); })(0); }; Benchmark.prototype.start = function() { if (this._started) { throw new Error('Called start more than once in a single benchmark'); } this._started = true; this._observer.observe({ entryTypes: ['gc'], buffered: false }); this._time = process.hrtime(); }; Benchmark.prototype.end = function(operations) { // Get elapsed time now and do error checking later for accuracy. const elapsed = process.hrtime(this._time); if (!this._started) { throw new Error('called end without start'); } 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'); } if (elapsed[0] === 0 && elapsed[1] === 0) { if (!process.env.NODEJS_BENCHMARK_ZERO_ALLOWED) throw new Error('insufficient clock precision for short benchmark'); // Avoid dividing by zero elapsed[1] = 1; } this._observer.disconnect(); const time = elapsed[0] + elapsed[1] / 1e9; const rate = operations / time; this.report(rate, elapsed); }; 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])}`; } var rate = data.rate.toString().split('.'); rate[0] = rate[0].replace(/(\d)(?=(?:\d\d\d)+(?!\d))/g, '$1,'); rate = (rate[1] ? rate.join('.') : rate[0]); var gcInfo; gcInfo = `(minor: ${data.gcStats[NODE_PERFORMANCE_GC_MINOR].count} - ${data.gcStats[NODE_PERFORMANCE_GC_MINOR].totalDuration}ms,`; gcInfo += ` major: ${data.gcStats[NODE_PERFORMANCE_GC_MAJOR].count} - ${data.gcStats[NODE_PERFORMANCE_GC_MAJOR].totalDuration}ms,`; gcInfo += ` incremental: ${data.gcStats[NODE_PERFORMANCE_GC_INCREMENTAL].count} - ${data.gcStats[NODE_PERFORMANCE_GC_INCREMENTAL].totalDuration}ms)`; return `${data.name}${conf}: ${rate} ${gcInfo}`; } function sendResult(data) { if (process.send) { // If forked, report by process send process.send(data); } else { // Otherwise report by stdout console.log(formatResult(data)); } } Benchmark.prototype.report = function(rate, elapsed) { sendResult({ name: this.name, conf: this.config, rate: rate, time: elapsed[0] + elapsed[1] / 1e9, type: 'report', gcStats: this._gcStats }); }; function createBenchmark(fn, configs, options) { return new Benchmark(fn, configs, options); } function createConnection(cb) { var config = JSON.parse(fs.readFileSync(require('os').homedir() + '/.tedious/test-connection.json', 'utf8')).config; var connection = new Connection(config); connection.connect(function() { cb(connection); }); } module.exports.createBenchmark = createBenchmark; module.exports.createConnection = createConnection;