@stdlib/bench-harness
Version:
Benchmark harness.
304 lines (278 loc) • 8.32 kB
JavaScript
/**
* @license Apache-2.0
*
* Copyright (c) 2018 The Stdlib Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
// MODULES //
var setReadOnly = require( '@stdlib/utils-define-nonenumerable-read-only-property' );
var setReadOnlyAccessor = require( '@stdlib/utils-define-nonenumerable-read-only-accessor' );
var isString = require( '@stdlib/assert-is-string' ).isPrimitive;
var isFunction = require( '@stdlib/assert-is-function' );
var isBoolean = require( '@stdlib/assert-is-boolean' ).isPrimitive;
var isObject = require( '@stdlib/assert-is-plain-object' );
var hasOwnProp = require( '@stdlib/assert-has-own-property' );
var format = require( '@stdlib/string-format' );
var copy = require( '@stdlib/utils-copy' );
var Benchmark = require( './../benchmark-class' );
var Runner = require( './../runner' );
var nextTick = require( './../utils/next_tick.js' );
var DEFAULTS = require( './../defaults.json' );
var validate = require( './validate.js' );
var init = require( './init.js' );
// MAIN //
/**
* Creates a benchmark harness.
*
* @param {Options} [options] - function options
* @param {boolean} [options.autoclose] - boolean indicating whether to automatically close a harness after a harness finishes running all benchmarks
* @param {Callback} [clbk] - callback to invoke when a harness finishes running all benchmarks
* @throws {TypeError} options argument must be an object
* @throws {TypeError} must provide valid options
* @throws {TypeError} callback argument must be a function
* @returns {Function} benchmark harness
*
* @example
* var bench = createHarness( onFinish );
*
* function onFinish() {
* bench.close();
* console.log( 'Exit code: %d', bench.exitCode );
* }
*
* bench( 'beep', function benchmark( b ) {
* var x;
* var i;
* b.tic();
* for ( i = 0; i < b.iterations; i++ ) {
* x = Math.sin( Math.random() );
* if ( x !== x ) {
* b.ok( false, 'should not return NaN' );
* }
* }
* b.toc();
* if ( x !== x ) {
* b.ok( false, 'should not return NaN' );
* }
* b.end();
* });
*
* @example
* var stdout = require( '@stdlib/streams-node-stdout' );
*
* var stream = createHarness().createStream();
* stream.pipe( stdout );
*/
function createHarness( options, clbk ) {
var exitCode;
var runner;
var queue;
var opts;
var cb;
opts = {};
if ( arguments.length === 1 ) {
if ( isFunction( options ) ) {
cb = options;
} else if ( isObject( options ) ) {
opts = options;
} else {
throw new TypeError( format( 'invalid argument. Must provide either an options object or a function. Value: `%s`.', options ) );
}
} else if ( arguments.length > 1 ) {
if ( !isObject( options ) ) {
throw new TypeError( format( 'invalid argument. First argument must be an object. Value: `%s`.', options ) );
}
if ( hasOwnProp( options, 'autoclose' ) ) {
opts.autoclose = options.autoclose;
if ( !isBoolean( opts.autoclose ) ) {
throw new TypeError( format( 'invalid option. `%s` option must be a boolean. Option: `%s`.', 'autoclose', opts.autoclose ) );
}
}
cb = clbk;
if ( !isFunction( cb ) ) {
throw new TypeError( format( 'invalid argument. Second argument must be a function. Value: `%s`.', cb ) );
}
}
runner = new Runner();
if ( opts.autoclose ) {
runner.once( 'done', close );
}
if ( cb ) {
runner.once( 'done', cb );
}
exitCode = 0;
queue = [];
/**
* Benchmark harness.
*
* @private
* @param {string} name - benchmark name
* @param {Options} [options] - benchmark options
* @param {boolean} [options.skip=false] - boolean indicating whether to skip a benchmark
* @param {(PositiveInteger|null)} [options.iterations=null] - number of iterations
* @param {PositiveInteger} [options.repeats=3] - number of repeats
* @param {PositiveInteger} [options.timeout=300000] - number of milliseconds before a benchmark automatically fails
* @param {Function} [benchmark] - function containing benchmark code
* @throws {TypeError} first argument must be a string
* @throws {TypeError} options argument must be an object
* @throws {TypeError} must provide valid options
* @throws {TypeError} benchmark argument must a function
* @throws {Error} benchmark error
* @returns {Function} benchmark harness
*/
function harness( name, options, benchmark ) {
var opts;
var err;
var b;
if ( !isString( name ) ) {
throw new TypeError( format( 'invalid argument. First argument must be a string. Value: `%s`.', name ) );
}
opts = copy( DEFAULTS );
if ( arguments.length === 2 ) {
if ( isFunction( options ) ) {
b = options;
} else {
err = validate( opts, options );
if ( err ) {
throw err;
}
}
} else if ( arguments.length > 2 ) {
err = validate( opts, options );
if ( err ) {
throw err;
}
b = benchmark;
if ( !isFunction( b ) ) {
throw new TypeError( format( 'invalid argument. Third argument must be a function. Value: `%s`.', b ) );
}
}
// Add the benchmark to the initialization queue:
queue.push( [ name, opts, b ] );
// Perform initialization on the next turn of the event loop (note: this allows all benchmarks to be "registered" within the same turn of the loop; otherwise, we run the risk of registration-execution race conditions (i.e., a benchmark registers and executes before other benchmarks can register, depleting the benchmark queue and leading the harness to close)):
if ( queue.length === 1 ) {
nextTick( initialize );
}
return harness;
}
/**
* Initializes each benchmark.
*
* @private
* @returns {void}
*/
function initialize() {
var idx = -1;
return next();
/**
* Initialize the next benchmark.
*
* @private
* @returns {void}
*/
function next() {
var args;
idx += 1;
// If all benchmarks have been initialized, begin running the benchmarks:
if ( idx === queue.length ) {
queue.length = 0;
return runner.run();
}
// Initialize the next benchmark:
args = queue[ idx ];
init( args[ 0 ], args[ 1 ], args[ 2 ], onInit );
}
/**
* Callback invoked after performing initialization tasks.
*
* @private
* @param {string} name - benchmark name
* @param {Options} opts - benchmark options
* @param {(Function|undefined)} benchmark - function containing benchmark code
* @returns {void}
*/
function onInit( name, opts, benchmark ) {
var b;
var i;
// Create a `Benchmark` instance for each repeat to ensure each benchmark has its own state...
for ( i = 0; i < opts.repeats; i++ ) {
b = new Benchmark( name, opts, benchmark );
b.on( 'result', onResult );
runner.push( b );
}
return next();
}
}
/**
* Callback invoked upon a `result` event.
*
* @private
* @param {(string|Object)} result - result
*/
function onResult( result ) {
if (
!isString( result ) &&
!result.ok &&
!result.todo
) {
exitCode = 1;
}
}
/**
* Returns a results stream.
*
* @private
* @param {Object} [options] - options
* @returns {TransformStream} transform stream
*/
function createStream( options ) {
if ( arguments.length ) {
return runner.createStream( options );
}
return runner.createStream();
}
/**
* Closes a benchmark harness.
*
* @private
*/
function close() {
runner.close();
}
/**
* Forcefully exits a benchmark harness.
*
* @private
*/
function exit() {
runner.exit();
}
/**
* Returns the harness exit code.
*
* @private
* @returns {NonNegativeInteger} exit code
*/
function getExitCode() {
return exitCode;
}
setReadOnly( harness, 'createStream', createStream );
setReadOnly( harness, 'close', close );
setReadOnly( harness, 'exit', exit );
setReadOnlyAccessor( harness, 'exitCode', getExitCode );
return harness;
}
// EXPORTS //
module.exports = createHarness;