exam
Version:
Clustered JavaScript test runner with builtin assertions.
177 lines (150 loc) • 4.68 kB
JavaScript
function up (node, key, defaultValue) {
var value
while (node && !value) {
value = node[key]
node = node.parent
}
return value || defaultValue
}
/**
* Run a benchmark, then call a function to signal it's finished.
*
* @param {Function} done A function to call when the benchmark is finished.
*/
module.exports = function (done) {
var self = this
// Log progress to the console.
var reporter = require('./reporters/' + self.root.options.reporter)
// Limit execution time, defaulting to 10 seconds.
var runTime = up(self, 'benchTime', self.root.options.benchTime)
var stopTime = Date.now() + runTime
// Require a level of confidence to declare a winner, defaulting to 99%.
var confidentZ = up(self, 'z', 2.576)
// Require crazy confidence to stop testing a loser.
var stopZ = up(self, 'stopZ', 50)
// Put the children into an array of children to be analyzed.
var children = Array.prototype.slice.call(self.children)
if (!children.length) {
done()
}
// Initialize benchmark data.
var sampleSize = Math.min(100, runTime)
var passCount = 0
var runIndex
var childIndex
var child
var start
var fn
// Initialize benchmarking statistics.
children.forEach(initStats)
// Start the first pass.
nextPass()
// Initialize the mean speed and speed variance.
function initStats (child) {
child.runCount = 0
child.speed = 0
child.variance = 0
}
// Incorporate a speed sampling into the mean speed and (running) variance.
function recordSpeed (child, speed) {
child.runCount += sampleSize
if (passCount > 1) {
var square = Math.pow(speed - child.speed, 2)
child.variance += (square - child.variance) / (passCount - 1)
}
child.speed += (speed - child.speed) / passCount
}
// Initialize a new pass by resetting child and sample indexes.
function nextPass () {
passCount++
childIndex = runIndex = 0
nextChild()
}
// Reference the next child (if possible) and start running its function.
function nextChild () {
child = children[childIndex++]
if (child) {
fn = child.fn
runIndex = 0
var runFn = /^function.*?\([^\s\)]/.test(fn.toString()) ? runAsync : runSync
start = process.hrtime()
runFn()
} else {
calculateStats()
}
}
// Run the current child's test function synchronously.
function runSync () {
for (runIndex = 0; runIndex < sampleSize; runIndex++) {
fn.call(child)
}
setTimeout(finishChild, 0)
}
// Run the current child's test function asynchronously.
function runAsync () {
if (runIndex++ < sampleSize) {
fn.call(child, function () {
setTimeout(runAsync, 0)
})
} else {
setTimeout(finishChild, 0)
}
}
function finishChild () {
var end = process.hrtime()
var nanos = (end[0] - start[0]) * 1e9 + end[1] - start[1]
recordSpeed(child, sampleSize / nanos * 1e9)
nextChild()
}
function sortBySpeed (array) {
array.sort(function (a, b) {
return b.speed - a.speed
})
}
function calculateStats () {
// Sort children by speed, descending.
sortBySpeed(children)
// Perform Z-tests after the first pass.
if (passCount > 1) {
for (var i = children.length - 1; i > 0; i--) {
// Compare each child to the fastest.
var slow = children[i]
var fast = children[0]
// Calculate a standard error from average variance.
var variance = (fast.variance + slow.variance) / 2
var error = Math.sqrt(variance / passCount)
// Calculate a Z value.
var z = (fast.speed - slow.speed) / error
// If we're confident one is slower, record it as such.
if (z > confidentZ) {
slow.slower = true
// If we're super confident, narrow down to the last 2.
if (z > stopZ && i > 1) {
children.splice(i, 1)
}
} else {
slow.slower = false
}
fast.slower = false
}
}
// If there's time, do another pass.
if (Date.now() < stopTime) {
// After the 1st pass, adjust sample size using the slowest child.
if (passCount === 1) {
sampleSize = Math.ceil(children[children.length - 1].speed / 100)
}
setTimeout(nextPass, 0)
// When we're finished, sort children and calculate times.
} else {
sortBySpeed(self.children)
var best = children[0]
// Time is expressed as number of times slower than the best.
self.children.forEach(function (child) {
child.time = best.speed / child.speed
})
reporter.time()
done()
}
}
}