blob: 0feb0e11646b3e663ef90f0ac90c43a8899c3c58 [file] [log] [blame]
/*
* Copyright (C) 2009 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// A framework for running the benchmark suites and computing a score based
// on timing measurements.
//
// Time measurements are computed by summing individual measurements in a
// test's run() function. Because Javascript generally has a 1ms timer
// granularity, tests should be careful to take at least 2ms to run. Tests
// which run in less than 1ms are invalid. Some older browsers have less
// timer resolution, as low as 15ms. On these systems tests which take less
// than 15ms to run are invalid.
//
// Scores for a benchmark suite are calculated as the geometric mean across
// all benchmarks which comprise that suite.
//
// Overall score is calculated as the geometric mean across all benchmark
// suites.
// Benchmark Object.
// A benchmark is a test which can be run as part of a BenchmarkSuite.
//
// Each test provides the following properties:
// name
// The name of the benchmark.
// run
// The work function which is measured
// opt_setup (optional)
// A function which is run before each benchmark iteration.
// opt_cleanup (optional)
// A function to cleanup any side-effects of a benchmark.
// opt_shareSetup (optional)
// A flag indicating whether the setup function should be run once or with
// each benchmark iteration. (default is false)
//
// Add each method to Arrays, to allow easy iteration over elements
if(!Array.prototype.forEach) {
// forEach's calling syntax is:
// [].forEach(func, scope);
// registered callbacks always get 3 args:
// [].forEach(function(item, index, fullArray){});
Array.prototype.forEach = function(callback, scope) {
callback = hitch(scope, callback);
for (var i = 0, len = this.length; i < len; i++)
callback(this[i], i, this);
};
}
byId = function(id, doc) {
if (typeof id == "string")
return (doc||document).getElementById(id);
return id;
};
// A utility object for measuring time intervals.
function Interval() {
var start_ = 0;
var stop_ = 0;
this.start = function() { start_ = PerfTestRunner.now(); };
this.stop = function() { stop_ = PerfTestRunner.now(); };
this.microseconds = function() { return (stop_ - start_) * 1000; };
}
// A stub profiler object.
function PseudoProfiler() {}
PseudoProfiler.prototype.start = function() {};
PseudoProfiler.prototype.stop = function() {};
var chromiumProfilerInitOnce = false; // Chromium profiler initialization.
// Cross-platform function to get the profiler object.
// For chromium, returns a Profiler object which can be used during the run.
// For other browsers, returns a PseudoProfiler object.
function GetProfiler() {
if (window.chromium && window.chromium.Profiler) {
var profiler = new window.chromium.Profiler();
if (!chromiumProfilerInitOnce) {
chromiumProfilerInitOnce = true;
profiler.stop();
if (profiler.clear)
profiler.clear();
if (profiler.setThreadName)
profiler.setThreadName("domperf javascript");
}
return profiler;
}
return new PseudoProfiler();
}
function Benchmark(name, run, opt_setup, opt_cleanup, opt_shareSetup) {
this.name = name;
this.timeToRun = 100; // ms.
// Tests like DOM/DOMWalk.html are too fast and need to be ran multiple times.
// 100 ms was chosen since it was long enough to make DOMWalk and other fast tests stable
// but was short enough not to make other slow tests run multiple times.
this.run = run;
this.setup = opt_setup;
this.cleanup = opt_cleanup;
this.shareSetup = opt_shareSetup;
}
// BenchmarkSuite
// A group of benchmarks that can be run together.
function BenchmarkSuite(name, benchmarks) {
this.name = name;
this.benchmarks = benchmarks;
for (var i = 0; i < this.benchmarks.length; i++)
this.benchmarks[i].suite = this;
this.suiteFile = this.currentSuiteFile;
}
// This computes the amount of overhead is associated with the call to the test
// function and getting the date.
BenchmarkSuite.start = PerfTestRunner.now();
BenchmarkSuite.Math = new (function() {
// Computes the geometric mean of a set of numbers.
// nulls in numbers will be ignored
// minNumber is optional (defaults to 0.001) anything smaller than this
// will be changed to this value, eliminating infinite results
// mapFn is an optional arg that will be used as a map function on numbers
this.GeometricMean = function(numbers, minNumber, mapFn) {
if (mapFn)
numbers = dojo.map(numbers, mapFn);
var log = 0;
var nNumbers = 0;
for (var i = 0, n = numbers.length; i < n; i++) {
var number = numbers[i];
if (number) {
if (number < minNumber)
number = minNumber;
nNumbers++;
log += Math.log(number);
}
}
return Math.pow(Math.E, log / nNumbers);
};
// Compares two objects using built-in JavaScript operators.
this.ascend = function(a, b) {
if (a < b)
return -1;
else if (a > b)
return 1;
return 0;
};
});
// Benchmark results hold the benchmark and the measured time used to run the
// benchmark. The benchmark score is computed later once a full benchmark suite
// has run to completion.
function BenchmarkResult(benchmark, times, error, benchmarkContent) {
this.benchmark = benchmark;
this.error = error;
this.times = times;
this.countNodes = function(parent) {
var nDescendants = 0;
for (var child = parent.firstChild; child; child = child.nextSibling)
nDescendants += countNodes(child);
return nDescendants + 1;
};
if (benchmarkContent) {
var nNodes = countNodes(benchmarkContent) - 1;
if (nNodes > 0) {
this.html = benchmarkContent.innerHTML;
this.nNodes = nNodes;
}
}
if (!error) {
var statistics = PerfTestRunner.computeStatistics(times);
this.min = statistics.min;
this.max = statistics.max;
this.median = statistics.median;
this.mean = statistics.mean;
this.sum = statistics.sum;
this.variance = statistics.variance;
this.stdev = statistics.stdev;
}
// Convert results to numbers. Used by the geometric mean computation.
this.valueOf = function() { return this.time; };
}
// Runs a single benchmark and computes the average time it takes to run a
// single iteration.
BenchmarkSuite.prototype.RunSingle = function(benchmark, times) {
var elapsed = 0;
var start = PerfTestRunner.now();
var runInterval = new Interval();
var setupReturn = null;
var runReturn = null;
var time;
var totalTime = 0;
var nZeros = 0;
var error = null;
var profiler = GetProfiler();
for (var n = 0; !error && totalTime < benchmark.timeToRun; n++) {
if (this.benchmarkContent)
this.benchmarkContentHolder.removeChild(this.benchmarkContent);
this.benchmarkContent = this.benchmarkContentProto.cloneNode();
this.benchmarkContentHolder.appendChild(this.benchmarkContent);
PerfTestRunner.gc();
try {
if (benchmark.setup) {
if (!setupReturn || !benchmark.shareSetup)
setupReturn = benchmark.setup();
}
profiler.start();
runInterval.start();
runReturn = benchmark.run(setupReturn);
runInterval.stop();
profiler.stop();
time = runInterval.microseconds() / 1000;
if (time > 0) {
times.push(time);
elapsed += time;
} else
times.push(0);
if (benchmark.cleanup)
benchmark.cleanup(runReturn);
} catch (e) {
error = e;
}
totalTime = PerfTestRunner.now() - start;
}
var result = new BenchmarkResult(benchmark, times, error, null);
if (this.benchmarkContent) {
this.benchmarkContentHolder.removeChild(this.benchmarkContent);
this.benchmarkContent = null;
}
return result;
};
BenchmarkSuite.prototype.generateTree = function(parent, width, depth) {
var id = parent.id;
if (depth !== 0) {
var middle = Math.floor(width / 2);
for (var i = 0; i < width; i++) {
if (i == middle) {
var divNode = document.createElement("div");
// TODO:[dave] this causes us to have potentially very long
// ids. We might want to encode these values into a smaller string
divNode.id = id + ':(' + i + ', 0)';
parent.appendChild(divNode);
this.generateTree(divNode, width, depth - 1);
} else {
var p = parent;
for (var j = 0; j < i; j++) {
var divNode = document.createElement("div");
divNode.id = id + ':(' + i + ',' + j + ')';
p.appendChild(divNode);
p = divNode;
}
var span = document.createElement("span");
span.appendChild(document.createTextNode(p.id));
p.appendChild(span);
}
}
}
};
// Generates a DOM tree (doesn't insert it into the document).
// The width and depth arguments help shape the "bushiness" of the full tree.
// The approach is to recursively generate a set of subtrees. At each root we
// generate width trees, of increasing depth, from 1 to width. Then we recurse
// from the middle child of this subtree. We do this up to depth times. reps
// allows us to duplicate a set of trees, without additional recursions.
BenchmarkSuite.prototype.generateDOMTree = function(width, depth, reps) {
var top = document.createElement("div");
top.id = "test";
for (var i = 0; i < reps; i++) {
var divNode = document.createElement("div");
divNode.id = "test" + i;
this.generateTree(divNode, width, depth);
top.appendChild(divNode);
}
return top;
};
// Generate a small sized tree.
// 92 span leaves, max depth: 23, avg depth: 14
BenchmarkSuite.prototype.generateSmallTree = function() {
return this.generateDOMTree(14, 12, 10);
};
// Generate a medium sized tree.
// 1320 span leaves, max depth: 27, avg depth: 16
BenchmarkSuite.prototype.generateMediumTree = function() {
return this.generateDOMTree(19, 13, 9);
};
// Generate a large sized tree.
// 2600 span leaves, max depth: 55, avg depth: 30
BenchmarkSuite.prototype.generateLargeTree = function() {
return this.generateDOMTree(26, 26, 4);
};
function runBenchmarkSuite(suite) {
PerfTestRunner.measureTime({run: function () {
var container = document.getElementById('container');
var content = document.getElementById('benchmark_content');
suite.benchmarkContentHolder = container;
suite.benchmarkContentProto = content;
var totalMeanTime = 0;
for (var j = 0; j < suite.benchmarks.length; j++) {
var result = suite.RunSingle(suite.benchmarks[j], []);
if (result.error)
log(result.error);
else
totalMeanTime += result.mean;
}
return totalMeanTime;
},
done: function () {
var container = document.getElementById('container');
if (container.firstChild)
container.removeChild(container.firstChild);
}});
}