// FIXME: Use the real promise if available.
// FIXME: Make sure this interface is compatible with the real Promise.
function SimplePromise() {
this._chainedPromise = null;
this._callback = null;
}
SimplePromise.prototype.then = function (callback) {
if (this._callback)
throw "SimplePromise doesn't support multiple calls to then";
this._callback = callback;
this._chainedPromise = new SimplePromise;
if (this._resolved)
this.resolve(this._resolvedValue);
return this._chainedPromise;
}
SimplePromise.prototype.resolve = function (value) {
if (!this._callback) {
this._resolved = true;
this._resolvedValue = value;
return;
}
var result = this._callback(value);
if (result instanceof SimplePromise) {
var chainedPromise = this._chainedPromise;
result.then(function (result) { chainedPromise.resolve(result); });
} else
this._chainedPromise.resolve(result);
}
function BenchmarkTestStep(testName, testFunction) {
this.name = testName;
this.run = testFunction;
}
function BenchmarkRunner(suites, client) {
this._suites = suites;
this._prepareReturnValue = null;
this._client = client;
}
BenchmarkRunner.prototype.waitForElement = function (selector) {
var promise = new SimplePromise;
var contentDocument = this._frame.contentDocument;
function resolveIfReady() {
var element = contentDocument.querySelector(selector);
if (element) {
window.requestAnimationFrame(function () {
return promise.resolve(element);
});
return;
}
setTimeout(resolveIfReady, 50);
}
resolveIfReady();
return promise;
}
BenchmarkRunner.prototype._removeFrame = function () {
if (this._frame) {
this._frame.parentNode.removeChild(this._frame);
this._frame = null;
}
}
BenchmarkRunner.prototype._appendFrame = function (src) {
var frame = document.createElement('iframe');
frame.style.width = '800px';
frame.style.height = '600px';
frame.style.border = '0px none';
frame.style.position = 'absolute';
frame.setAttribute('scrolling', 'no');
var marginLeft = parseInt(getComputedStyle(document.body).marginLeft);
var marginTop = parseInt(getComputedStyle(document.body).marginTop);
if (window.innerWidth > 800 + marginLeft && window.innerHeight > 600 + marginTop) {
frame.style.left = marginLeft + 'px';
frame.style.top = marginTop + 'px';
} else {
frame.style.left = '0px';
frame.style.top = '0px';
}
if (this._client && this._client.willAddTestFrame)
this._client.willAddTestFrame(frame);
document.body.insertBefore(frame, document.body.firstChild);
this._frame = frame;
return frame;
}
BenchmarkRunner.prototype._waitAndWarmUp = function () {
var startTime = Date.now();
function Fibonacci(n) {
if (Date.now() - startTime > 100)
return;
if (n <= 0)
return 0;
else if (n == 1)
return 1;
return Fibonacci(n - 2) + Fibonacci(n - 1);
}
var promise = new SimplePromise;
setTimeout(function () {
Fibonacci(100);
promise.resolve();
}, 200);
return promise;
}
BenchmarkRunner.prototype._writeMark = function(name) {
if (window.performance && window.performance.mark)
window.performance.mark(name);
}
// This function ought be as simple as possible. Don't even use SimplePromise.
BenchmarkRunner.prototype._runTest = function(suite, test, prepareReturnValue, callback)
{
var self = this;
var now = window.performance && window.performance.now ? function () { return window.performance.now(); } : Date.now;
var contentWindow = self._frame.contentWindow;
var contentDocument = self._frame.contentDocument;
self._writeMark(suite.name + '.' + test.name + '-start');
var startTime = now();
test.run(prepareReturnValue, contentWindow, contentDocument);
var endTime = now();
self._writeMark(suite.name + '.' + test.name + '-sync-end');
var syncTime = endTime - startTime;
var startTime = now();
setTimeout(function () {
// Some browsers don't immediately update the layout for paint.
// Force the layout here to ensure we're measuring the layout time.
var height = self._frame.contentDocument.body.getBoundingClientRect().height;
var endTime = now();
self._frame.contentWindow._unusedHeightValue = height; // Prevent dead code elimination.
self._writeMark(suite.name + '.' + test.name + '-async-end');
window.requestAnimationFrame(function () {
callback(syncTime, endTime - startTime, height);
});
}, 0);
}
function BenchmarkState(suites) {
this._suites = suites;
this._suiteIndex = -1;
this._testIndex = 0;
this.next();
}
BenchmarkState.prototype.currentSuite = function() {
return this._suites[this._suiteIndex];
}
BenchmarkState.prototype.currentTest = function () {
var suite = this.currentSuite();
return suite ? suite.tests[this._testIndex] : null;
}
BenchmarkState.prototype.next = function () {
this._testIndex++;
var suite = this._suites[this._suiteIndex];
if (suite && this._testIndex < suite.tests.length)
return this;
this._testIndex = 0;
do {
this._suiteIndex++;
} while (this._suiteIndex < this._suites.length && this._suites[this._suiteIndex].disabled);
return this;
}
BenchmarkState.prototype.isFirstTest = function () {
return !this._testIndex;
}
BenchmarkState.prototype.prepareCurrentSuite = function (runner, frame) {
var suite = this.currentSuite();
var promise = new SimplePromise;
frame.onload = function () {
suite.prepare(runner, frame.contentWindow, frame.contentDocument).then(function (result) { promise.resolve(result); });
}
frame.src = 'resources/' + suite.url;
return promise;
}
BenchmarkRunner.prototype.step = function (state) {
if (!state) {
state = new BenchmarkState(this._suites);
this._measuredValues = {tests: {}, total: 0, mean: NaN, geomean: NaN, score: NaN};
}
var suite = state.currentSuite();
if (!suite) {
this._finalize();
var promise = new SimplePromise;
promise.resolve();
return promise;
}
if (state.isFirstTest()) {
this._removeFrame();
var self = this;
return state.prepareCurrentSuite(this, this._appendFrame()).then(function (prepareReturnValue) {
self._prepareReturnValue = prepareReturnValue;
return self._runTestAndRecordResults(state);
});
}
return this._runTestAndRecordResults(state);
}
BenchmarkRunner.prototype.runAllSteps = function (startingState) {
var nextCallee = this.runAllSteps.bind(this);
this.step(startingState).then(function (nextState) {
if (nextState)
nextCallee(nextState);
});
}
BenchmarkRunner.prototype.runMultipleIterations = function (iterationCount) {
var self = this;
var currentIteration = 0;
this._runNextIteration = function () {
currentIteration++;
if (currentIteration < iterationCount)
self.runAllSteps();
else if (this._client && this._client.didFinishLastIteration)
this._client.didFinishLastIteration();
}
if (this._client && this._client.willStartFirstIteration)
this._client.willStartFirstIteration(iterationCount);
self.runAllSteps();
}
BenchmarkRunner.prototype._runTestAndRecordResults = function (state) {
var promise = new SimplePromise;
var suite = state.currentSuite();
var test = state.currentTest();
if (this._client && this._client.willRunTest)
this._client.willRunTest(suite, test);
var self = this;
setTimeout(function () {
self._runTest(suite, test, self._prepareReturnValue, function (syncTime, asyncTime) {
var suiteResults = self._measuredValues.tests[suite.name] || {tests:{}, total: 0};
var total = syncTime + asyncTime;
self._measuredValues.tests[suite.name] = suiteResults;
suiteResults.tests[test.name] = {tests: {'Sync': syncTime, 'Async': asyncTime}, total: total};
suiteResults.total += total;
if (self._client && self._client.didRunTest)
self._client.didRunTest(suite, test);
state.next();
promise.resolve(state);
});
}, 0);
return promise;
}
BenchmarkRunner.prototype._finalize = function () {
this._removeFrame();
if (this._client && this._client.didRunSuites) {
var product = 1;
var values = [];
for (var suiteName in this._measuredValues.tests) {
var suiteTotal = this._measuredValues.tests[suiteName].total;
product *= suiteTotal;
values.push(suiteTotal);
}
values.sort(function (a, b) { return a - b }); // Avoid the loss of significance for the sum.
var total = values.reduce(function (a, b) { return a + b });
var geomean = Math.pow(product, 1 / values.length);
var correctionFactor = 3; // This factor makes the test score look reasonably fit within 0 to 140.
this._measuredValues.total = total;
this._measuredValues.mean = total / values.length;
this._measuredValues.geomean = geomean;
this._measuredValues.score = 60 * 1000 / geomean / correctionFactor;
this._client.didRunSuites(this._measuredValues);
}
if (this._runNextIteration)
this._runNextIteration();
}