benchtable
Version:
Benchmark.js results in ascii tables for NodeJS
249 lines (214 loc) • 8.79 kB
JavaScript
"use strict";
// Module dependencies.
const Benchmark = require('benchmark');
const Table = require('cli-table');
const colorIt = require('color-it');
module.exports = class BenchTable extends Benchmark.Suite {
/**
* BenchTable
* The `BenchTable` constructor. `BenchTable` is extended from [Benchmark.Suite](http://benchmarkjs.com/docs#Suite).
*
* @name BenchTable
* @function
* @param {String} name A name to identify the suite.
* @param {String} options Options object. A special boolean option `isTransposed`
* is available for Benchtable–if set to `true`, the output ASCII table will be
* transposed: the benchmarked functions will be organized into columns, while
* inputs will be organized into rows. This is useful for situations where the
* number of inputs is large, and the number of functions is small. This option
* defaults to `false`.
*/
constructor(name, options) {
super(name, options);
this._functions = [];
this._functionNames = [];
this._functionOptions = [];
this._inputs = [];
this._inputNames = [];
this._results = {};
// mappings from benchmark names to function and input indices
// as { funcIdx, inputIdx }
this._mappings = {};
// if transposed, then functions are in columns and inputs are in rows
this._transposed = options && options.isTransposed;
/**
* Use the 'cycle' event to store results and build a table
* after all BenchTable functions are evaluated for all inputs.
**/
this.on('cycle', event => {
var key = event.target.name;
var worst_idx, best_idx, worst, best, curr;
var i, j;
var funName, inputName, item;
if (!(key in this._mappings)) {
return;
}
// store current result
funName = this._functionNames[this._mappings[key].funcIdx];
this._results[funName].push(event.target);
this._counter -= 1;
if (this._counter !== 0) {
return;
}
// compute best and worst results for each input
for (i = 0; i < this._inputs.length; i++) {
worst_idx = 0, best_idx = 0;
best = this._results[this._functionNames[best_idx]][i];
worst = this._results[this._functionNames[worst_idx]][i];
for (j = 0; j < this._functions.length; j++) {
curr = this._results[this._functionNames[j]][i];
if (curr.hz <= worst.hz) {
worst_idx = j;
worst = this._results[this._functionNames[worst_idx]][i];
}
if (curr.hz >= best.hz) {
best_idx = j;
best = this._results[this._functionNames[best_idx]][i];
}
}
worst._isWorst = true;
best._isBest = true;
}
function toTableStr(par) {
if (par.error) {
return 'ERROR';
} else {
return Benchmark.formatNumber(par.hz.toFixed(par.hz < 100 ? 2 : 0)) + ' ops/sec';
}
}
// create cli-table based on results
var headers = this._transposed ? [""].concat(this._functionNames) : [""].concat(this._inputNames);
this.table = new Table({
head: headers,
chars: {
'top': '-',
'top-mid': '+',
'top-left': '+',
'top-right': '+',
'bottom': '-',
'bottom-mid': '+',
'bottom-left': '+',
'bottom-right': '+',
'left': '|',
'left-mid': '+',
'mid': '-',
'mid-mid': '+',
'right': '|',
'right-mid': '+'
},
truncate: '…'
});
if (!this._transposed) {
for (i = 0; i < this._functions.length; i++) {
item = {};
funName = this._functionNames[i];
item[funName] = [];
for (j = 0; j < this._results[funName].length; j++) {
curr = this._results[funName][j];
item[funName].push(toTableStr(curr));
if (this._functions.length > 1) {
if (curr._isWorst) {
item[funName][j] = colorIt(item[funName][j]).red().toString();
} else if (curr._isBest) {
item[funName][j] = colorIt(item[funName][j]).green().toString();
}
}
}
this.table.push(item);
}
} else {
for (i = 0; i < this._inputs.length; i++) {
item = {};
inputName = this._inputNames[i];
item[inputName] = [];
for (j = 0; j < this._functionNames.length; j++) {
funName = this._functionNames[j];
curr = this._results[funName][i];
item[inputName].push(toTableStr(curr));
if (this._functions.length > 1) {
if (curr._isWorst) {
item[inputName][j] = colorIt(item[inputName][j]).red().toString();
} else if (curr._isBest) {
item[inputName][j] = colorIt(item[inputName][j]).green().toString();
}
}
}
this.table.push(item);
}
}
});
}
/**
* run
* Runs the suite.
*
* @name run
* @function
* @param {Object} config The options object. [See `benchmark.js` API docs.](http://benchmarkjs.com/docs#Suite_prototype_run)
*/
run(config) {
var mapkey, mapfun;
let createBenchmarkFunction = (fun, options, input) => {
if (options.defer) {
return deferred => {
fun.apply(this, [deferred].concat(input))
};
}
return () => {
fun.apply(this, input);
};
}
for (var i = 0; i < this._functions.length; i++) {
for (var j = 0; j < this._inputs.length; j++) {
mapkey = this._functionNames[i] + " for inputs " + this._inputNames[j];
mapfun = createBenchmarkFunction(this._functions[i], this._functionOptions[i], this._inputs[j]);
this.add(mapkey, mapfun, this._functionOptions[i]);
this._mappings[mapkey] = {
funcIdx: i,
inputIdx: j
};
}
}
this._counter = this._functions.length * this._inputs.length;
super.run(config);
}
/**
* addFunction
* Specify functions to be benchmarked.
* This function may be called multiple times to add multiple functions.
*
* @name addFunction
* @function
* @param {String} name A name to identify the function.
* @param {Function} fun The test to benchmark.
* @param {Object} options The options object.
* @return {BenchTable} The `BenchTable` instance.
*/
addFunction(name, fun, options) {
this._functions.push(fun);
this._functionNames.push(name);
this._functionOptions.push(options || {});
this._results[name] = [];
return this;
}
/**
* addInput
* Specify inputs for functions.
* This function may be called multiple times to add multiple inputs.
*
* @name addInput
* @function
* @param {String} name A name to identify the input.
* @param {Array} input The array containing arguments that will be
* passed to each benchmarked function. Therefore, the number of
* elements in the `input` array should match the number of arguments of
* each specified function (i.e. the array will be "unpacked" when
* invoking functions, they will not receive a single array argument).
* @return {BenchTable} The `BenchTable` instance.
*/
addInput(name, input) {
this._inputs.push(input);
this._inputNames.push(name);
return this;
}
}