Import Chromium's dom_perf test
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 27 Jan 2012 22:03:37 +0000 (22:03 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 27 Jan 2012 22:03:37 +0000 (22:03 +0000)
https://bugs.webkit.org/show_bug.cgi?id=77175

Reviewed by Adam Barth.

Import dom_perf.

Note resources/dom/suites.js isn't used by any html file yet but it will be used by Chromium port
once its perf bots start pulling test files from WebKit repository instead of Google's internal repository.

* DOM: Added.
* DOM/Accessors.html: Added.
* DOM/CloneNodes.html: Added.
* DOM/CreateNodes.html: Added.
* DOM/DOMDivWalk.html: Added.
* DOM/DOMTable.html: Added.
* DOM/DOMWalk.html: Added.
* DOM/Events.html: Added.
* DOM/GetElement.html: Added.
* DOM/GridSort.html: Added.
* DOM/Template.html: Added.
* DOM/resources: Added.
* DOM/resources/dom-perf.js: Added.
* DOM/resources/dom-perf: Added.
* DOM/resources/dom-perf/accessors.js: Added.
* DOM/resources/dom-perf/clonenodes.js: Added.
* DOM/resources/dom-perf/createnodes.js: Added.
* DOM/resources/dom-perf/domdivwalk.js: Added.
* DOM/resources/dom-perf/domtable.js: Added.
* DOM/resources/dom-perf/domwalk.js: Added.
* DOM/resources/dom-perf/events.js: Added.
* DOM/resources/dom-perf/getelement.js: Added.
* DOM/resources/dom-perf/gridsort.js: Added.
* DOM/resources/dom-perf/suites.js: Added.
* DOM/resources/dom-perf/template.js: Added.
* resources/runner.js:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@106147 268f45cc-cd09-0410-ab3c-d52691b4dbfc

24 files changed:
PerformanceTests/ChangeLog
PerformanceTests/DOM/Accessors.html [new file with mode: 0644]
PerformanceTests/DOM/CloneNodes.html [new file with mode: 0644]
PerformanceTests/DOM/CreateNodes.html [new file with mode: 0644]
PerformanceTests/DOM/DOMDivWalk.html [new file with mode: 0644]
PerformanceTests/DOM/DOMTable.html [new file with mode: 0644]
PerformanceTests/DOM/DOMWalk.html [new file with mode: 0644]
PerformanceTests/DOM/Events.html [new file with mode: 0644]
PerformanceTests/DOM/GetElement.html [new file with mode: 0644]
PerformanceTests/DOM/GridSort.html [new file with mode: 0644]
PerformanceTests/DOM/Template.html [new file with mode: 0644]
PerformanceTests/DOM/resources/dom-perf.js [new file with mode: 0644]
PerformanceTests/DOM/resources/dom-perf/accessors.js [new file with mode: 0644]
PerformanceTests/DOM/resources/dom-perf/clonenodes.js [new file with mode: 0644]
PerformanceTests/DOM/resources/dom-perf/createnodes.js [new file with mode: 0644]
PerformanceTests/DOM/resources/dom-perf/domdivwalk.js [new file with mode: 0644]
PerformanceTests/DOM/resources/dom-perf/domtable.js [new file with mode: 0644]
PerformanceTests/DOM/resources/dom-perf/domwalk.js [new file with mode: 0644]
PerformanceTests/DOM/resources/dom-perf/events.js [new file with mode: 0644]
PerformanceTests/DOM/resources/dom-perf/getelement.js [new file with mode: 0644]
PerformanceTests/DOM/resources/dom-perf/gridsort.js [new file with mode: 0644]
PerformanceTests/DOM/resources/dom-perf/suites.js [new file with mode: 0644]
PerformanceTests/DOM/resources/dom-perf/template.js [new file with mode: 0644]
PerformanceTests/resources/runner.js

index 99411a3..c250215 100644 (file)
@@ -1,3 +1,42 @@
+2012-01-26  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Import Chromium's dom_perf test
+        https://bugs.webkit.org/show_bug.cgi?id=77175
+
+        Reviewed by Adam Barth.
+
+        Import dom_perf.
+
+        Note resources/dom/suites.js isn't used by any html file yet but it will be used by Chromium port
+        once its perf bots start pulling test files from WebKit repository instead of Google's internal repository.
+
+        * DOM: Added.
+        * DOM/Accessors.html: Added.
+        * DOM/CloneNodes.html: Added.
+        * DOM/CreateNodes.html: Added.
+        * DOM/DOMDivWalk.html: Added.
+        * DOM/DOMTable.html: Added.
+        * DOM/DOMWalk.html: Added.
+        * DOM/Events.html: Added.
+        * DOM/GetElement.html: Added.
+        * DOM/GridSort.html: Added.
+        * DOM/Template.html: Added.
+        * DOM/resources: Added.
+        * DOM/resources/dom-perf.js: Added.
+        * DOM/resources/dom-perf: Added.
+        * DOM/resources/dom-perf/accessors.js: Added.
+        * DOM/resources/dom-perf/clonenodes.js: Added.
+        * DOM/resources/dom-perf/createnodes.js: Added.
+        * DOM/resources/dom-perf/domdivwalk.js: Added.
+        * DOM/resources/dom-perf/domtable.js: Added.
+        * DOM/resources/dom-perf/domwalk.js: Added.
+        * DOM/resources/dom-perf/events.js: Added.
+        * DOM/resources/dom-perf/getelement.js: Added.
+        * DOM/resources/dom-perf/gridsort.js: Added.
+        * DOM/resources/dom-perf/suites.js: Added.
+        * DOM/resources/dom-perf/template.js: Added.
+        * resources/runner.js:
+
 2012-01-25  Ryosuke Niwa  <rniwa@webkit.org>
 
         html5-full-render.html fails due to a log
diff --git a/PerformanceTests/DOM/Accessors.html b/PerformanceTests/DOM/Accessors.html
new file mode 100644 (file)
index 0000000..fbad29f
--- /dev/null
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+<div id="container"><span id="benchmark_content"></span></div>
+<pre id="log"></pre>
+<script type="text/javascript" src="../resources/runner.js"></script>
+<script type="text/javascript" src="resources/dom-perf.js"></script>
+<script type="text/javascript" src="resources/dom-perf/accessors.js"></script>
+<script> runBenchmarkSuite(AccessorsTest); </script>
+</body>
+</html>
diff --git a/PerformanceTests/DOM/CloneNodes.html b/PerformanceTests/DOM/CloneNodes.html
new file mode 100644 (file)
index 0000000..a8dbbaa
--- /dev/null
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+<div id="container"><span id="benchmark_content"></span></div>
+<pre id="log"></pre>
+<script type="text/javascript" src="../resources/runner.js"></script>
+<script type="text/javascript" src="resources/dom-perf.js"></script>
+<script type="text/javascript" src="resources/dom-perf/clonenodes.js"></script>
+<script> runBenchmarkSuite(CloneNodesTest); </script>
+</body>
+</html>
diff --git a/PerformanceTests/DOM/CreateNodes.html b/PerformanceTests/DOM/CreateNodes.html
new file mode 100644 (file)
index 0000000..31f3e04
--- /dev/null
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+<div id="container"><span id="benchmark_content"></span></div>
+<pre id="log"></pre>
+<script type="text/javascript" src="../resources/runner.js"></script>
+<script type="text/javascript" src="resources/dom-perf.js"></script>
+<script type="text/javascript" src="resources/dom-perf/createnodes.js"></script>
+<script> runBenchmarkSuite(CreateNodesTest); </script>
+</body>
+</html>
diff --git a/PerformanceTests/DOM/DOMDivWalk.html b/PerformanceTests/DOM/DOMDivWalk.html
new file mode 100644 (file)
index 0000000..960cf69
--- /dev/null
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+<div id="container"><span id="benchmark_content"></span></div>
+<pre id="log"></pre>
+<script type="text/javascript" src="../resources/runner.js"></script>
+<script type="text/javascript" src="resources/dom-perf.js"></script>
+<script type="text/javascript" src="resources/dom-perf/domdivwalk.js"></script>
+<script> runBenchmarkSuite(DOMDivWalkTest); </script>
+</body>
+</html>
diff --git a/PerformanceTests/DOM/DOMTable.html b/PerformanceTests/DOM/DOMTable.html
new file mode 100644 (file)
index 0000000..3badc8a
--- /dev/null
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+<div id="container"><span id="benchmark_content"></span></div>
+<pre id="log"></pre>
+<script type="text/javascript" src="../resources/runner.js"></script>
+<script type="text/javascript" src="resources/dom-perf.js"></script>
+<script type="text/javascript" src="resources/dom-perf/domtable.js"></script>
+<script> runBenchmarkSuite(DOMTableTest); </script>
+</body>
+</html>
diff --git a/PerformanceTests/DOM/DOMWalk.html b/PerformanceTests/DOM/DOMWalk.html
new file mode 100644 (file)
index 0000000..d270560
--- /dev/null
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+<div id="container"><span id="benchmark_content"></span></div>
+<pre id="log"></pre>
+<script type="text/javascript" src="../resources/runner.js"></script>
+<script type="text/javascript" src="resources/dom-perf.js"></script>
+<script type="text/javascript" src="resources/dom-perf/domwalk.js"></script>
+<script> runBenchmarkSuite(DOMWalkTest); </script>
+</body>
+</html>
diff --git a/PerformanceTests/DOM/Events.html b/PerformanceTests/DOM/Events.html
new file mode 100644 (file)
index 0000000..5b5e089
--- /dev/null
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+<div id="container"><span id="benchmark_content"></span></div>
+<pre id="log"></pre>
+<script type="text/javascript" src="../resources/runner.js"></script>
+<script type="text/javascript" src="resources/dom-perf.js"></script>
+<script type="text/javascript" src="resources/dom-perf/events.js"></script>
+<script> runBenchmarkSuite(EventTest); </script>
+</body>
+</html>
diff --git a/PerformanceTests/DOM/GetElement.html b/PerformanceTests/DOM/GetElement.html
new file mode 100644 (file)
index 0000000..7d18b9b
--- /dev/null
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+<div id="container"><span id="benchmark_content"></span></div>
+<pre id="log"></pre>
+<script type="text/javascript" src="../resources/runner.js"></script>
+<script type="text/javascript" src="resources/dom-perf.js"></script>
+<script type="text/javascript" src="resources/dom-perf/getelement.js"></script>
+<script> runBenchmarkSuite(GetElementTest); </script>
+</body>
+</html>
diff --git a/PerformanceTests/DOM/GridSort.html b/PerformanceTests/DOM/GridSort.html
new file mode 100644 (file)
index 0000000..82fbfc9
--- /dev/null
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+<div id="container"><span id="benchmark_content"></span></div>
+<pre id="log"></pre>
+<script type="text/javascript" src="../resources/runner.js"></script>
+<script type="text/javascript" src="resources/dom-perf.js"></script>
+<script type="text/javascript" src="resources/dom-perf/gridsort.js"></script>
+<script> runBenchmarkSuite(GridSortTest); </script>
+</body>
+</html>
diff --git a/PerformanceTests/DOM/Template.html b/PerformanceTests/DOM/Template.html
new file mode 100644 (file)
index 0000000..8241ccc
--- /dev/null
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+<div id="container"><span id="benchmark_content"></span></div>
+<pre id="log"></pre>
+<script type="text/javascript" src="../resources/runner.js"></script>
+<script type="text/javascript" src="resources/dom-perf.js"></script>
+<script type="text/javascript" src="resources/dom-perf/template.js"></script>
+<script> runBenchmarkSuite(TemplateTest); </script>
+</body>
+</html>
diff --git a/PerformanceTests/DOM/resources/dom-perf.js b/PerformanceTests/DOM/resources/dom-perf.js
new file mode 100644 (file)
index 0000000..2cc4e08
--- /dev/null
@@ -0,0 +1,405 @@
+/*
+ * 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_ = new Date(); };
+  this.stop = function() { stop_ = new Date(); };
+  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();
+}
+
+// To make the benchmark results predictable, we replace Math.random with a
+// 100% deterministic alternative.
+Math.random = (function() {
+    var seed = 49734321;
+    return function() {
+        // Robert Jenkins' 32 bit integer hash function.
+        seed = ((seed + 0x7ed55d16) + (seed << 12))  & 0xffffffff;
+        seed = ((seed ^ 0xc761c23c) ^ (seed >>> 19)) & 0xffffffff;
+        seed = ((seed + 0x165667b1) + (seed << 5))   & 0xffffffff;
+        seed = ((seed + 0xd3a2646c) ^ (seed << 9))   & 0xffffffff;
+        seed = ((seed + 0xfd7046c5) + (seed << 3))   & 0xffffffff;
+        seed = ((seed ^ 0xb55a4f09) ^ (seed >>> 16)) & 0xffffffff;
+        return (seed & 0xfffffff) / 0x10000000;
+    };
+})();
+
+function Benchmark(name, run, opt_setup, opt_cleanup, opt_shareSetup) {
+    this.name = name;
+    this.timeToRun = 500; // ms
+    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 = new Date();
+
+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 data = times.slice();
+        var count = data.length;
+
+        // Sort the data so that all seemingly
+        // insignificant values such as 0.000000003 will
+        // be at the beginning of the array and their
+        // contribution to the mean and variance of the
+        // data will not be lost because of the precision
+        // of the CPU.
+        data.sort(BenchmarkSuite.Math.ascend);
+
+        // Since the data is now sorted, the minimum value
+        // is at the beginning of the array, the median
+        // value is in the middle of the array, and the
+        // maximum value is at the end of the array.
+        this.min = data[0];
+        var middle = Math.floor(data.length / 2);
+        if ((data.length % 2) !== 0)
+            this.median = data[middle];
+        else
+            this.median = (data[middle - 1] + data[middle]) / 2;
+        this.max = data[data.length - 1];
+
+        // Compute the mean and variance using a
+        // numerically stable algorithm.
+        var sqsum = 0;
+        this.mean = data[0];
+        var nZeros = 0;
+        for (var i = 1; i < data.length; ++i) {
+            var x = data[i];
+            var delta = x - this.mean;
+            var sweep = i + 1.0;
+            this.mean += delta / sweep;
+            sqsum += delta * delta * (i / sweep);
+        }
+        this.sum = this.mean * count;
+        this.variance = sqsum / count;
+
+        this.sdev = Math.sqrt(this.variance);
+        this.score = 1000 / this.mean;
+    }
+
+    this.toString = function() {
+        var s =
+          " min: " + this.min + 
+          " max: " + this.max + 
+          " mean: " + this.mean + 
+          " median: " + this.median + 
+          " sdev: " + this.sdev;
+        return s;
+    };
+
+    // 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 = new Date();
+    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);
+        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 = new Date() - 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) {
+    startCustom(20, 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;
+    }, function () {
+        var container = document.getElementById('container');
+        if (container.firstChild)
+            container.removeChild(container.firstChild);
+    });
+}
diff --git a/PerformanceTests/DOM/resources/dom-perf/accessors.js b/PerformanceTests/DOM/resources/dom-perf/accessors.js
new file mode 100644 (file)
index 0000000..4386611
--- /dev/null
@@ -0,0 +1,154 @@
+/*
+ * 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.
+ */
+
+// Accessors - measure access get/set properties on various objects.
+
+// Accessors are generally pretty quick.  We loop on each accessor
+// many times in order to crank up the time of the microbenchmark
+// so that measurements are more substantial.  Values were chosen
+// to make Firefox3 performance land in the 100-300ms range.
+var kBigCount = 1500000;
+var kLittleCount = 300000;
+var kTinyCount = 30000;
+var kVeryTinyCount = 5000;
+
+var Accessors = {};
+
+Accessors.WindowGet = function() {
+    var nLoops = kLittleCount;
+    for (var loop = 0; loop < nLoops; loop++)
+        window.length;
+}
+
+Accessors.WindowSet = function() {
+    var nLoops = kBigCount;
+    for (var loop = 0; loop < kLittleCount; loop++)
+        window.name = "title";
+}
+
+Accessors.RootGet = function() {
+    var nLoops = kLittleCount;
+    for (var loop = 0; loop < nLoops; loop++)
+        length;
+}
+
+Accessors.RootSet = function() {
+    var nLoops = kLittleCount;
+    for (var loop = 0; loop < nLoops; loop++)
+        title = "name";
+}
+
+Accessors.DocumentGet = function() {
+    var nLoops = kLittleCount;
+    for (var loop = 0; loop < nLoops; loop++)
+        document.nodeType;
+}
+
+Accessors.DocumentSet = function() {
+    var nLoops = kVeryTinyCount;
+    for (var loop = 0; loop < nLoops; loop++)
+        document.title = "name";
+}
+
+Accessors.DOMObjectSetup = function() {
+    var o1 = document.createElement("span");
+    var o2 = document.createElement("span");
+    o1.appendChild(o2);
+    this.suite.benchmarkContent.appendChild(o1);
+    return o1;
+}
+
+Accessors.DOMObjectGet = function(o1) {
+    var nLoops = kLittleCount;
+    for (var loop = 0; loop < nLoops; loop++)
+        o1.nodeType;
+}
+
+Accessors.DOMObjectSet = function(o1) {
+    var nLoops = kLittleCount;
+    var title = "title";
+    for (var loop = 0; loop < nLoops; loop++)
+        o1.title = title;
+}
+
+Accessors.ObjectGet = function(o1) {
+    var nLoops = kBigCount;
+    for (var loop = 0; loop < nLoops; loop++)
+        o1.nodeType;
+}
+
+Accessors.NodeListSetup = function() {
+    var o1 = document.createElement("span");
+    var o2 = document.createElement("span");
+    o1.appendChild(o2);
+    this.suite.benchmarkContent.appendChild(o1);
+    return o1.childNodes;
+}
+
+Accessors.NodeListGet = function(o1) {
+    var nLoops = kLittleCount;
+    for (var loop = 0; loop < nLoops; loop++)
+        o1.length;
+}
+
+Accessors.CSSSetup = function() {
+    var span = document.createElement("span");
+    span.appendChild(document.createTextNode("test"));
+    span.style.fontWeight = "bold";
+    this.suite.benchmarkContent.appendChild(span);
+    return span;
+}
+  
+
+Accessors.CSSGet = function(span) {
+    var nLoops = kLittleCount;
+    for (var loop = 0; loop < nLoops; loop++)
+        span.style.fontWeight;
+}
+
+Accessors.CSSSet = function(span) {
+    var nLoops = kTinyCount;
+    for (var loop = 0; loop < nLoops; loop++)
+        span.style.fontWeight = "bold";
+}
+
+var AccessorsTest = new BenchmarkSuite('Accessors', [
+    new Benchmark("CSS Style Get", Accessors.CSSGet, Accessors.CSSSetup),
+    new Benchmark("CSS Style Set", Accessors.CSSSet, Accessors.CSSSetup),
+    new Benchmark("Document Get NodeType", Accessors.DocumentGet),
+    new Benchmark("Document Set Title", Accessors.DocumentSet),
+    new Benchmark("Nodelist Get Length", Accessors.NodeListGet, Accessors.NodeListSetup),
+    new Benchmark("Span Get NodeType", Accessors.DOMObjectGet, Accessors.DOMObjectSetup),
+    new Benchmark("Span Set Title", Accessors.DOMObjectSet, Accessors.DOMObjectSetup),
+    new Benchmark("Root Get Length", Accessors.RootGet),
+    new Benchmark("Root Set Title", Accessors.RootSet),
+    new Benchmark("Window Get Length", Accessors.WindowGet),
+    new Benchmark("Window Set Name", Accessors.WindowSet)
+]);
diff --git a/PerformanceTests/DOM/resources/dom-perf/clonenodes.js b/PerformanceTests/DOM/resources/dom-perf/clonenodes.js
new file mode 100644 (file)
index 0000000..3b54d0f
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * 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.
+ */
+
+// Tests cloning and appending small and medium DOM trees.
+function CloneNodes(size, style) {
+    var me = this;
+    this.size = size;
+    this.style = style;
+
+    this.name = me.size + ', ' + me.style;
+
+    var nClones = 0;
+
+    this.Setup = function() {
+        // getDOMTree will initialize the global tree if necessary.
+        me.domTree = me.getDOMTree();
+        if (me.style == 'append')
+            me.domTree = me.domTree.cloneNode(true);
+    };
+
+    this.Test = function() {
+        var kIterations = 10;
+        if (me.style == 'clone') {
+            for (var iterations = 0; iterations < kIterations; iterations++)
+                me.domTree.cloneNode(true);
+        } else {
+            for (var iterations = 0; iterations < kIterations; iterations++)
+                this.suite.benchmarkContent.appendChild(me.domTree);
+        }
+    };
+
+    // Returns a tree of size me.size from the pool in the prototype, creating 
+    // one if needed.
+    this.getDOMTree = function() {
+        var domTree = me.Trees[me.size];
+        if (!domTree) {
+            switch (this.size) {
+            case 'small':
+                domTree = BenchmarkSuite.prototype.generateSmallTree();
+                break;
+            case 'medium':
+                domTree = BenchmarkSuite.prototype.generateMediumTree();
+                break;
+            case 'large':
+                domTree = BenchmarkSuite.prototype.generateLargeTree();
+                break;
+            }
+            me.Trees[me.size] = domTree;
+        }
+        return domTree;
+    };
+
+    this.GetBenchmark = function() {
+        return new Benchmark(this.name, this.Test, this.Setup);
+    };
+};
+
+CloneNodes.prototype = {
+    Sizes: ['small', 'medium'],
+    Styles: ['clone', 'append'],
+    Trees: {}
+};
+
+// Generate a test for each size/style combination.
+var benchmarks = [];
+CloneNodes.prototype.Sizes.forEach(function(size) {
+    CloneNodes.prototype.Styles.forEach(function(style) {
+        benchmarks.push(new CloneNodes(size, style).GetBenchmark());
+    });
+});
+
+var CloneNodesTest = new BenchmarkSuite('CloneNodes', benchmarks);
diff --git a/PerformanceTests/DOM/resources/dom-perf/createnodes.js b/PerformanceTests/DOM/resources/dom-perf/createnodes.js
new file mode 100644 (file)
index 0000000..8c35bf7
--- /dev/null
@@ -0,0 +1,229 @@
+/*
+ * 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.
+ */
+
+// CreateNodes
+// Test mechanisms for creating and inserting nodes into a DOM tree.
+//
+// There are many ways to add nodes into a DOM tree.  This test exercises
+// use of:
+//    - createElement()/appendChild()
+//    - createDocumentFragment()
+//    - innerHTML()
+//    - spans
+
+function CreateNodes() {}
+
+CreateNodes.nIterations = 10000;
+CreateNodes.nIterationsSlow = 2000;
+CreateNodes.currentIterations = CreateNodes.nIterations;
+CreateNodes.nodeProto = null;
+
+CreateNodes.createNode = function() {
+    if (!CreateNodes.nodeProto) {
+        var html = this.getNodeHTML();
+        var span = document.createElement("span");
+        span.innerHTML = html;
+        CreateNodes.nodeProto = span.firstChild;
+    }
+    return CreateNodes.nodeProto;
+};
+
+CreateNodes.getNodeHTML = function() {
+    return "<div style=\"font-style:bold\">test</div>";
+};
+
+CreateNodes.appendNodesWithDOM = function(node) {
+    var nodes = CreateNodes.createNodesWithDOM(node);
+    for (var i = 0; i < CreateNodes.nIterations; i++)
+        this.suite.benchmarkContent.appendChild(nodes[i]);
+    CreateNodes.forceNode(this);
+};
+
+CreateNodes.justAppendNodesWithDOM = function(nodes) {
+    for (var i = 0; i < CreateNodes.nIterations; i++)
+        this.suite.benchmarkContent.appendChild(nodes[i]);
+};
+
+CreateNodes.createNodesWithHTML = function(html) {
+    var allHTML = "";
+    for (var i = 0; i < CreateNodes.currentIterations; i++)
+        allHTML += html;
+    return allHTML;
+};
+
+CreateNodes.checkNodes = function() {
+    var length = this.suite.benchmarkContent.childNodes.length;
+    var count = CreateNodes.currentIterations;
+    if (length != count)
+        throw "Should have " + count + " nodes, have: " + length;
+};
+
+// Ensures that the node has really been created and not just delayed
+CreateNodes.forceNode = function(benchmark) {
+    var child =  benchmark.suite.benchmarkContent.childNodes[CreateNodes.nIterations / 2];
+};
+
+CreateNodes.appendNodesWithHTML = function(html) {
+    var allHTML = CreateNodes.createNodesWithHTML(html);
+    this.suite.benchmarkContent.innerHTML = allHTML;
+    CreateNodes.forceNode(this);
+};
+
+CreateNodes.createNodesWithDOM = function(node) {
+    var nodes = [];
+    for (var i = 0; i < CreateNodes.nIterations; i++)
+        nodes.push(node.cloneNode(true));
+    return nodes;
+};
+
+CreateNodes.createNodesWithDOMSetup = function() {
+    return createNodesWithDOM(createNode());
+};
+
+CreateNodes.createNodeSetup = function() {
+    CreateNodes.currentIterations = CreateNodes.nIterations;
+    return CreateNodes.createNode();
+};
+
+CreateNodes.createNodesWithHTMLUsingSpans = function(html) {
+    var spans = [];
+    for (var i = 0; i < CreateNodes.currentIterations; i++) {
+        var spanNode = document.createElement("span");
+        spanNode.innerHTML = html;
+        spans.push(spanNode);
+    }
+    return spans;
+};
+
+CreateNodes.appendNodesWithHTMLUsingSpans = function(html) {
+    var spans = CreateNodes.createNodesWithHTMLUsingSpans(html);
+    for (var i = 0; i < CreateNodes.currentIterations; i++)
+        this.suite.benchmarkContent.appendChild(spans[i]);
+    CreateNodes.forceNode(this);
+};
+
+CreateNodes.appendNodesWithHTMLUsingDocumentFragments = function(html) {
+    var fragments = CreateNodes.createNodesWithHTMLUsingDocumentFragments(html);
+    for (var i = 0; i < CreateNodes.nIterations; i++)
+        this.suite.benchmarkContent.appendChild(fragments[i]);
+    CreateNodes.forceNode(this);
+};
+
+CreateNodes.createNodesWithHTMLUsingDocumentFragments = function(html) {
+    var fragments = [];
+    for (var i = 0; i < CreateNodes.nIterations; i++) {
+        var fragment = document.createDocumentFragment();
+        fragment.innerHTML = html;
+        fragments.push(fragment);
+    }
+    return fragments;
+};
+
+CreateNodes.appendNodesWithDOMUsingDocumentFragment = function(node) {
+    var fragment = CreateNodes.createNodesWithDOMUsingDocumentFragment(node);
+    this.suite.benchmarkContent.appendChild(fragment);
+    CreateNodes.forceNode(this);
+};
+
+CreateNodes.appendNodesWithDOMUsingSharedDocumentFragment = function(fragment) {
+    this.suite.benchmarkContent.appendChild(fragment.cloneNode(true));
+    CreateNodes.forceNode(this);
+};
+
+CreateNodes.createNodesWithDOMUsingDocumentFragment = function(node) {
+    var nodes = CreateNodes.createNodesWithDOM(node);
+    var fragment = document.createDocumentFragment();
+    for (var i = 0; i < CreateNodes.nIterations; i++)
+        fragment.appendChild(nodes[i]);
+    return fragment;
+};
+
+CreateNodes.createSharedDocumentFragment = function() {
+    var nodes = CreateNodes.createNodesWithDOM(CreateNodes.createNode());
+    var fragment = document.createDocumentFragment();
+    for (var i = 0; i < CreateNodes.nIterations; i++)
+        fragment.appendChild(nodes[i]);
+    return fragment;
+};
+
+CreateNodes.createHTMLSetup = function() {
+    CreateNodes.currentIterations = CreateNodes.nIterationsSlow;
+    return CreateNodes.getNodeHTML();
+};
+
+CreateNodes.createIFramesHTML = function() {
+    var html = [];
+    for (var i = 0; i < 100; i++)
+        html.push("<iframe src='blank.html'></iframe>");
+    return html.join('');
+}
+
+CreateNodes.appendIFramesHTML = function(html) {
+    this.suite.benchmarkContent.innerHTML = html;
+}
+
+CreateNodes.createIFramesDOM = function() {
+    var nodes = [];
+    for (var i = 0; i < 100; i++) {
+        var iframe = document.createElement('iframe');
+        iframe.src = 'blank.html';
+        nodes.push(iframe);
+    }
+    return nodes;
+}
+
+CreateNodes.appendIFramesDOM = function(nodes) {
+    var content = this.suite.benchmarkContent;
+    for (var i = 0, l = nodes.length; i < l; i++)
+        content.appendChild(nodes[i]);
+}
+
+var CreateNodesTest = new BenchmarkSuite('CreateNodes', [
+    new Benchmark("append, DOM, DocumentFragment",
+        CreateNodes.appendNodesWithDOMUsingDocumentFragment, CreateNodes.createNodeSetup, CreateNodes.checkNodes),
+    new Benchmark("create, DOM, DocumentFragment",
+        CreateNodes.createNodesWithDOMUsingDocumentFragment, CreateNodes.createNodeSetup),
+    new Benchmark("append, DOM, SharedDocumentFragment",
+        CreateNodes.appendNodesWithDOMUsingSharedDocumentFragment, CreateNodes.createSharedDocumentFragment,  CreateNodes.checkNodes),
+    new Benchmark("create, DOM",
+        CreateNodes.createNodesWithDOM, CreateNodes.createNodeSetup),
+    new Benchmark("append, DOM", 
+        CreateNodes.appendNodesWithDOM, CreateNodes.createNodeSetup, CreateNodes.checkNodes),
+    new Benchmark("append, DOM, iFrame",
+        CreateNodes.appendIFramesDOM, CreateNodes.createIFramesDOM),
+    new Benchmark("append, HTML", 
+        CreateNodes.appendNodesWithHTML, CreateNodes.createHTMLSetup, CreateNodes.checkNodes),
+    new Benchmark("create, HTML, Spans", 
+        CreateNodes.createNodesWithHTMLUsingSpans, CreateNodes.createHTMLSetup),
+    new Benchmark("append, HTML, Spans",
+        CreateNodes.appendNodesWithHTMLUsingSpans, CreateNodes.createHTMLSetup, CreateNodes.checkNodes),
+    new Benchmark("append, HTML, iFrame",
+        CreateNodes.appendIFramesHTML, CreateNodes.createIFramesHTML)
+]);
diff --git a/PerformanceTests/DOM/resources/dom-perf/domdivwalk.js b/PerformanceTests/DOM/resources/dom-perf/domdivwalk.js
new file mode 100644 (file)
index 0000000..e317023
--- /dev/null
@@ -0,0 +1,175 @@
+/*
+ * 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.
+ */
+
+// DOMDivWalk
+// Tests iteration of a DOM tree which is comprised of a hierarchical
+// set of div sections.
+//
+// Two mechanisms are used to iterate the DOM tree:
+//    - recursive iteration via Node.children
+//    - document.getElementsByTagName
+//
+// Various tree sizes are tested:
+//    - small, medium, and large DOMs
+//
+// Because this test is run repeatedly on the same DOM tree, the first
+// iteration may cause creation of the JS DOM Objects, and subsequent
+// iterations will use the cached instance of that JS Object.
+//
+
+// These tests try walking over different DOM trees full of divs
+// First create a DDWalkTest, using the above values,
+// then call GetBenchmark()
+function DDWalkTest(size, appendStyle, traverseStyle) {
+    var me = this;
+    this.size = size;
+    this.appendStyle = appendStyle;
+    this.traverseStyle = traverseStyle;
+    this.nodesFound = 0;
+
+    this.name = (this.Sizes.length > 1 ? (me.size + ", ") : "") + me.appendStyle + ", " + me.traverseStyle;
+
+    this.setupDOM = function() {
+        var domTree = me.getDOMTree().cloneNode(true);
+        this.suite.benchmarkContent.appendChild(domTree);
+        me.nodesFound = 0;
+        return domTree;
+    };
+
+    this.setupHTML = function() {
+        this.suite.benchmarkContent.innerHTML = me.getDOMTree().innerHTML;
+        me.nodesFound = 0;
+    };
+  
+    if (this.appendStyle == "DOM")
+        this.Setup = this.setupDOM;
+    else
+        this.Setup = this.setupHTML;
+  
+    this.recurseChildNodes = function(handle) {
+        function walkSubtree(parent, depth) {
+            if (me.nodesFound == me.maxNodesFound)
+                return;
+            else {
+                if (parent.nodeName == "SPAN" && depth > 0)
+                    me.nodesFound++;
+                var children = parent.childNodes;
+                var nChildren = children.length;
+                while (nChildren-- > 0) {
+                    var child = children[nChildren];
+                    walkSubtree(child, depth + 1);
+                }
+            }
+        }
+        walkSubtree(this.suite.benchmarkContent, 0);
+    };
+  
+    this.recurseSiblings = function(handle) {
+        function walkSubtree(parent, depth) {
+            if (me.nodesFound == me.maxNodesFound)
+                return;
+            else {
+                if (parent.nodeName == "SPAN" && depth > 0)
+                    me.nodesFound++;
+                for (var child = parent.firstChild; child !== null; child = child.nextSibling)
+                  walkSubtree(child, depth + 1);
+            }
+        }
+        walkSubtree(this.suite.benchmarkContent, 0);
+    };
+
+    this.getElements = function(handle) {
+        var kIterations = 10;
+
+        for (var iterations = 0; iterations < kIterations; iterations++) {
+            me.nodesFound = 0;
+            var items = this.suite.benchmarkContent.getElementsByTagName("span");
+            for (var index = 0, length = items.length; index < length; ++index) {
+                var item = items[index];
+                var nodeName = item.nodeName;
+                ++me.nodesFound;
+                if (me.nodesFound == me.maxNodesFound)
+                    return;
+            }
+        }
+    };
+
+    this.Test = this[this.traverseStyle];
+
+    this.Cleanup = function(handle) {
+        var expectedCount = me.ExpectedCounts[me.size];
+        if (me.nodesFound != expectedCount)
+            throw "Wrong number of nodes found: " + me.nodesFound + " expected: " + expectedCount;
+    };
+
+    this.GetBenchmark = function() {
+        return new Benchmark(this.name, this.Test, this.Setup, this.Cleanup);    
+    };
+
+    this.getDOMTree = function() {
+        var domTree = me.Trees[me.size];
+        if (!domTree) {
+            switch (me.size) {
+            case "small" : domTree = BenchmarkSuite.prototype.generateSmallTree(); break;
+            case "medium" : domTree = BenchmarkSuite.prototype.generateMediumTree(); break;
+            case "large" : domTree = BenchmarkSuite.prototype.generateLargeTree(); break;
+            }
+            me.Trees[me.size] = domTree;
+        }
+        return domTree;
+    };
+}
+
+DDWalkTest.prototype = {
+    // Different sizes are possible, but we try to keep the runtime and memory
+    // consumption reasonable.
+    Sizes: ["medium"],
+    Trees: {},
+    AppendStyles: ["DOM", "HTML"],
+    TraverseStyles: ["recurseChildNodes", "recurseSiblings", "getElements"],
+    ExpectedCounts : {"small" : 90, "medium": 2106, "large" : 2000},
+
+    // Limits the number of nodes to this number
+    // If we don't do this the tests can take too long to run
+    maxNodesFound : 10000
+};
+
+
+// Generate the matrix of all benchmarks
+var benchmarks = [];
+DDWalkTest.prototype.Sizes.forEach(function(size) {
+    DDWalkTest.prototype.AppendStyles.forEach(function(appendStyle) {
+        DDWalkTest.prototype.TraverseStyles.forEach(function(traverseStyle) {
+            benchmarks.push(new DDWalkTest(size, appendStyle, traverseStyle).GetBenchmark());
+        });
+    });
+});
+
+var DOMDivWalkTest = new BenchmarkSuite("DOMDivWalk", benchmarks);
diff --git a/PerformanceTests/DOM/resources/dom-perf/domtable.js b/PerformanceTests/DOM/resources/dom-perf/domtable.js
new file mode 100644 (file)
index 0000000..c2e629b
--- /dev/null
@@ -0,0 +1,181 @@
+/*
+ * 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.
+ */
+
+// DOMTable - a benchmark creating tables and accessing table elements
+//
+// This benchmark tests different mechanisms for creating an HTML table. By
+// either creating the DOM elements individually or by creating an inner HTML
+// as a string. The effect of forcing a layout is also measured.
+// A second part of the benchmark sums the contents of all elements. Again
+// in one set the benchmark is created using DOM functions from JavaScript,
+// causing all nodes to be prewrapped, while in a second set the table is
+// created using inner HTML which will wrap the elements at access.
+
+// Size of the created tables.
+var DOMTable_maxRows = 100;
+var DOMTable_maxCols = 40;
+
+// Helper variable to create consistent values for the table elements.
+var DOMTable_element_count = 0;
+
+// Functions needed to create a table by creating individual DOM elements.
+function DOMTable_CreateCell(row_id, col_id) {
+    var cell = document.createElement("td");
+    cell.id = "$" + row_id + "$" + col_id;
+    cell.textContent = DOMTable_element_count++;
+    return cell;
+}
+
+function DOMTable_CreateRow(row_id, cols) {
+    var row = document.createElement("tr");
+    for (var i = 0; i < cols; i++)
+        row.appendChild(DOMTable_CreateCell(row_id, i));
+    return row;
+}
+
+function DOMTable_CreateTable(rows, cols) {
+    var table = document.createElement("table");
+    for (var i = 0; i < rows; i++)
+        table.appendChild(DOMTable_CreateRow(i, cols));
+    return table;
+}
+
+// Functions needed to create a table by creating a big piece of HTML in a
+// single string.
+function DOMTable_CreateCellIH(row_id, col_id) {
+    return '<td id="$' + row_id + '$' + col_id + '">' + DOMTable_element_count++ + '</td>';
+}
+
+function DOMTable_CreateRowIH(row_id, cols) {
+    var html_string = '<tr>';
+    for (var i = 0; i < cols; i++)
+        html_string += DOMTable_CreateCellIH(row_id, i);
+    return html_string + '</tr>';
+}
+
+function DOMTable_CreateTableIH(rows, cols) {
+    var html_string = '<table>';
+    for (var i = 0; i < rows; i++)
+        html_string += DOMTable_CreateRowIH(i, cols);
+    return html_string + '</table>';
+}
+
+
+// Shared setup function for all table creation tests.
+function DOMTable_CreateSetup() {
+    DOMTable_element_count = 0;
+    return document.getElementById("benchmark_content");
+}
+
+function DOMTable_Create(root_element) {
+    // Create the table and add it to the root_element for the benchmark.
+    root_element.appendChild(DOMTable_CreateTable(DOMTable_maxRows, DOMTable_maxCols));
+    return root_element;
+}
+
+function DOMTable_CreateLayout(root_element) {
+    // Create the table and add it to the root_element for the benchmark.
+    root_element.appendChild(DOMTable_CreateTable(DOMTable_maxRows, DOMTable_maxCols));
+    // Force a layout by requesting the height of the table. The result is
+    // going to be ignored because there is not cleanup function registered.
+    return root_element.scrollHeight;
+}
+
+function DOMTable_InnerHTML(root_element) {
+    // Create the HTML string for the table and set it at the root_element for the benchmark.
+    root_element.innerHTML = DOMTable_CreateTableIH(DOMTable_maxRows, DOMTable_maxCols);
+    return root_element;
+}
+
+function DOMTable_InnerHTMLLayout(root_element) {
+    // Create the HTML string for the table and set it at the root_element for the benchmark.
+    root_element.innerHTML = DOMTable_CreateTableIH(DOMTable_maxRows, DOMTable_maxCols);
+    // Force a layout by requesting the height of the table. The result is
+    // going to be ignored because there is not cleanup function registered.
+    return root_element.scrollHeight;
+}
+
+function DOMTableSum_Setup() {
+    // Create the table to be summed using DOM operations from JavaScript. By
+    // doing it this way the elements are all pre-wrapped.
+    DOMTable_element_count = 0;
+    var root_element = document.getElementById("benchmark_content");
+    var table = DOMTable_CreateTable(DOMTable_maxRows, DOMTable_maxCols*5);
+    root_element.appendChild(table);
+    return root_element;
+}
+
+function DOMTableSum_SetupIH() {
+    // Create the table to be summed using InnerHTML. By doing it this way the
+    // elements need to be wrapped on access.
+    DOMTable_element_count = 0;
+    var root_element = document.getElementById("benchmark_content");
+    var table = DOMTable_CreateTableIH(DOMTable_maxRows, DOMTable_maxCols*5);
+    root_element.innerHTML = table;
+    return root_element;
+}
+
+function DOMTableSum_ById(ignore) {
+    // Sum all elements in the table by finding each element by its id.
+    var sum = 0;
+    var maxRows = DOMTable_maxRows;
+    var maxCols = DOMTable_maxCols*5;
+    for (var r = 0; r < maxRows; r++) {
+        for (var c = 0; c < maxCols; c++) {
+            var cell = document.getElementById("$"+r+"$"+c);
+            sum += (+cell.textContent);
+        }
+    }
+    return sum;
+}
+
+function DOMTableSum_ByTagName(root_element) {
+    // Sum all elements in the table by getting a NodeList of all "td" elements.
+    var sum = 0;
+    var nodes = root_element.getElementsByTagName("td");
+    var length = nodes.length;
+    for (var i = 0; i < length; i++) {
+        var cell = nodes[i];
+        sum += (+cell.textContent);
+    }
+    return sum;
+}
+
+var DOMTableTest = new BenchmarkSuite('DOMTable', [
+    new Benchmark("create", DOMTable_Create, DOMTable_CreateSetup),
+    new Benchmark("create and layout", DOMTable_CreateLayout, DOMTable_CreateSetup),
+    new Benchmark("create with innerHTML", DOMTable_InnerHTML, DOMTable_CreateSetup),
+    new Benchmark("create and layout with innerHTML", DOMTable_InnerHTMLLayout, DOMTable_CreateSetup),
+    new Benchmark("sum elements by id", DOMTableSum_ById, DOMTableSum_Setup),
+    new Benchmark("sum elements by id with innerHTML", DOMTableSum_ById, DOMTableSum_SetupIH),
+    new Benchmark("sum elements by tagname", DOMTableSum_ByTagName, DOMTableSum_Setup),
+    new Benchmark("sum elements by tagname with innerHTML", DOMTableSum_ByTagName, DOMTableSum_SetupIH)
+]);
+
diff --git a/PerformanceTests/DOM/resources/dom-perf/domwalk.js b/PerformanceTests/DOM/resources/dom-perf/domwalk.js
new file mode 100644 (file)
index 0000000..4f23d43
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ * 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.
+ */
+
+// DOMWalk - a benchmark for walking the DOM
+//
+// This benchmark tests different mechanisms for touching every element
+// in a section of the DOM. The elements are created from JavaScript causing
+// them to be pre-wrapped.
+//
+// Tests small iterations and large iterations to see if there is a difference.
+
+
+// We use two mechanisms to walk the DOM, and then
+// verify that both yield the same result.
+var __num_nodes = 0;
+
+// We create a table to iterate as part of this test.
+// For now it's just a big table with lots of elements.
+function DOMWalk_CreateTable(height, width) {
+    var table = document.getElementById("fake_dom");
+    if (table) {
+        // clear out existing table
+        while (table.rows.length > 0)
+            table.deleteRow(0);
+    } else {
+        var doc = document;
+        table = doc.createElement("table");
+        table.id = "fake_dom";
+        table.border = 1;
+
+        for (var row = 0; row < height; row++) {
+            var row_object = doc.createElement("tr");
+            for (var column = 0; column < width; column++) {
+                var col_object = doc.createElement("td");
+                var text = document.createTextNode(row.toString() + "." + column.toString());
+                col_object.appendChild(text);
+                row_object.appendChild(col_object);
+            }
+            table.appendChild(row_object);
+        }
+        var content = document.getElementById("benchmark_content");
+        content.appendChild(table);
+        var html = content.innerHTML;
+        var width = content.clientWidth;
+    }
+    return table;
+}
+
+function DOMWalk_SetupSmall() {
+    // Creates a table with 100 nodes.
+    DOMWalk_CreateTable(10, 10);
+}
+
+function DOMWalk_SetupLarge() {
+    // Creates a table with 4000 nodes.
+    DOMWalk_CreateTable(200, 200);
+}
+
+// Walks the DOM via getElementsByTagName.
+function DOMWalk_ByTagName(table) {
+    var items = table.getElementsByTagName("*");
+    var item;
+    var length = items.length;
+    for (var index = 0; index < length; index++)
+        item = items[index];
+
+    // Return the number of nodes seen.
+    return items.length;
+}
+
+function DOMWalk_ByTagNameDriver(loops) {
+    var table = document.getElementById("benchmark_content");
+    for (var loop = 0; loop < loops; loop++)
+        __num_nodes = DOMWalk_ByTagName(table);
+}
+
+function DOMWalk_ByTagNameSmall() {
+    // This test runs in a short time.  We loop a few times in order to avoid small time measurements.
+    DOMWalk_ByTagNameDriver(1000);
+}
+
+function DOMWalk_ByTagNameLarge() {
+    DOMWalk_ByTagNameDriver(1);
+}
+
+function DOMWalk_Recursive(node) {
+    // Count Element Nodes only.
+    // IE does not define the Node constants.
+    var count = (node.nodeType == /* Node.ELEMENT_NODE */ 1) ? 1 : 0;
+
+    var child = node.firstChild;
+    while (child !== null) {
+        count += DOMWalk_Recursive(child);
+        child = child.nextSibling;
+    }
+
+    return count;
+}
+
+// Walks the DOM via a recursive walk
+function DOMWalk_RecursiveDriver(loops) {
+    var table = document.getElementById("benchmark_content");
+    for (var loop = 0; loop < loops; loop++) {
+        var count = DOMWalk_Recursive(table) - 1;  // don't count the root node.
+        if (count != __num_nodes || count === 0)
+            throw "DOMWalk failed!  Expected " + __num_nodes + " nodes but found " + count + ".";
+    }
+}
+
+function DOMWalk_RecursiveSmall() {
+    // This test runs in a short time.  We loop a few times in order to avoid small time measurements.
+    DOMWalk_RecursiveDriver(200);
+}
+
+function DOMWalk_RecursiveLarge() {
+    // Only iterate once over the large DOM structures.
+    DOMWalk_RecursiveDriver(1);
+}
+
+
+var DOMWalkTest = new BenchmarkSuite('DOMWalk', [
+    new Benchmark("DOMWalkByTag (100 nodes)", DOMWalk_ByTagNameSmall, DOMWalk_SetupSmall),
+    new Benchmark("DOMWalkRecursive (100 nodes)", DOMWalk_RecursiveSmall, DOMWalk_SetupSmall),
+    new Benchmark("DOMWalkByTag (4000 nodes)", DOMWalk_ByTagNameLarge, DOMWalk_SetupLarge),
+    new Benchmark("DOMWalkRecursive (4000 nodes)", DOMWalk_RecursiveLarge, DOMWalk_SetupLarge)
+]);
diff --git a/PerformanceTests/DOM/resources/dom-perf/events.js b/PerformanceTests/DOM/resources/dom-perf/events.js
new file mode 100644 (file)
index 0000000..0886489
--- /dev/null
@@ -0,0 +1,274 @@
+/*
+ * 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.
+ */
+
+// Events test - test the hooking and dispatch of events.
+//
+// This is a fairly simple test for measuring event peformance.
+// We create a DOM structure (a set of nested divs) to test with.
+//
+// The Hooking test measures the time to register onclick handlers for
+// each node in the structure.  This simulates conditions where applications
+// register event handlers on many nodes programatically.
+//
+// The Dispatch test measures the time to dispatch events to each node
+// in the structure.  In this case, we register the event handler as part
+// of the HTML for the structure, and then simply simulate onclick events
+// to each node.
+//
+// Works in IE, FF, Safari, and Chrome.
+
+var Events_counter = 0;
+function EventClickHandler() {
+    Events_counter++;
+}
+
+function EventsTest(rows, cols) {
+    var me = this;
+    this.rows = rows;
+    this.cols = cols;
+    this.cell_count = 0; // Track the number of cells created in our dom tree.
+    this.proxies = [];
+    this.random_ids = [];
+
+    // Create a DOM structure and optionally register event handlers on each node.
+    // Create the structure by setting innerHTML so that the DOM nodes are not
+    // pre-wrapped for JS access.
+    this.CreateTable = function(add_event_listeners) {
+        var html_string = '<div>';
+        for (var i = 0; i < me.rows; i++)
+            html_string += me.CreateRow(i, me.cols, add_event_listeners);
+        return html_string + '</div>';
+    };
+
+    // Returns an html string for a div with a row/column based id, with an optional onclick handler.
+    this.CreateCell = function(row_id, col_id, add_event_listeners) {
+        var str =  '<div id="r' + row_id + 'c' + col_id + '"';
+        if (add_event_listeners)
+            str += ' onclick="EventClickHandler();"';
+        str += '>'+ me.cell_count++ + '</div>';
+        return str;
+    };
+
+    // Returns an html string with an outer div containing |cols| inner divs,
+    // optionally having an onclick handler.
+    this.CreateRow = function(row_id, cols, add_event_listeners) {
+        var html_string = '<div id="r' + row_id + '">';
+        for (var i = 0; i < cols; i++)
+            html_string += me.CreateCell(row_id, i, add_event_listeners);
+        return html_string + '</div>';
+    };
+
+    // Prepares for testing with elements that have no pre-defined onclick
+    // handlers.
+    this.Setup = function() {
+        me.cell_count = 0;
+        Events_counter = 0;
+        var root_element = document.getElementById("benchmark_content");
+        root_element.innerHTML = me.CreateTable(false);
+        return root_element;
+    };
+
+    // Similar to Setup, but with onclick handlers already defined in the html.
+    this.SetupWithListeners = function() {
+        me.cell_count = 0;
+        Events_counter = 0;
+        var root_element = document.getElementById("benchmark_content");
+        root_element.innerHTML = me.CreateTable(true);
+        return root_element;
+    };
+
+    // Sets up for testing performance of removing event handlers.
+    this.SetupForTeardown = function() {
+        me.random_ids = [];
+        me.SetupWithListeners();
+        var tmp = [];
+        for (var row = 0; row < me.rows; row++) {
+            for (var col = 0; col < me.cols; col++)
+                tmp.push("r" + row + "c" + col);
+        }
+        while (tmp.length > 0) {
+            var index = Math.floor(Math.random() * tmp.length);
+            me.random_ids.push(tmp.splice(index, 1));
+        }
+    };
+
+    // Tests the time it takes to go through and hook all elements in our dom.
+    this.HookTest = function() {
+        var node_count = 0;
+
+        var row_id = 0;
+        while(true) {
+            var row = document.getElementById('r' + row_id);
+            if (row == undefined)
+                break;
+
+            var col_id = 0;
+            while(true) {
+                var col = document.getElementById('r' + row_id + 'c' + col_id);
+                if (col == undefined)
+                    break;
+
+                if (col.addEventListener)
+                    col.addEventListener("click", EventClickHandler, false);
+                else if (col.attachEvent)
+                    col.attachEvent("onclick", EventClickHandler); // To support IE
+                else
+                    throw "FAILED TO ATTACH EVENTS";
+                col_id++;
+                node_count++;
+            }
+
+            row_id++;
+        }
+
+        if (node_count != me.rows * me.cols)
+            throw "ERROR - did not iterate all nodes";
+    };
+
+    // Tests the time it takes to go through and hook all elements in our dom.
+    // Creates new proxy object for each registration
+    this.HookTestProxy = function() {
+        var node_count = 0;
+
+        var row_id = 0;
+        while(true) {
+            var row = document.getElementById('r' + row_id);
+            if (row == undefined)
+                break;
+
+            var col_id = 0;
+            while(true) {
+                var col = document.getElementById('r' + row_id + 'c' + col_id);
+                if (col == undefined)
+                    break;
+
+                var proxy = function() {};
+                proxy.col = col;
+                me.proxies.push(proxy);
+                if (col.addEventListener)
+                    col.addEventListener("click", proxy, false);
+                else if (col.attachEvent)
+                    col.attachEvent("onclick", proxy); // To support IE
+                else
+                    throw "FAILED TO ATTACH EVENTS";
+                col_id++;
+                node_count++;
+            }
+
+            row_id++;
+        }
+
+        if (node_count != me.rows * me.cols)
+            throw "ERROR - did not iterate all nodes";
+    };
+
+    // Tests firing the events for each element in our dom.
+    this.DispatchTest = function() {
+        var node_count = 0;
+
+        var row_id = 0;
+        while(true) {
+            var row = document.getElementById('r' + row_id);
+            if (row == undefined)
+                break;
+
+            var col_id = 0;
+            while(true) {
+                var col = document.getElementById('r' + row_id + 'c' + col_id);
+                if (col == undefined)
+                  break;
+
+                if (document.createEvent) {
+                    var event = document.createEvent("MouseEvents");
+                    event.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+                    col.dispatchEvent(event);
+                } else if (col.fireEvent) {
+                    var event = document.createEventObject();
+                    col.fireEvent("onclick", event);
+                } else
+                    throw "FAILED TO FIRE EVENTS";
+
+                col_id++;
+                node_count++;
+            }
+
+            row_id++;
+        }
+
+        if (Events_counter != me.rows * me.cols)
+            throw "ERROR - did not fire events on all nodes!" + Events_counter;
+    };
+
+    // Tests removing event handlers.
+    this.TeardownTest = function() {
+        var node_count = 0;
+        for (var i = 0; i < me.random_ids.length; i++) {
+            var col = document.getElementById(me.random_ids[i]);
+            if (col.removeEventListener)
+                col.removeEventListener("click", EventClickHandler, false);
+            else if (col.detachEvent)
+                col.detachEvent("onclick", EventClickHandler);
+            else
+                throw "FAILED TO FIRE EVENTS";
+            node_count++;
+        }
+
+        if (node_count != me.rows * me.cols)
+            throw "ERROR - did not remove listeners from all nodes! " + node_count;
+    };
+
+    // Removes event handlers and their associated proxy objects.
+    this.ProxyCleanup = function() {
+        for (var i = 0, n = me.proxies.length; i < n; i++) {
+            var proxy = me.proxies[i];
+            var col = proxy.col;
+            if (col.removeEventListener)
+                col.removeEventListener("click", proxy, false);
+            else if (col.detachEvent)
+                col.detachEvent("onclick", proxy); // To support IE
+        }
+        me.proxies = [];
+    };
+}
+
+var small_test = new EventsTest(100, 10);
+var large_test = new EventsTest(100, 50);
+var extra_large_test = new EventsTest(200, 20);
+
+var EventTest = new BenchmarkSuite('Events', [
+    new Benchmark("Event Hooking (1000 nodes)", small_test.HookTest, small_test.Setup),
+    new Benchmark("Event Dispatch (1000 nodes)", small_test.DispatchTest, small_test.SetupWithListeners),
+    new Benchmark("Event Hooking (5000 nodes)", large_test.HookTest, large_test.Setup),
+    new Benchmark("Event Hooking Proxy (4000 nodes)",
+        extra_large_test.HookTestProxy, extra_large_test.Setup, extra_large_test.ProxyCleanup),
+    new Benchmark("Event Dispatch (5000 nodes)", large_test.DispatchTest, large_test.SetupWithListeners),
+    new Benchmark("Event Teardown (5000 nodes)", large_test.TeardownTest, large_test.SetupForTeardown),
+    new Benchmark("Event Teardown (4000 nodes)", extra_large_test.TeardownTest, extra_large_test.SetupForTeardown)
+]);
diff --git a/PerformanceTests/DOM/resources/dom-perf/getelement.js b/PerformanceTests/DOM/resources/dom-perf/getelement.js
new file mode 100644 (file)
index 0000000..3a12cd8
--- /dev/null
@@ -0,0 +1,207 @@
+/*
+ * 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.
+ */
+
+// Tests looking for ids in different DOM trees full of div elements.
+function GetElementTest(size, appendStyle, treeStyle) {
+    var me = this;
+    this.size = size;
+    this.appendStyle = appendStyle;
+    this.ids = null;
+    this.treeStyle = treeStyle;
+    this.name = (this.Sizes.length > 1 ? (me.size + ", ") : "") + me.appendStyle + ", " + me.treeStyle;
+
+    this.setupDOM = function() {
+        var domTree = me.getDOMTree().cloneNode(true);
+        this.suite.benchmarkContent.appendChild(domTree);
+        me.nodesFound = 0;
+        me.ids = me.getIds();
+        return domTree;
+    };
+
+    this.setupHTML = function() {
+        this.suite.benchmarkContent.innerHTML = me.getDOMTree().innerHTML;
+        me.nodesFound = 0;
+        me.ids = me.getIds();
+    };
+
+    if (this.appendStyle == "DOM")
+        this.Setup = this.setupDOM;
+    else
+        this.Setup = this.setupHTML;
+
+    this.Test = function(handle) {
+        var kIterations = 1;
+        for (var iterations = 0; iterations < kIterations; iterations++) {
+            me.nodesFound = 0;
+            for (var i = 0, len = me.ids.length; i < len; i++) {
+                var div = document.getElementById(me.ids[i]);
+                var nodeName = div.nodeName;
+                me.nodesFound++;
+            }
+        }
+    };
+
+    this.Cleanup = function(handle) {
+        var expectedCount = me.ids.length;
+        if (me.nodesFound != expectedCount)
+            throw "Wrong number of nodes found: " + me.nodesFound + " expected: " + expectedCount;
+    };
+
+    this.GetBenchmark = function() {
+        return new Benchmark(this.name, this.Test, this.Setup, this.Cleanup);
+    };
+
+    this.getIdsFromTree = function(parent, maxNumberOfNodes) {
+        var allDivs = parent.getElementsByTagName("div");
+        var len = allDivs.length;
+        var skip;
+        if (maxNumberOfNodes >= allDivs.length)
+            skip = 0;
+        else
+            skip = Math.floor(allDivs.length / maxNumberOfNodes) - 1;
+        var ids = [];
+        var l = 0;
+        for (var i = 0, len = allDivs.length; i < len && l < maxNumberOfNodes; i += (skip + 1)) {
+            var div = allDivs[i];
+            ids.push(div.id);
+            l++;
+        }
+        return ids;
+    };
+
+    this.createTreeAndIds = function() {
+        var maxNumberOfNodes = 20000;
+        var domTree;
+
+        if (me.treeStyle == "dups") {
+            // We use four of the trees for the dups style,
+            // so they get too big if you use the full size for the bigger trees
+            switch (me.size) {
+            case "small":
+                domTree = BenchmarkSuite.prototype.generateSmallTree();
+                break;
+            case "medium":
+                domTree = BenchmarkSuite.prototype.generateDOMTree(15, 12, 4);
+                break;
+            case "large":
+                domTree = BenchmarkSuite.prototype.generateDOMTree(26, 26, 1);
+                break;
+            }
+        } else {
+            switch (me.size) {
+            case "small":
+                domTree = BenchmarkSuite.prototype.generateSmallTree();
+                break;
+            case "medium":
+                domTree = BenchmarkSuite.prototype.generateMediumTree();
+                break;
+            case "large":
+                domTree = BenchmarkSuite.prototype.generateLargeTree();
+                break;
+            }
+        }
+
+        var allDivs = domTree.getElementsByTagName("*");
+        var len = allDivs.length;
+        var modBy;
+        if (maxNumberOfNodes >= allDivs.length)
+            modBy = 1;
+        else
+            modBy = Math.floor(allDivs.length / maxNumberOfNodes);
+        var ids = [];
+        for (var i = 0, len = allDivs.length; i < len; i++) {
+            var mod = i % modBy;
+            var div = allDivs[i];
+            if (mod == 0 && ids.length < maxNumberOfNodes) {
+                if (div.id && div.id != "")
+                    ids.push(div.id);
+            } else if (me.treeStyle == "sparse")
+                div.id = null;
+        }
+
+        if (me.treeStyle == "dups") {
+            var newRoot = document.createElement("div");
+            for (var i = 0; i < 5; i++)
+                newRoot.appendChild(domTree.cloneNode(true));
+            domTree = newRoot;
+        }
+
+        var treeAndIds = {
+            tree: domTree,
+            ids: ids
+        };
+        return treeAndIds;
+    };
+
+    this.getTreeAndIds = function() {
+        var treeAndIdsMap = me.TreeAndIds[me.size];
+        if (!treeAndIdsMap) {
+            treeAndIdsMap = {};
+            me.TreeAndIds[me.size] = treeAndIdsMap;
+        }
+        var treeAndIds = treeAndIdsMap[me.treeStyle];
+        if (!treeAndIds) {
+            treeAndIds = me.createTreeAndIds();
+            treeAndIdsMap[me.treeStyle] = treeAndIds;
+        }
+        return treeAndIds;
+    };
+
+    this.getDOMTree = function() {
+        var treeAndIds = me.getTreeAndIds();
+        return treeAndIds.tree;
+    };
+
+    this.getIds = function() {
+        var treeAndIds = me.getTreeAndIds();
+        return treeAndIds.ids;
+    };
+}
+
+GetElementTest.prototype = {
+    // Different sizes are possible, but we try to keep the runtime and memory
+    // consumption reasonable.
+    Sizes: ["medium"],
+    TreeStyles: ["sparse", "dense", "dups"],
+    TreeAndIds: {},
+    AppendStyles: ["DOM", "HTML"]
+};
+
+// Generate the matrix of all benchmarks
+var benchmarks = [];
+GetElementTest.prototype.Sizes.forEach(function(size) {
+    GetElementTest.prototype.AppendStyles.forEach(function(appendStyle) {
+        GetElementTest.prototype.TreeStyles.forEach(function(treeStyle) {
+            benchmarks.push(new GetElementTest(size, appendStyle, treeStyle).GetBenchmark());
+        });
+    });
+});
+
+var GetElementTest = new BenchmarkSuite("Get Elements", benchmarks);
diff --git a/PerformanceTests/DOM/resources/dom-perf/gridsort.js b/PerformanceTests/DOM/resources/dom-perf/gridsort.js
new file mode 100644 (file)
index 0000000..b7010de
--- /dev/null
@@ -0,0 +1,166 @@
+/*
+ * 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.
+ */
+
+// GridSort
+//
+// This test is designed to test the performance of sorting a grid of nodes
+// such as what you might use in a spreadsheet application.
+
+// returns an array of integers from 0 to totalCells
+function generateValuesArray(totalCells) {
+    var values = [];
+    for (var i = 0; i < totalCells; i++)
+        values[i] = i;
+    return values;
+}
+
+// creates value text nodes in a table using DOM methods
+function populateTableUsingDom(tableId, width, height) {
+    var table = document.getElementById(tableId);
+    var values = generateValuesArray(width * height);
+
+    for (var i = 0; i < height; i++) {
+        var row = table.insertRow(i);
+        for (var j = 0; j < width; j++) {
+            var cell = row.insertCell(j);
+            var valueIndex = Math.floor(Math.random() * values.length);
+            var value = values.splice(valueIndex, 1);
+            cell.appendChild(document.createTextNode(value));
+        }
+    }
+}
+
+// returns HTML string for rows/columns of table data
+function getTableContentHTML(width, height) {
+    var values = generateValuesArray(width * height);
+
+    var html = []; // fragments we will join together at the end
+    var htmlIndex  = 0;
+
+    for (var i = 0; i < height; i++) {
+        html.push("<tr>");
+        for (var j = 0; j < width; j++) {
+            html.push("<td>");
+            var valueIndex = Math.floor(Math.random() * values.length);
+            var value = values.splice(valueIndex, 1);
+            html.push(value);
+            html.push("</td>");
+        }
+        html.push("</tr>");
+    }
+    return html.join("");
+}
+
+// When sorting a table by a column, we create one of these for each
+// cell in the column, and it keeps pointers to all the nodes in that
+// row. This way we can sort an array of SortHelper objects, and then
+// use the sibling node pointers to move all values in a row to their
+// proper place according to the new sort order.
+function SortHelper(row, index) {
+    this.nodes = [];
+    var numCells = row.cells.length;
+    for (var i = 0; i < numCells; i++)
+        this.nodes[i] = row.cells[i].firstChild;
+    this.originalIndex = index;
+}
+
+function compare(a, b) {
+    return a - b;
+}
+
+// sorts all rows of the table on a given column
+function sortTableOnColumn(table, columnIndex) {
+    var numRows = table.rows.length;
+    var sortHelpers = [];
+    for (var i = 0; i < numRows; i++)
+        sortHelpers.push(new SortHelper(table.rows[i], i));
+
+    // sort by nodeValue with original position breaking ties
+    sortHelpers.sort(function(a, b) {
+        var cmp = compare(Number(a.nodes[columnIndex].nodeValue), Number(b.nodes[columnIndex].nodeValue));
+        if (cmp === 0)
+            return compare(a.originalIndex, b.originalIndex);
+        return cmp;
+    });
+
+    // now place all cells in their new position
+    var numSortHelpers = sortHelpers.length;
+    for (var i = 0; i < numSortHelpers; i++) {
+        var helper = sortHelpers[i];
+        if (i == helper.originalIndex)
+            continue; // no need to move this row
+        var columnCount = table.rows[i].cells.length;
+        for (var j = 0; j < columnCount; j++) {
+            var cell = table.rows[i].cells[j];
+            if (cell.firstChild) {
+                // a SortHelper will still have a reference to this node, so it
+                // won't get orphaned/garbage collected
+                cell.removeChild(cell.firstChild);
+            }
+            cell.appendChild(helper.nodes[j]);
+        }
+    }
+}
+
+function clearExistingTable() {
+    var table = document.getElementById("gridsort_table");
+    if (table) {
+        // clear out existing table
+        table.parentNode.removeChild(table);
+    }
+}
+
+function createTableUsingDom() {
+    clearExistingTable();
+    var table = document.createElement("table");
+    table.id = "gridsort_table";
+    table.border = 1;
+    document.getElementById("benchmark_content").appendChild(table);
+    populateTableUsingDom("gridsort_table", 60, 60);
+}
+
+function createTableUsingInnerHtml() {
+    clearExistingTable();
+    var tableContent = getTableContentHTML(60, 60);
+    var html = "<table id='gridsort_table' border='1'>" + tableContent + "</table>";
+    document.getElementById("benchmark_content").innerHTML = html;
+}
+
+function sortTable() {
+    var table = document.getElementById("gridsort_table");
+    // TODO - it might be interesting to sort several (or all)
+    // columns in succession, but for now that's fairly slow
+    sortTableOnColumn(table, 0);
+}
+
+var GridSortTest = new BenchmarkSuite('GridSort', [
+    new Benchmark("SortDomTable (60x60)", sortTable, createTableUsingDom, null, false),
+    new Benchmark("SortInnerHtmlTable (60x60)", sortTable, createTableUsingInnerHtml, null, false)
+]);
diff --git a/PerformanceTests/DOM/resources/dom-perf/suites.js b/PerformanceTests/DOM/resources/dom-perf/suites.js
new file mode 100644 (file)
index 0000000..eaba53c
--- /dev/null
@@ -0,0 +1,12 @@
+var suiteFiles = [
+    "dom/accessors.js", 
+    "dom/createnodes.js",
+    "dom/domtable.js", 
+    "dom/domwalk.js", 
+    "dom/domdivwalk.js", 
+    "dom/clonenodes.js", 
+    "dom/getelement.js", 
+    "dom/events.js", 
+    "dom/gridsort.js", 
+    "dom/template.js"
+];
diff --git a/PerformanceTests/DOM/resources/dom-perf/template.js b/PerformanceTests/DOM/resources/dom-perf/template.js
new file mode 100644 (file)
index 0000000..db9930a
--- /dev/null
@@ -0,0 +1,1180 @@
+/*
+ * 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.
+ */
+
+// Template Test
+// This test uses a simple JS templating system for injecting JSON data into
+// an HTML page.  It is designed to use the kind of DOM manipulation common
+// in templating systems.
+//
+// Code from http://code.google.com/p/google-jstemplate/source/browse/trunk/jstemplate.js
+
+// This is the HTML code to use in the template test
+var content ="\
+<style type=\"text/css\"> \
+body {\
+  border-top: 10px solid #3B85E3;\
+  color: #333;\
+  font-family: Verdana,Arial,Helvetica,sans-serif;\
+}\
+body, td {\
+  font-size: 11px;\
+}\
+a:link, a:visited {\
+  color: #2C3EBA;\
+  text-decoration: none;\
+}\
+a:hover {\
+  color: red;\
+  text-decoration: underline;\
+}\
+h1 {\
+  border-left: 10px solid #FFF;\
+  font-size: 16px;\
+  font-weight: bold;\
+  margin: 0;\
+  padding: 0.2em;\
+  color: #3B85E3;\
+}\
+h2 {\
+  border-left: 10px solid #FFF;\
+  font-size: 11px;\
+  font-weight: normal;\
+  margin: 0;\
+  padding: 0 6em 0.2em 0.2em;\
+}\
+.details {\
+  margin: 0.4em 1.9em 0 1.2em;\
+  padding: 0 0.4em 0.3em 0;\
+  white-space: nowrap;\
+}\
+.details .outer {\
+  padding-right: 0;\
+  vertical-align: top;\
+}\
+.details .top {\
+  border-top: 2px solid #333;\
+  font-weight: bold;\
+  margin-top: 0.4em;\
+}\
+.details .header2 {\
+  font-weight: bold;\
+  padding-left: 0.9em;\
+}\
+.details .key {\
+  padding-left: 1.1em;\
+  vertical-align: top;\
+}\
+.details .value {\
+  text-align: right;\
+  color: #333;\
+  font-weight: bold;\
+}\
+.details .zebra {\
+  background: #EEE;\
+}\
+.lower {\
+  text-transform: lowercase;\
+}\
+</style> \
+  <h1 class=\"lower\">About Stats</h1> \
+  <table class=\"details\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\"> \
+    <tbody> \
+      <tr> \
+        <td class=\"outer\"> \
+          <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\"> \
+            <tbody> \
+              <tr> \
+                <td class=\"top\" width=\"100\">Counters</td> \
+                <td class=\"top value\" colspan=2></td> \
+              </tr> \
+              <tr> \
+                <td class=\"header2 lower\" width=\"200\">name</td> \
+                <td class=\"header2 lower\">value</td> \
+                <td class=\"header2 lower\">delta</td> \
+              </tr> \
+              <tr jsselect=\"counters\" name=\"counter\"> \
+                <td class=\"key\" width=\"200\" jscontent=\"name\"></td> \
+                <td class=\"value\" jscontent=\"value\"></td> \
+                <td class=\"value\" jscontent=\"delta\"></td> \
+              </tr> \
+            </tbody> \
+          </table> \
+        </td> \
+        <td width=\"15\"/> \
+        <td class=\"outer\"> \
+          <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\"> \
+            <tbody> \
+              <tr> \
+                <td class=\"top\" width=\"100\">Timers</td> \
+                <td class=\"top value\"></td> \
+                <td class=\"top value\" colspan=3></td> \
+              </tr> \
+              <tr> \
+                <td class=\"header2 lower\" width=\"200\">name</td> \
+                <td class=\"header2 lower\">count</td> \
+                <td class=\"header2 lower\">time (ms)</td> \
+                <td class=\"header2 lower\">avg time (ms)</td> \
+              </tr> \
+              <tr jsselect=\"timers\" name=\"timer\"> \
+                <td class=\"key\" width=\"200\" jscontent=\"name\"></td> \
+                <td class=\"value\" jscontent=\"value\"></td> \
+                <td class=\"value\" jscontent=\"time\"></td> \
+                <td class=\"value\"></td> \
+              </tr> \
+            </tbody> \
+          </table> \
+        </td> \
+      </tr> \
+    </tbody> \
+  </table><br/> \
+</body> \
+</html> \
+";
+
+// Generic Template Library
+
+/**
+ * @fileoverview This file contains miscellaneous basic functionality.
+ *
+ */
+
+/**
+ * Returns the document owner of the given element. In particular,
+ * returns window.document if node is null or the browser does not
+ * support ownerDocument.
+ *
+ * @param {Node} node  The node whose ownerDocument is required.
+ * @returns {Document|Null}  The owner document or null if unsupported.
+ */
+function Template_ownerDocument(node) {
+    return (node ? node.ownerDocument : null) || document;
+}
+
+/**
+ * Wrapper function to create CSS units (pixels) string
+ *
+ * @param {Number} numPixels  Number of pixels, may be floating point.
+ * @returns {String}  Corresponding CSS units string.
+ */
+function Template_px(numPixels) {
+    return round(numPixels) + "px";
+}
+
+/**
+ * Sets display to none. Doing this as a function saves a few bytes for
+ * the 'style.display' property and the 'none' literal.
+ *
+ * @param {Element} node  The dom element to manipulate.
+ */
+function Template_displayNone(node) {
+    node.style.display = 'none';
+}
+
+/**
+ * Sets display to default.
+ *
+ * @param {Element} node  The dom element to manipulate.
+ */
+function Template_displayDefault(node) {
+    node.style.display = '';
+}
+
+var DOM_ELEMENT_NODE = 1;
+var DOM_ATTRIBUTE_NODE = 2;
+var DOM_TEXT_NODE = 3;
+var DOM_CDATA_SECTION_NODE = 4;
+var DOM_ENTITY_REFERENCE_NODE = 5;
+var DOM_ENTITY_NODE = 6;
+var DOM_PROCESSING_INSTRUCTION_NODE = 7;
+var DOM_COMMENT_NODE = 8;
+var DOM_DOCUMENT_NODE = 9;
+var DOM_DOCUMENT_TYPE_NODE = 10;
+var DOM_DOCUMENT_FRAGMENT_NODE = 11;
+var DOM_NOTATION_NODE = 12;
+
+/**
+ * Get an attribute from the DOM.  Simple redirect, exists to compress code.
+ *
+ * @param {Element} node  Element to interrogate.
+ * @param {String} name  Name of parameter to extract.
+ * @return {String}  Resulting attribute.
+ */
+function Template_domGetAttribute(node, name) {
+    return node.getAttribute(name);
+}
+
+/**
+ * Set an attribute in the DOM.  Simple redirect to compress code.
+ *
+ * @param {Element} node  Element to interrogate.
+ * @param {String} name  Name of parameter to set.
+ * @param {String} value  Set attribute to this value.
+ */
+function Template_domSetAttribute(node, name, value) {
+    node.setAttribute(name, value);
+}
+
+/**
+ * Remove an attribute from the DOM.  Simple redirect to compress code.
+ *
+ * @param {Element} node  Element to interrogate.
+ * @param {String} name  Name of parameter to remove.
+ */
+function Template_domRemoveAttribute(node, name) {
+    node.removeAttribute(name);
+}
+
+/**
+ * Clone a node in the DOM.
+ *
+ * @param {Node} node  Node to clone.
+ * @return {Node}  Cloned node.
+ */
+function Template_domCloneNode(node) {
+    return node.cloneNode(true);
+}
+
+/**
+ * Return a safe string for the className of a node.
+ * If className is not a string, returns "".
+ *
+ * @param {Element} node  DOM element to query.
+ * @return {String}
+ */
+function Template_domClassName(node) {
+    return node.className ? "" + node.className : "";
+}
+
+/**
+ * Inserts a new child before a given sibling.
+ *
+ * @param {Node} newChild  Node to insert.
+ * @param {Node} oldChild  Sibling node.
+ * @return {Node}  Reference to new child.
+ */
+function Template_domInsertBefore(newChild, oldChild) {
+    return oldChild.parentNode.insertBefore(newChild, oldChild);
+}
+
+/**
+ * Appends a new child to the specified (parent) node.
+ *
+ * @param {Element} node  Parent element.
+ * @param {Node} child  Child node to append.
+ * @return {Node}  Newly appended node.
+ */
+function Template_domAppendChild(node, child) {
+    return node.appendChild(child);
+}
+
+/**
+ * Remove a new child from the specified (parent) node.
+ *
+ * @param {Element} node  Parent element.
+ * @param {Node} child  Child node to remove.
+ * @return {Node}  Removed node.
+ */
+function Template_domRemoveChild(node, child) {
+    return node.removeChild(child);
+}
+
+/**
+ * Replaces an old child node with a new child node.
+ *
+ * @param {Node} newChild  New child to append.
+ * @param {Node} oldChild  Old child to remove.
+ * @return {Node}  Replaced node.
+ */
+function Template_domReplaceChild(newChild, oldChild) {
+    return oldChild.parentNode.replaceChild(newChild, oldChild);
+}
+
+/**
+ * Removes a node from the DOM.
+ *
+ * @param {Node} node  The node to remove.
+ * @return {Node}  The removed node.
+ */
+function Template_domRemoveNode(node) {
+    return Template_domRemoveChild(node.parentNode, node);
+}
+
+/**
+ * Creates a new text node in the given document.
+ *
+ * @param {Document} doc  Target document.
+ * @param {String} text  Text composing new text node.
+ * @return {Text}  Newly constructed text node.
+ */
+function Template_domCreateTextNode(doc, text) {
+    return doc.createTextNode(text);
+}
+
+/**
+ * Redirect to document.getElementById
+ *
+ * @param {Document} doc  Target document.
+ * @param {String} id  Id of requested node.
+ * @return {Element|Null}  Resulting element.
+ */
+function Template_domGetElementById(doc, id) {
+    return doc.getElementById(id);
+}
+
+/**
+ * @fileoverview This file contains javascript utility functions that
+ * do not depend on anything defined elsewhere.
+ *
+ */
+
+/**
+ * Returns the value of the length property of the given object. Used
+ * to reduce compiled code size.
+ *
+ * @param {Array | String} a  The string or array to interrogate.
+ * @return {Number}  The value of the length property.
+ */
+function Template_jsLength(a) {
+    return a.length;
+}
+
+var min = Math.min;
+var max = Math.max;
+var ceil = Math.ceil;
+var floor = Math.floor;
+var round = Math.round;
+var abs = Math.abs;
+
+/**
+ * Copies all properties from second object to the first.  Modifies to.
+ *
+ * @param {Object} to  The target object.
+ * @param {Object} from  The source object.
+ */
+function Template_copyProperties(to, from) {
+    foreachin(from, function(p) { to[p] = from[p]; });
+}
+
+/**
+ * Iterates over the array, calling the given function for each
+ * element.
+ *
+ * @param {Array} array
+ * @param {Function} fn
+ */
+function foreach(array, fn) {
+    var I = Template_jsLength(array);
+    for (var i = 0; i < I; ++i)
+        fn(array[i], i);
+}
+
+/**
+ * Safely iterates over all properties of the given object, calling
+ * the given function for each property. If opt_all isn't true, uses
+ * hasOwnProperty() to assure the property is on the object, not on
+ * its prototype.
+ *
+ * @param {Object} object
+ * @param {Function} fn
+ * @param {Boolean} opt_all  If true, also iterates over inherited properties.
+ */
+function foreachin(object, fn, opt_all) {
+    for (var i in object) {
+        if (opt_all || !object.hasOwnProperty || object.hasOwnProperty(i))
+            fn(i, object[i]);
+    }
+}
+
+/**
+ * Trim whitespace from begin and end of string.
+ *
+ * @see testStringTrim();
+ *
+ * @param {String} str  Input string.
+ * @return {String}  Trimmed string.
+ */
+function Template_stringTrim(str) {
+    return Template_stringTrimRight(stringTrimLeft(str));
+}
+
+/**
+ * Trim whitespace from beginning of string.
+ *
+ * @see testStringTrimLeft();
+ *
+ * @param {String} str  Input string.
+ * @return {String}  Trimmed string.
+ */
+function Template_stringTrimLeft(str) {
+    return str.replace(/^\s+/, "");
+}
+
+/**
+ * Trim whitespace from end of string.
+ *
+ * @see testStringTrimRight();
+ *
+ * @param {String} str  Input string.
+ * @return {String}  Trimmed string.
+ */
+function Template_stringTrimRight(str) {
+    return str.replace(/\s+$/, "");
+}
+
+/**
+ * Jscompiler wrapper for parseInt() with base 10.
+ *
+ * @param {String} s String repersentation of a number.
+ *
+ * @return {Number} The integer contained in s, converted on base 10.
+ */
+function Template_parseInt10(s) {
+    return parseInt(s, 10);
+}
+/**
+ * @fileoverview A simple formatter to project JavaScript data into
+ * HTML templates. The template is edited in place. I.e. in order to
+ * instantiate a template, clone it from the DOM first, and then
+ * process the cloned template. This allows for updating of templates:
+ * If the templates is processed again, changed values are merely
+ * updated.
+ *
+ * NOTE: IE DOM doesn't have importNode().
+ *
+ * NOTE: The property name "length" must not be used in input
+ * data, see comment in jstSelect_().
+ */
+
+/**
+ * Names of jstemplate attributes. These attributes are attached to
+ * normal HTML elements and bind expression context data to the HTML
+ * fragment that is used as template.
+ */
+var ATT_select = 'jsselect';
+var ATT_instance = 'jsinstance';
+var ATT_display = 'jsdisplay';
+var ATT_values = 'jsvalues';
+var ATT_eval = 'jseval';
+var ATT_transclude = 'transclude';
+var ATT_content = 'jscontent';
+
+/**
+ * Names of special variables defined by the jstemplate evaluation
+ * context. These can be used in js expression in jstemplate
+ * attributes.
+ */
+var VAR_index = '$index';
+var VAR_this = '$this';
+
+/**
+ * Context for processing a jstemplate. The context contains a context
+ * object, whose properties can be referred to in jstemplate
+ * expressions, and it holds the locally defined variables.
+ *
+ * @param {Object} opt_data The context object. Null if no context.
+ *
+ * @param {Object} opt_parent The parent context, from which local
+ * variables are inherited. Normally the context object of the parent
+ * context is the object whose property the parent object is. Null for the
+ * context of the root object.
+ *
+ * @constructor
+ */
+function JsExprContext(opt_data, opt_parent) {
+    var me = this;
+
+    /**
+     * The local context of the input data in which the jstemplate
+     * expressions are evaluated. Notice that this is usually an Object,
+     * but it can also be a scalar value (and then still the expression
+     * $this can be used to refer to it). Notice this can be a scalar
+     * value, including undefined.
+     *
+     * @type {Object}
+     */
+    me.data_ = opt_data;
+
+    /**
+     * The context for variable definitions in which the jstemplate
+     * expressions are evaluated. Other than for the local context,
+     * which replaces the parent context, variable definitions of the
+     * parent are inherited. The special variable $this points to data_.
+     *
+     * @type {Object}
+     */
+    me.vars_ = {};
+    if (opt_parent)
+        Template_copyProperties(me.vars_, opt_parent.vars_);
+    this.vars_[VAR_this] = me.data_;
+}
+
+/**
+ * Evaluates the given expression in the context of the current
+ * context object and the current local variables.
+ *
+ * @param {String} expr A javascript expression.
+ *
+ * @param {Element} template DOM node of the template.
+ *
+ * @return The value of that expression.
+ */
+JsExprContext.prototype.jseval = function(expr, template) {
+    with (this.vars_) {
+        with (this.data_) {
+            try {
+                return (function() { return eval('[' + expr + '][0]'); }).call(template);
+            } catch (e) {
+                return null;
+            }
+        }
+    }
+};
+
+/**
+ * Clones the current context for a new context object. The cloned
+ * context has the data object as its context object and the current
+ * context as its parent context. It also sets the $index variable to
+ * the given value. This value usually is the position of the data
+ * object in a list for which a template is instantiated multiply.
+ *
+ * @param {Object} data The new context object.
+ *
+ * @param {Number} index Position of the new context when multiply
+ * instantiated. (See implementation of jstSelect().)
+ *
+ * @return {JsExprContext}
+ */
+JsExprContext.prototype.clone = function(data, index) {
+    var ret = new JsExprContext(data, this);
+    ret.setVariable(VAR_index, index);
+    if (this.resolver_)
+        ret.setSubTemplateResolver(this.resolver_);
+    return ret;
+};
+
+/**
+ * Binds a local variable to the given value. If set from jstemplate
+ * jsvalue expressions, variable names must start with $, but in the
+ * API they only have to be valid javascript identifier.
+ *
+ * @param {String} name
+ *
+ * @param {Object} value
+ */
+JsExprContext.prototype.setVariable = function(name, value) {
+    this.vars_[name] = value;
+};
+
+/**
+ * Sets the function used to resolve the values of the transclude
+ * attribute into DOM nodes. By default, this is jstGetTemplate(). The
+ * value set here is inherited by clones of this context.
+ *
+ * @param {Function} resolver The function used to resolve transclude
+ * ids into a DOM node of a subtemplate. The DOM node returned by this
+ * function will be inserted into the template instance being
+ * processed. Thus, the resolver function must instantiate the
+ * subtemplate as necessary.
+ */
+JsExprContext.prototype.setSubTemplateResolver = function(resolver) {
+    this.resolver_ = resolver;
+};
+
+/**
+ * Resolves a sub template from an id. Used to process the transclude
+ * attribute. If a resolver function was set using
+ * setSubTemplateResolver(), it will be used, otherwise
+ * jstGetTemplate().
+ *
+ * @param {String} id The id of the sub template.
+ *
+ * @return {Node} The root DOM node of the sub template, for direct
+ * insertion into the currently processed template instance.
+ */
+JsExprContext.prototype.getSubTemplate = function(id) {
+    return (this.resolver_ || jstGetTemplate).call(this, id);
+};
+
+/**
+ * HTML template processor. Data values are bound to HTML templates
+ * using the attributes transclude, jsselect, jsdisplay, jscontent,
+ * jsvalues. The template is modifed in place. The values of those
+ * attributes are JavaScript expressions that are evaluated in the
+ * context of the data object fragment.
+ *
+ * @param {JsExprContext} context Context created from the input data
+ * object.
+ *
+ * @param {Element} template DOM node of the template. This will be
+ * processed in place. After processing, it will still be a valid
+ * template that, if processed again with the same data, will remain
+ * unchanged.
+ */
+function jstProcess(context, template) {
+    var processor = new JstProcessor();
+    processor.run_([ processor, processor.jstProcess_, context, template ]);
+}
+
+/**
+ * Internal class used by jstemplates to maintain context.
+ * NOTE: This is necessary to process deep templates in Safari
+ * which has a relatively shallow stack.
+ * @class
+ */
+function JstProcessor() {
+}
+
+/**
+ * Runs the state machine, beginning with function "start".
+ *
+ * @param {Array} start The first function to run, in the form
+ * [object, method, args ...]
+ */
+JstProcessor.prototype.run_ = function(start) {
+    var me = this;
+
+    me.queue_ = [ start ];
+    while (Template_jsLength(me.queue_)) {
+        var f = me.queue_.shift();
+        f[1].apply(f[0], f.slice(2));
+    }
+};
+
+/**
+ * Appends a function to be called later.
+ * Analogous to calling that function on a subsequent line, or a subsequent
+ * iteration of a loop.
+ *
+ * @param {Array} f  A function in the form [object, method, args ...]
+ */
+JstProcessor.prototype.enqueue_ = function(f) {
+    this.queue_.push(f);
+};
+
+/**
+ * Implements internals of jstProcess.
+ *
+ * @param {JsExprContext} context
+ *
+ * @param {Element} template
+ */
+JstProcessor.prototype.jstProcess_ = function(context, template) {
+    var me = this;
+
+    var transclude = Template_domGetAttribute(template, ATT_transclude);
+    if (transclude) {
+        var tr = context.getSubTemplate(transclude);
+        if (tr) {
+            Template_domReplaceChild(tr, template);
+            me.enqueue_([ me, me.jstProcess_, context, tr ]);
+        } else
+            Template_domRemoveNode(template);
+        return;
+    }
+
+    var select = Template_domGetAttribute(template, ATT_select);
+    if (select) {
+        me.jstSelect_(context, template, select);
+        return;
+    }
+
+    var display = Template_domGetAttribute(template, ATT_display);
+    if (display) {
+        if (!context.jseval(display, template)) {
+            Template_displayNone(template);
+            return;
+        }
+        Template_displayDefault(template);
+    }
+
+    var values = Template_domGetAttribute(template, ATT_values);
+    if (values)
+        me.jstValues_(context, template, values);
+
+    var expressions = Template_domGetAttribute(template, ATT_eval);
+    if (expressions) {
+        foreach(expressions.split(/\s*;\s*/), function(expression) {
+            expression = Template_stringTrim(expression);
+            if (Template_jsLength(expression))
+                context.jseval(expression, template);
+        });
+    }
+
+    var content = Template_domGetAttribute(template, ATT_content);
+    if (content)
+        me.jstContent_(context, template, content);
+    else {
+        var childnodes = [];
+        for (var i = 0; i < Template_jsLength(template.childNodes); ++i) {
+            if (template.childNodes[i].nodeType == DOM_ELEMENT_NODE)
+                me.enqueue_([me, me.jstProcess_, context, template.childNodes[i]]);
+        }
+    }
+};
+
+/**
+ * Implements the jsselect attribute: evalutes the value of the
+ * jsselect attribute in the current context, with the current
+ * variable bindings (see JsExprContext.jseval()). If the value is an
+ * array, the current template node is multiplied once for every
+ * element in the array, with the array element being the context
+ * object. If the array is empty, or the value is undefined, then the
+ * current template node is dropped. If the value is not an array,
+ * then it is just made the context object.
+ *
+ * @param {JsExprContext} context The current evaluation context.
+ *
+ * @param {Element} template The currently processed node of the template.
+ *
+ * @param {String} select The javascript expression to evaluate.
+ *
+ * @param {Function} process The function to continue processing with.
+ */
+JstProcessor.prototype.jstSelect_ = function(context, template, select) {
+    var me = this;
+
+    var value = context.jseval(select, template);
+    Template_domRemoveAttribute(template, ATT_select);
+
+    var instance = Template_domGetAttribute(template, ATT_instance);
+    var instance_last = false;
+    if (instance) {
+        if (instance.charAt(0) == '*') {
+            instance = Template_parseInt10(instance.substr(1));
+            instance_last = true;
+        } else
+            instance = Template_parseInt10(instance);
+    }
+
+    var multiple = (value !== null && typeof value == 'object' && typeof value.length == 'number');
+    var multiple_empty = (multiple && value.length == 0);
+
+    if (multiple) {
+        if (multiple_empty) {
+            if (!instance) {
+                Template_domSetAttribute(template, ATT_select, select);
+                Template_domSetAttribute(template, ATT_instance, '*0');
+                Template_displayNone(template);
+            } else
+                Template_domRemoveNode(template);
+        } else {
+            Template_displayDefault(template);
+            if (instance === null || instance === "" || instance === undefined ||
+                (instance_last && instance < Template_jsLength(value) - 1)) {
+                var templatenodes = [];
+                var instances_start = instance || 0;
+                for (var i = instances_start + 1; i < Template_jsLength(value); ++i) {
+                    var node = Template_domCloneNode(template);
+                    templatenodes.push(node);
+                    Template_domInsertBefore(node, template);
+                }
+                templatenodes.push(template);
+
+                for (var i = 0; i < Template_jsLength(templatenodes); ++i) {
+                    var ii = i + instances_start;
+                    var v = value[ii];
+                    var t = templatenodes[i];
+
+                    me.enqueue_([me, me.jstProcess_, context.clone(v, ii), t]);
+                    var instanceStr = (ii == Template_jsLength(value) - 1 ? '*' : '') + ii;
+                    me.enqueue_([null, postProcessMultiple_, t, select, instanceStr]);
+                }
+            } else if (instance < Template_jsLength(value)) {
+                var v = value[instance];
+
+                me.enqueue_([me, me.jstProcess_, context.clone(v, instance), template]);
+                var instanceStr = (instance == Template_jsLength(value) - 1 ? '*' : '') + instance;
+                me.enqueue_([null, postProcessMultiple_, template, select, instanceStr]);
+            } else
+                Template_domRemoveNode(template);
+        }
+    } else {
+        if (value == null) {
+            Template_domSetAttribute(template, ATT_select, select);
+            Template_displayNone(template);
+        } else {
+            me.enqueue_([me, me.jstProcess_, context.clone(value, 0), template]);
+            me.enqueue_([null, postProcessSingle_, template, select]);
+        }
+    }
+};
+
+/**
+ * Sets ATT_select and ATT_instance following recursion to jstProcess.
+ *
+ * @param {Element} template  The template
+ *
+ * @param {String} select  The jsselect string
+ *
+ * @param {String} instanceStr  The new value for the jsinstance attribute
+ */
+function postProcessMultiple_(template, select, instanceStr) {
+    Template_domSetAttribute(template, ATT_select, select);
+    Template_domSetAttribute(template, ATT_instance, instanceStr);
+}
+
+/**
+ * Sets ATT_select and makes the element visible following recursion to
+ * jstProcess.
+ *
+ * @param {Element} template  The template
+ *
+ * @param {String} select  The jsselect string
+ */
+function postProcessSingle_(template, select) {
+    Template_domSetAttribute(template, ATT_select, select);
+    Template_displayDefault(template);
+}
+
+/**
+ * Implements the jsvalues attribute: evaluates each of the values and
+ * assigns them to variables in the current context (if the name
+ * starts with '$', javascript properties of the current template node
+ * (if the name starts with '.'), or DOM attributes of the current
+ * template node (otherwise). Since DOM attribute values are always
+ * strings, the value is coerced to string in the latter case,
+ * otherwise it's the uncoerced javascript value.
+ *
+ * @param {JsExprContext} context Current evaluation context.
+ *
+ * @param {Element} template Currently processed template node.
+ *
+ * @param {String} valuesStr Value of the jsvalues attribute to be
+ * processed.
+ */
+JstProcessor.prototype.jstValues_ = function(context, template, valuesStr) {
+    var values = valuesStr.split(/\s*;\s*/);
+    for (var i = 0; i < Template_jsLength(values); ++i) {
+        var colon = values[i].indexOf(':');
+        if (colon < 0)
+            continue;
+        var label = Template_stringTrim(values[i].substr(0, colon));
+        var value = context.jseval(values[i].substr(colon + 1), template);
+
+        if (label.charAt(0) == '$')
+            context.setVariable(label, value);
+        else if (label.charAt(0) == '.')
+            template[label.substr(1)] = value;
+        else if (label) {
+            if (typeof value == 'boolean') {
+                if (value)
+                    Template_domSetAttribute(template, label, label);
+                else
+                    Template_domRemoveAttribute(template, label);
+            } else
+                Template_domSetAttribute(template, label, '' + value);
+        }
+    }
+};
+
+/**
+ * Implements the jscontent attribute. Evalutes the expression in
+ * jscontent in the current context and with the current variables,
+ * and assigns its string value to the content of the current template
+ * node.
+ *
+ * @param {JsExprContext} context Current evaluation context.
+ *
+ * @param {Element} template Currently processed template node.
+ *
+ * @param {String} content Value of the jscontent attribute to be
+ * processed.
+ */
+JstProcessor.prototype.jstContent_ = function(context, template, content) {
+    var value = '' + context.jseval(content, template);
+    if (template.innerHTML == value)
+        return;
+    while (template.firstChild)
+        Template_domRemoveNode(template.firstChild);
+    var t = Template_domCreateTextNode(Template_ownerDocument(template), value);
+    Template_domAppendChild(template, t);
+};
+
+/**
+ * Helps to implement the transclude attribute, and is the initial
+ * call to get hold of a template from its ID.
+ *
+ * @param {String} name The ID of the HTML element used as template.
+ *
+ * @returns {Element} The DOM node of the template. (Only element
+ * nodes can be found by ID, hence it's a Element.)
+ */
+function jstGetTemplate(name) {
+    var section = Template_domGetElementById(document, name);
+    if (section) {
+        var ret = Template_domCloneNode(section);
+        Template_domRemoveAttribute(ret, 'id');
+        return ret;
+    } else
+        return null;
+}
+
+window['jstGetTemplate'] = jstGetTemplate;
+window['jstProcess'] = jstProcess;
+window['JsExprContext'] = JsExprContext;
+
+function TemplateTest() {
+    // Find the location to insert the content
+    var tp = document.getElementById('benchmark_content');
+
+    // Inject the content
+    tp.innerHTML = content;
+
+    // Run the template
+    var cx = new JsExprContext(
+    {"counters": [
+        {"name":"Chrome:Init","time":5},
+        {"delta":0,"name":"Shutdown:window_close:time","time":111,"value":1},
+        {"delta":0,"name":" Shutdown:window_close:timeMA","value":111},
+        {"delta":0,"name":"Shutdown:window_close:time_pe","time":111,"value":1},
+        {"delta":0,"name":" Shutdown:window_close:time_p","value":111},
+        {"delta":0,"name":"Shutdown:renderers:total","time":1,"value":1},
+        {"delta":0,"name":" Shutdown:renderers:totalMAX","value":1},
+        {"delta":0,"name":"Shutdown:renderers:slow","time":0,"value":1},
+        {"delta":0,"name":" Shutdown:renderers:slowMAX","value":10},
+        {"delta":0,"name":"DNS:PrefetchQueue","time":2,"value":6},
+        {"delta":0,"name":" DNS:PrefetchQueueMAX","value":1},
+        {"delta":0,"name":"DNS:PrefetchFoundNameL","time":1048,"value":1003},
+        {"delta":0,"name":" DNS:PrefetchFoundNameLMAX","value":46},
+        {"delta":0,"name":"SB:QueueDepth","time":0,"value":1},
+        {"delta":0,"name":" SB:QueueDepthMAX","value":0},
+        {"delta":102,"name":"IPC:SendMsgCount","value":1016},
+        {"delta":98,"name":"Chrome:ProcMsgL UI","time":2777,"value":1378},
+        {"delta":2381,"name":" Chrome:ProcMsgL UIMAX","value":2409},
+        {"delta":98,"name":"Chrome:TotalMsgL UI","time":5715,"value":1378},
+        {"delta":1518,"name":" Chrome:TotalMsgL UIMAX","value":2409},
+        {"name":"Chrome:RendererInit","time":9},
+        {"delta":0,"name":"WebFrameActiveCount","value":2},
+        {"delta":0,"name":"Gears:LoadTime","time":1,"value":1},
+        {"delta":0,"name":" Gears:LoadTimeMAX","value":1},
+        {"delta":1,"name":"URLRequestCount","value":41},
+        {"delta":1,"name":"mime_sniffer:ShouldSniffMimeT","time":27,"value":27},
+        {"delta":0,"name":" mime_sniffer:ShouldSniffMime","value":1},
+        {"delta":3,"name":"ResourceLoadServer","time":1065,"value":73},
+        {"delta":0,"name":" ResourceLoadServerMAX","value":51},
+        {"delta":11,"name":"WebFramePaintTime","time":232,"value":42},
+        {"delta":9,"name":" WebFramePaintTimeMAX","value":41},
+        {"delta":11,"name":"MPArch:RWH_OnMsgPaintRect","time":136,"value":42},
+        {"delta":0,"name":" MPArch:RWH_OnMsgPaintRectMAX","value":9},
+        {"delta":0,"name":"NPObjects","value":2},
+        {"delta":6008832,"name":"V8:OsMemoryAllocated","value":28422144},
+        {"delta":7905,"name":"V8:GlobalHandles","value":16832},
+        {"delta":0,"name":"V8:PcreMallocCount","value":0},
+        {"delta":1,"name":"V8:ObjectPropertiesToDictiona","value":16},
+        {"delta":0,"name":"V8:ObjectElementsToDictionary","value":0},
+        {"delta":1128652,"name":"V8:AliveAfterLastGC","value":4467596},
+        {"delta":0,"name":"V8:ObjsSinceLastYoung","value":0},
+        {"delta":0,"name":"V8:ObjsSinceLastFull","value":0},
+        {"delta":2048,"name":"V8:SymbolTableCapacity","value":12288},
+        {"delta":1493,"name":"V8:NumberOfSymbols","value":6865},
+        {"delta":100442,"name":"V8:TotalExternalStringMemory","value":359184},
+        {"delta":0,"name":"V8:ScriptWrappers","value":0},
+        {"delta":3,"name":"V8:CallInitializeStubs","value":20},
+        {"delta":0,"name":"V8:CallPreMonomorphicStubs","value":4},
+        {"delta":0,"name":"V8:CallNormalStubs","value":0},
+        {"delta":6,"name":"V8:CallMegamorphicStubs","value":44},
+        {"delta":0,"name":"V8:ArgumentsAdaptors","value":0},
+        {"delta":647,"name":"V8:CompilationCacheHits","value":1269},
+        {"delta":9,"name":"V8:CompilationCacheMisses","value":57},
+        {"delta":0,"name":"V8:RegExpCacheHits","value":2},
+        {"delta":0,"name":"V8:RegExpCacheMisses","value":6},
+        {"delta":6260,"name":"V8:TotalEvalSize","value":12621},
+        {"delta":50221,"name":"V8:TotalLoadSize","value":217362},
+        {"delta":63734,"name":"V8:TotalParseSize","value":299135},
+        {"delta":15174,"name":"V8:TotalPreparseSkipped","value":61824},
+        {"delta":69932,"name":"V8:TotalCompileSize","value":313048},
+        {"delta":22,"name":"V8:CodeStubs","value":117},
+        {"delta":1185,"name":"V8:TotalStubsCodeSize","value":6456},
+        {"delta":45987,"name":"V8:TotalCompiledCodeSize","value":169546},
+        {"delta":0,"name":"V8:GCCompactorCausedByRequest","value":0},
+        {"delta":0,"name":"V8:GCCompactorCausedByPromote","value":0},
+        {"delta":0,"name":"V8:GCCompactorCausedByOldspac","value":0},
+        {"delta":0,"name":"V8:GCCompactorCausedByWeakHan","value":0},
+        {"delta":0,"name":"V8:GCLastResortFromJS","value":0},
+        {"delta":0,"name":"V8:GCLastResortFromHandles","value":0},
+        {"delta":0,"name":"V8:KeyedLoadGenericSmi","value":0},
+        {"delta":0,"name":"V8:KeyedLoadGenericSymbol","value":0},
+        {"delta":0,"name":"V8:KeyedLoadGenericSlow","value":0},
+        {"delta":0,"name":"V8:KeyedLoadFunctionPrototype","value":0},
+        {"delta":0,"name":"V8:KeyedLoadStringLength","value":0},
+        {"delta":0,"name":"V8:KeyedLoadArrayLength","value":0},
+        {"delta":0,"name":"V8:KeyedLoadConstantFunction","value":0},
+        {"delta":0,"name":"V8:KeyedLoadField","value":0},
+        {"delta":0,"name":"V8:KeyedLoadCallback","value":0},
+        {"delta":0,"name":"V8:KeyedLoadInterceptor","value":0},
+        {"delta":0,"name":"V8:KeyedStoreField","value":0},
+        {"delta":0,"name":"V8:ForIn","value":0},
+        {"delta":2,"name":"V8:EnumCacheHits","value":9},
+        {"delta":4,"name":"V8:EnumCacheMisses","value":23},
+        {"delta":3724,"name":"V8:RelocInfoCount","value":18374},
+        {"delta":6080,"name":"V8:RelocInfoSize","value":30287},
+        {"delta":0,"name":"History:InitTime","time":12,"value":1},
+        {"delta":0,"name":" History:InitTimeMAX","value":12},
+        {"delta":1,"name":"History:GetFavIconForURL","time":0,"value":22},
+        {"delta":0,"name":" History:GetFavIconForURLMAX","value":0},
+        {"delta":2,"name":"V8:PreParse","time":9,"value":11},
+        {"delta":9,"name":"V8:Parse","time":9,"value":57},
+        {"delta":3,"name":"V8:Compile","time":3,"value":22},
+        {"delta":49,"name":"V8:ParseLazy","time":17,"value":231},
+        {"delta":47,"name":"V8:CompileLazy","time":3,"value":221},
+        {"delta":12,"name":"V8:GCScavenger","time":13,"value":28},
+        {"delta":0,"name":"NewTabPage:SearchURLs:Total","time":0,"value":1},
+        {"delta":0,"name":" NewTabPage:SearchURLs:TotalM","value":0},
+        {"delta":6,"name":"V8:CompileEval","time":1,"value":35},
+        {"delta":0,"name":"Memory:CachedFontAndDC","time":3,"value":3},
+        {"delta":0,"name":" Memory:CachedFontAndDCMAX","value":2},
+        {"delta":0,"name":"ResourceLoaderWait","time":1296,"value":48},
+        {"delta":0,"name":" ResourceLoaderWaitMAX","value":55},
+        {"delta":0,"name":"History:GetPageThumbnail","time":15,"value":9},
+        {"delta":0,"name":" History:GetPageThumbnailMAX","value":10},
+        {"delta":9,"name":"MPArch:RWH_InputEventDelta","time":327,"value":170},
+        {"delta":0,"name":" MPArch:RWH_InputEventDeltaMA","value":154},
+        {"delta":0,"name":"Omnibox:QueryBookmarksTime","time":2,"value":44},
+        {"delta":0,"name":" Omnibox:QueryBookmarksTimeMA","value":1},
+        {"delta":0,"name":"Chrome:DelayMsgUI","value":3},
+        {"delta":0,"name":"Autocomplete:HistoryAsyncQuer","time":351,"value":86},
+        {"delta":0,"name":" Autocomplete:HistoryAsyncQue","value":10},
+        {"delta":0,"name":"History:QueryHistory","time":1018,"value":44},
+        {"delta":0,"name":" History:QueryHistoryMAX","value":233},
+        {"delta":0,"name":"DiskCache:GetFileForNewBlock","time":0,"value":34},
+        {"delta":0,"name":" DiskCache:GetFileForNewBlock","value":0},
+        {"delta":0,"name":"DiskCache:CreateBlock","time":0,"value":34},
+        {"delta":0,"name":" DiskCache:CreateBlockMAX","value":0},
+        {"delta":0,"name":"DiskCache:CreateTime","time":0,"value":10},
+        {"delta":0,"name":" DiskCache:CreateTimeMAX","value":0},
+        {"delta":0,"name":"DNS:PrefetchPositiveHitL","time":1048,"value":2},
+        {"delta":0,"name":" DNS:PrefetchPositiveHitLMAX","value":1002},
+        {"delta":0,"name":"DiskCache:GetRankings","time":0,"value":27},
+        {"delta":0,"name":" DiskCache:GetRankingsMAX","value":0},
+        {"delta":0,"name":"DiskCache:DeleteHeader","time":0,"value":3},
+        {"delta":0,"name":" DiskCache:DeleteHeaderMAX","value":0},
+        {"delta":0,"name":"DiskCache:DeleteData","time":0,"value":3},
+        {"delta":0,"name":" DiskCache:DeleteDataMAX","value":0},
+        {"delta":0,"name":"DiskCache:DeleteBlock","time":0,"value":6},
+        {"delta":0,"name":" DiskCache:DeleteBlockMAX","value":0},
+        {"delta":0,"name":"SessionRestore:last_session_f","time":0,"value":1},
+        {"delta":0,"name":" SessionRestore:last_session_","value":0},
+        {"delta":3,"name":"SessionRestore:command_size","time":2940,"value":36},
+        {"delta":0,"name":" SessionRestore:command_sizeM","value":277},
+        {"delta":0,"name":"DNS:IndependentNavigation","time":2,"value":4},
+        {"delta":0,"name":" DNS:IndependentNavigationMAX","value":1},
+        {"delta":0,"name":"DiskCache:UpdateRank","time":1,"value":25},
+        {"delta":0,"name":" DiskCache:UpdateRankMAX","value":1},
+        {"delta":0,"name":"DiskCache:WriteTime","time":1,"value":21},
+        {"delta":0,"name":" DiskCache:WriteTimeMAX","value":1},
+        {"delta":0,"name":"Net:Transaction_Latency","time":183,"value":7},
+        {"delta":0,"name":" Net:Transaction_LatencyMAX","value":37},
+        {"delta":0,"name":"Net:Transaction_Bandwidth","time":40,"value":7},
+        {"delta":0,"name":" Net:Transaction_BandwidthMAX","value":8},
+        {"delta":0,"name":"NewTabUI load","time":564,"value":1},
+        {"delta":0,"name":" NewTabUI loadMAX","value":564},
+        {"delta":0,"name":"DiskCache:OpenTime","time":0,"value":2},
+        {"delta":0,"name":" DiskCache:OpenTimeMAX","value":0},
+        {"delta":0,"name":"DiskCache:ReadTime","time":0,"value":4},
+        {"delta":0,"name":" DiskCache:ReadTimeMAX","value":0},
+        {"delta":0,"name":"MPArch:RWHH_WhiteoutDuration_","time":27,"value":1},
+        {"delta":0,"name":" MPArch:RWHH_WhiteoutDuration","value":27},
+        {"delta":1,"name":"AsyncIO:IPCChannelClose","time":0,"value":4},
+        {"delta":0,"name":" AsyncIO:IPCChannelCloseMAX","value":0},
+        {"name":"GetHistoryTimer","time":0},
+        {"delta":0,"name":"DiskCache:Entries","time":7,"value":1},
+        {"delta":0,"name":" DiskCache:EntriesMAX","value":7},
+        {"delta":0,"name":"DiskCache:Size","time":0,"value":1},
+        {"delta":0,"name":" DiskCache:SizeMAX","value":0},
+        {"delta":0,"name":"DiskCache:MaxSize","time":80,"value":1},
+        {"delta":0,"name":" DiskCache:MaxSizeMAX","value":80},
+        {"delta":0,"name":"History:AddFTSData","time":1,"value":1},
+        {"delta":0,"name":" History:AddFTSDataMAX","value":1},
+        {"delta":0,"name":"Chrome:SlowMsgUI","value":1}
+    ],
+    "timers":[
+        {"name":"Chrome:Init","time":5},
+        {"delta":0,"name":"Shutdown:window_close:time","time":111,"value":1},
+        {"delta":0,"name":"Shutdown:window_close:time_pe","time":111,"value":1},
+        {"delta":0,"name":"Shutdown:renderers:total","time":1,"value":1},
+        {"delta":0,"name":"Shutdown:renderers:slow","time":0,"value":1},
+        {"delta":0,"name":"DNS:PrefetchQueue","time":2,"value":6},
+        {"delta":0,"name":"DNS:PrefetchFoundNameL","time":1048,"value":1003},
+        {"delta":0,"name":"SB:QueueDepth","time":0,"value":1},
+        {"delta":98,"name":"Chrome:ProcMsgL UI","time":2777,"value":1378},
+        {"delta":98,"name":"Chrome:TotalMsgL UI","time":5715,"value":1378},
+        {"name":"Chrome:RendererInit","time":9},
+        {"delta":0,"name":"Gears:LoadTime","time":1,"value":1},
+        {"delta":1,"name":"mime_sniffer:ShouldSniffMimeT","time":27,"value":27},
+        {"delta":3,"name":"ResourceLoadServer","time":1065,"value":73},
+        {"delta":11,"name":"WebFramePaintTime","time":232,"value":42},
+        {"delta":11,"name":"MPArch:RWH_OnMsgPaintRect","time":136,"value":42},
+        {"delta":0,"name":"History:InitTime","time":12,"value":1},
+        {"delta":1,"name":"History:GetFavIconForURL","time":0,"value":22},
+        {"delta":2,"name":"V8:PreParse","time":9,"value":11},
+        {"delta":9,"name":"V8:Parse","time":9,"value":57},
+        {"delta":3,"name":"V8:Compile","time":3,"value":22},
+        {"delta":49,"name":"V8:ParseLazy","time":17,"value":231},
+        {"delta":47,"name":"V8:CompileLazy","time":3,"value":221},
+        {"delta":12,"name":"V8:GCScavenger","time":13,"value":28},
+        {"delta":0,"name":"NewTabPage:SearchURLs:Total","time":0,"value":1},
+        {"delta":6,"name":"V8:CompileEval","time":1,"value":35},
+        {"delta":0,"name":"Memory:CachedFontAndDC","time":3,"value":3},
+        {"delta":0,"name":"ResourceLoaderWait","time":1296,"value":48},
+        {"delta":0,"name":"History:GetPageThumbnail","time":15,"value":9},
+        {"delta":9,"name":"MPArch:RWH_InputEventDelta","time":327,"value":170},
+        {"delta":0,"name":"Omnibox:QueryBookmarksTime","time":2,"value":44},
+        {"delta":0,"name":"Autocomplete:HistoryAsyncQuer","time":351,"value":86},
+        {"delta":0,"name":"History:QueryHistory","time":1018,"value":44},
+        {"delta":0,"name":"DiskCache:GetFileForNewBlock","time":0,"value":34},
+        {"delta":0,"name":"DiskCache:CreateBlock","time":0,"value":34},
+        {"delta":0,"name":"DiskCache:CreateTime","time":0,"value":10},
+        {"delta":0,"name":"DNS:PrefetchPositiveHitL","time":1048,"value":2},
+        {"delta":0,"name":"DiskCache:GetRankings","time":0,"value":27},
+        {"delta":0,"name":"DiskCache:DeleteHeader","time":0,"value":3},
+        {"delta":0,"name":"DiskCache:DeleteData","time":0,"value":3},
+        {"delta":0,"name":"DiskCache:DeleteBlock","time":0,"value":6},
+        {"delta":0,"name":"SessionRestore:last_session_f","time":0,"value":1},
+        {"delta":3,"name":"SessionRestore:command_size","time":2940,"value":36},
+        {"delta":0,"name":"DNS:IndependentNavigation","time":2,"value":4},
+        {"delta":0,"name":"DiskCache:UpdateRank","time":1,"value":25},
+        {"delta":0,"name":"DiskCache:WriteTime","time":1,"value":21},
+        {"delta":0,"name":"Net:Transaction_Latency","time":183,"value":7},
+        {"delta":0,"name":"Net:Transaction_Bandwidth","time":40,"value":7},
+        {"delta":0,"name":"NewTabUI load","time":564,"value":1},
+        {"delta":0,"name":"DiskCache:OpenTime","time":0,"value":2},
+        {"delta":0,"name":"DiskCache:ReadTime","time":0,"value":4},
+        {"delta":0,"name":"MPArch:RWHH_WhiteoutDuration_","time":27,"value":1},
+        {"delta":1,"name":"AsyncIO:IPCChannelClose","time":0,"value":4},
+        {"name":"GetHistoryTimer","time":0},
+        {"delta":0,"name":"DiskCache:Entries","time":7,"value":1},
+        {"delta":0,"name":"DiskCache:Size","time":0,"value":1},
+        {"delta":0,"name":"DiskCache:MaxSize","time":80,"value":1},
+        {"delta":0,"name":"History:AddFTSData","time":1,"value":1}
+    ]});
+    jstProcess(cx, tp);
+}
+
+var TemplateTest = new BenchmarkSuite('Template', [new Benchmark("Template",TemplateTest)]);
index 7d4871e..155453d 100644 (file)
@@ -85,10 +85,15 @@ function runLoop()
 }
 
 function run() {
-    var start = new Date();
-    for (var i = 0; i < window.loopsPerRun; ++i)
-        window.runFunction();
-    var time = new Date() - start;
+    if (window.customRunFunction)
+        var time = window.customRunFunction();
+    else {
+        var start = new Date();
+        for (var i = 0; i < window.loopsPerRun; ++i)
+            window.runFunction();
+        var time = new Date() - start;
+    }
+
     window.completedRuns++;
     if (window.completedRuns <= 0) {
         log("Ignoring warm-up run (" + time + ")");
@@ -109,6 +114,16 @@ function start(runCount, runFunction, loopsPerRun, doneFunction) {
     runLoop();
 }
 
+function startCustom(runCount, customRunFunction, doneFunction) {
+    window.runCount = runCount;
+    window.customRunFunction = customRunFunction;
+    window.loopsPerRun = 1;
+    window.doneFunction = doneFunction || function() {};
+
+    log("Running " + runCount + " times");
+    runLoop();
+}
+
 if (window.layoutTestController) {
     layoutTestController.waitUntilDone();
     layoutTestController.dumpAsText();