Merge from Chromium at DEPS revision r167172

This commit was generated by merge_to_master.py.

Change-Id: Iead6b4948cd90f0aac77a0e5e2b6c1749577569b
diff --git a/Tools/TestResultServer/static-dashboards/LICENSE.dygraph.txt b/Tools/TestResultServer/static-dashboards/LICENSE.dygraph.txt
new file mode 100644
index 0000000..536c0a8
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/LICENSE.dygraph.txt
@@ -0,0 +1,22 @@
+Copyright (c) 2009 Dan Vanderkam
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Tools/TestResultServer/static-dashboards/README.dygraph.txt b/Tools/TestResultServer/static-dashboards/README.dygraph.txt
new file mode 100644
index 0000000..e800692
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/README.dygraph.txt
@@ -0,0 +1,55 @@
+dygraphs JavaScript charting library
+Copyright (c) 2006-, Dan Vanderkam.
+
+Support: http://groups.google.com/group/dygraphs-users
+Source: http://github.com/danvk/dygraphs
+Issues: http://code.google.com/p/dygraphs/
+
+
+The dygraphs JavaScript library produces produces interactive, zoomable charts of time series.
+
+Features
+- Plots time series without using an external server or Flash
+- Supports multiple data series
+- Supports error bands around data series
+- Displays values on mouseover
+- Interactive zoom
+- Adjustable averaging period
+- Customizable click-through actions
+- Compatible with the Google Visualization API
+
+Demo
+For a gallery and documentation, see http://danvk.org/dygraphs/
+
+Minimal Example
+<html>
+<head>
+<script type="text/javascript" src="dygraph-combined.js"></script>
+</head>
+<body>
+<div id="graphdiv"></div>
+<script type="text/javascript">
+  g = new Dygraph(
+        document.getElementById("graphdiv"),  // containing div
+        "Date,Temperature\n" +                // the data series
+        "2008-05-07,75\n" +
+        "2008-05-08,70\n" +
+        "2008-05-09,80\n"
+      );
+</script>
+</body>
+</html>
+
+License(s)
+dygraphs uses:
+ - rgbcolor.js (Public Domain)
+ - strftime.js (BSD License)
+ - excanvas.js (Apache License)
+ - YUI compressor (BSD License)
+
+rgbcolor: http://www.phpied.com/rgb-color-parser-in-javascript/
+strftime: http://tech.bluesmoon.info/2008/04/strftime-in-javascript.html
+excanvas: http://code.google.com/p/explorercanvas/
+yui compressor: http://developer.yahoo.com/yui/compressor/
+
+dygraphs is available under the MIT license, included in LICENSE.txt.
diff --git a/Tools/TestResultServer/static-dashboards/README.webtreemap.txt b/Tools/TestResultServer/static-dashboards/README.webtreemap.txt
new file mode 100644
index 0000000..d9d885b
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/README.webtreemap.txt
@@ -0,0 +1,63 @@
+# webtreemap
+
+A simple treemap implementation using web technologies (DOM nodes, CSS
+styling and transitions) rather than a big canvas/svg/plugin.
+
+Play with a [demo][].
+
+[demo]: http://martine.github.com/webtreemap/demo/demo.html
+
+## Creating your own
+
+1. Create a page with a DOM node (i.e. a `<div>`) that will contain
+   your treemap.
+2. Add the treemap to the node via something like
+
+        appendTreemap(document.getElementById('mynode'), mydata);
+3. Style the treemap using CSS.
+
+### Input format
+
+The input data (`mydata` in the overview snippet) is a tree of nodes,
+likely imported via a separate JSON file.  Each node (including the
+root) should contain data in the following format.
+
+    {
+      name: (HTML that is displayed via .innerHTML on the caption),
+      data: {
+        "$area": (a number, in arbitrary units)
+      },
+      children: (list of child tree nodes)
+    }
+
+(This strange format for data comes from the the [JavaScript InfoVis
+Toolkit][thejit].  I might change it in the future.)
+
+The `$area` of a node should be the sum of the `$area` of all of its
+`children`.
+
+(At runtime, tree nodes will dynamically will gain two extra
+attributes, `parent` and `dom`; this is only worth pointing out so
+that you don't accidentally conflict with them.)
+
+### CSS styling
+
+The treemap is constructed with one `div` per region with a separate
+`div` for the caption.  Each div is styleable via the
+`webtreemap-node` CSS class.  The captions are stylable as
+`webtreemap-caption`.
+
+Each level of the tree also gets a per-level CSS class,
+`webtreemap-level0` through `webtreemap-level4`.  These can be
+adjusted to e.g. made different levels different colors.  To control
+the caption on a per-level basis, use a CSS selector like
+`.webtreemap-level2 > .webtreemap-caption`.
+
+Your best bet is to modify the included `webtreemap.css`, which
+contains comments about required and optional CSS attributes.
+
+## Related projects
+
+* [JavaScript InfoVis Toolkit][thejit]
+
+[thejit]: http://thejit.org/
\ No newline at end of file
diff --git a/Tools/TestResultServer/static-dashboards/aggregate_results.html b/Tools/TestResultServer/static-dashboards/aggregate_results.html
new file mode 100644
index 0000000..fa033a5
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/aggregate_results.html
@@ -0,0 +1,299 @@
+<!-- Copyright (C) 2011 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.
+-->
+<!DOCTYPE HTML>
+<html>
+
+<head>
+<title>Layout test passing status</title>
+<style>
+h2 {
+    margin: 0;
+    font-size: 1.2em;
+}
+h3 {
+    margin-bottom: 0;
+    font-size: 1em;
+}
+.container {
+    display: inline-block;
+    padding: 3px;
+}
+img {
+    border: 1px dotted grey;
+    margin-right: 5px;
+    padding: 2px;
+}
+</style>
+<script src="builders.js"></script>
+<script src="loader.js"></script>
+<script src="dashboard_base.js"></script>
+<script>
+// @fileoverview Creates a dashboard for tracking number of passes/failures per run.
+//
+// Currently, only webkit tests are supported, but adding other test types
+// should just require the following steps:
+//     -generate results.json for these tests
+//     -copy them to the appropriate location
+//     -add the builder name to the list of builders in dashboard_base.js.
+
+//////////////////////////////////////////////////////////////////////////////
+// Methods and objects from dashboard_base.js to override.
+//////////////////////////////////////////////////////////////////////////////
+function generatePage()
+{
+    var html = htmlForTestTypeSwitcher(true) + '<br>';
+    for (var builder in g_builders)
+        html += htmlForBuilder(builder);
+    document.body.innerHTML = html;
+}
+
+function handleValidHashParameter(key, value)
+{
+    switch(key) {
+    case 'rawValues':
+        g_currentState[key] = value == 'true';
+        return true;
+
+    default:
+        return false;
+    }
+}
+
+g_defaultDashboardSpecificStateValues = {
+    rawValues: false
+};
+
+function htmlForBuilder(builder)
+{
+    var results = g_resultsByBuilder[builder];
+    // Some keys were added later than others, so they don't have as many
+    // builds. Use the shortest.
+    // FIXME: Once 500 runs have finished, we can get rid of passing this
+    // around and just assume all keys have the same number of builders for a
+    // given builder.
+    var numColumns = results[ALL_FIXABLE_COUNT_KEY].length;
+    var html = '<div class=container><h2>' + builder + '</h2>';
+
+    if (g_currentState.rawValues)
+        html += rawValuesHTML(results, numColumns);
+    else {
+        html += '<a href="timeline_explorer.html' + (location.hash ? location.hash + '&' : '#') + 'builder=' + builder + '">' +
+            chartHTML(results, numColumns) + '</a>';
+    }
+
+    html += '</div>';
+    return html;
+}
+
+function rawValuesHTML(results, numColumns)
+{
+    var html = htmlForSummaryTable(results, numColumns) +
+        htmlForTestType(results, FIXABLE_COUNTS_KEY, FIXABLE_DESCRIPTION, numColumns);
+    if (isLayoutTestResults()) {
+        html += htmlForTestType(results, DEFERRED_COUNTS_KEY, DEFERRED_DESCRIPTION, numColumns) +
+            htmlForTestType(results, WONTFIX_COUNTS_KEY, WONTFIX_DESCRIPTION, numColumns);
+    }
+    return html;
+}
+
+function chartHTML(results, numColumns)
+{
+    var shouldShowWebKitRevisions = isTipOfTreeWebKitBuilder();
+    var revisionKey = shouldShowWebKitRevisions ? WEBKIT_REVISIONS_KEY : CHROME_REVISIONS_KEY;
+    var startRevision = results[revisionKey][numColumns - 1];
+    var endRevision = results[revisionKey][0];
+    var revisionLabel = shouldShowWebKitRevisions ? "WebKit Revision" : "Chromium Revision";
+
+    var fixable = results[FIXABLE_COUNT_KEY].slice(0, numColumns);
+    var html = chart("Total failing", {"": fixable}, revisionLabel, startRevision, endRevision);
+
+    var values = valuesPerExpectation(results[FIXABLE_COUNTS_KEY], numColumns);
+    // Don't care about number of passes for the charts.
+    delete(values['P']);
+
+    return html + chart("Detailed breakdown", values, revisionLabel, startRevision, endRevision);
+}
+
+var LABEL_COLORS = ['FF0000', '00FF00', '0000FF', '000000', 'FF6EB4', 'FFA812', '9B30FF', '00FFCC'];
+
+// FIXME: Find a better way to exclude outliers. This is just so we exclude
+// runs where every test failed.
+var MAX_VALUE = 10000;
+
+function filteredValues(values, desiredNumberOfPoints)
+{
+    // Filter out values to make the graph a bit more readable and to keep URLs
+    // from exceeding the browsers max length restriction.
+    var filterAmount = Math.floor(values.length / desiredNumberOfPoints);
+    if (filterAmount < 1)
+        return values;
+
+    return values.filter(function(element, index, array) {
+        // Include the most recent and oldest values and exclude outliers.
+        return (index % filterAmount == 0 || index == array.length - 1) && (array[index] < MAX_VALUE && array[index] != 0);
+    });
+}
+
+function chartUrl(title, values, revisionLabel, startRevision, endRevision, desiredNumberOfPoints) {
+    var maxValue = 0;
+    for (var expectation in values)
+        maxValue = Math.max(maxValue, Math.max.apply(null, filteredValues(values[expectation], desiredNumberOfPoints)));
+
+    var chartData = '';
+    var labels = '';
+    var numLabels = 0;
+
+    var first = true;
+    for (var expectation in values) {
+        chartData += (first ? 'e:' : ',') + extendedEncode(filteredValues(values[expectation], desiredNumberOfPoints).reverse(), maxValue);
+
+        if (expectation) {
+            numLabels++;
+            labels += (first ? '' : '|') + expectationsMap()[expectation];
+        }
+        first = false;
+    }
+
+    var url = "http://chart.apis.google.com/chart?cht=lc&chs=600x400&chd=" +
+            chartData + "&chg=15,15,1,3&chxt=x,x,y&chxl=1:||" + revisionLabel +
+            "|&chxr=0," + startRevision + "," + endRevision + "|2,0," + maxValue + "&chtt=" + title;
+
+
+    if (labels)
+        url += "&chdl=" + labels + "&chco=" + LABEL_COLORS.slice(0, numLabels).join(',');
+    return url;
+}
+
+function chart(title, values, revisionLabel, startRevision, endRevision)
+{
+    var desiredNumberOfPoints = 400;
+    var url = chartUrl(title, values, revisionLabel, startRevision, endRevision, desiredNumberOfPoints);
+
+    while (url.length >= 2048) {
+        // Decrease the desired number of points gradually until we get a URL that
+        // doesn't exceed chartserver's max URL length.
+        desiredNumberOfPoints = 3 / 4 * desiredNumberOfPoints;
+        url = chartUrl(title, values, revisionLabel, startRevision, endRevision, desiredNumberOfPoints);
+    }
+
+    return '<img src="' + url + '">';
+}
+
+function htmlForRevisionRows(results, numColumns)
+{
+    return htmlForTableRow('WebKit Revision', results[WEBKIT_REVISIONS_KEY].slice(0, numColumns)) +
+        htmlForTableRow('Chrome Revision', results[CHROME_REVISIONS_KEY].slice(0, numColumns));
+}
+
+function wrapHTMLInTable(description, html)
+{
+    return '<h3>' + description + '</h3><table><tbody>' + html + '</tbody></table>';
+}
+
+function htmlForSummaryTable(results, numColumns)
+{
+    var percent = [];
+    var fixable = results[FIXABLE_COUNT_KEY].slice(0, numColumns);
+    var allFixable = results[ALL_FIXABLE_COUNT_KEY].slice(0, numColumns);
+    for (var i = 0; i < numColumns; i++) {
+        var percentage = 100 * (allFixable[i] - fixable[i]) / allFixable[i];
+        // Round to the nearest tenth of a percent.
+        percent.push(Math.round(percentage * 10) / 10 + '%');
+    }
+    var html = htmlForRevisionRows(results, numColumns) +
+        htmlForTableRow('Percent passed', percent) +
+        htmlForTableRow('Failures (deduped)', fixable) +
+        htmlForTableRow('Fixable Tests', allFixable);
+    return wrapHTMLInTable('Summary', html);
+}
+
+function valuesPerExpectation(counts, numColumns)
+{
+    var values = {};
+    for (var i = 0; i < numColumns; i++) {
+        for (var expectation in expectationsMap()) {
+            if (expectation in counts[i]) {
+                var count = counts[i][expectation];
+                if (!values[expectation])
+                    values[expectation] = [];
+                values[expectation].push(count);
+            }
+        }
+    }
+    return values;
+}
+
+function htmlForTestType(results, key, description, numColumns)
+{
+    var counts = results[key];
+    var html = htmlForRevisionRows(results, numColumns);
+    var values = valuesPerExpectation(counts, numColumns);
+    for (var expectation in values)
+        html += htmlForTableRow(expectationsMap()[expectation], values[expectation]);
+    return wrapHTMLInTable(description, html);
+}
+
+function htmlForTableRow(columnName, values)
+{
+    return '<tr><td>' + columnName + '</td><td>' + values.join('</td><td>') + '</td></tr>';
+}
+
+// Taken from http://code.google.com/apis/chart/docs/data_formats.html.
+function extendedEncode(arrVals, maxVal)
+{
+    var map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.';
+    var mapLength = map.length;
+    var mapLengthSquared = mapLength * mapLength;
+
+    var chartData = '';
+
+    for (var i = 0, len = arrVals.length; i < len; i++) {
+        // In case the array vals were translated to strings.
+        var numericVal = new Number(arrVals[i]);
+        // Scale the value to maxVal.
+        var scaledVal = Math.floor(mapLengthSquared * numericVal / maxVal);
+
+        if(scaledVal > mapLengthSquared - 1)
+            chartData += "..";
+        else if (scaledVal < 0)
+            chartData += '__';
+        else {
+            // Calculate first and second digits and add them to the output.
+            var quotient = Math.floor(scaledVal / mapLength);
+            var remainder = scaledVal - mapLength * quotient;
+            chartData += map.charAt(quotient) + map.charAt(remainder);
+        }
+    }
+
+    return chartData;
+}
+</script>
+</head>
+<body></body>
+</html>
diff --git a/Tools/TestResultServer/static-dashboards/builders.js b/Tools/TestResultServer/static-dashboards/builders.js
new file mode 100644
index 0000000..2a6b67d
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/builders.js
@@ -0,0 +1,332 @@
+// Copyright (C) 2012 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.
+
+// @fileoverview File that lists builders, their masters, and logical groupings
+// of them.
+
+function BuilderMaster(name, basePath)
+{
+    this.name = name;
+    this.basePath = basePath;
+}
+
+BuilderMaster.prototype.logPath = function(builder, buildNumber)
+{
+    return this.basePath + 'builders/' + builder + '/builds/' + buildNumber;
+};
+
+BuilderMaster.prototype.builderJsonPath = function()
+{
+    return this.basePath + 'json/builders';
+};
+
+CHROMIUM_WIN_BUILDER_MASTER = new BuilderMaster('ChromiumWin', 'http://build.chromium.org/p/chromium.win/');
+CHROMIUM_MAC_BUILDER_MASTER = new BuilderMaster('ChromiumMac', 'http://build.chromium.org/p/chromium.mac/');
+CHROMIUM_LINUX_BUILDER_MASTER = new BuilderMaster('ChromiumLinux', 'http://build.chromium.org/p/chromium.linux/');
+CHROMIUMOS_BUILDER_MASTER = new BuilderMaster('ChromiumChromiumOS', 'http://build.chromium.org/p/chromium.chromiumos/');
+CHROMIUM_GPU_BUILDER_MASTER = new BuilderMaster('ChromiumGPU', 'http://build.chromium.org/p/chromium.gpu/');
+CHROMIUM_GPU_FYI_BUILDER_MASTER = new BuilderMaster('ChromiumGPUFYI', 'http://build.chromium.org/p/chromium.gpu.fyi/');
+CHROMIUM_PERF_AV_BUILDER_MASTER = new BuilderMaster('ChromiumPerfAv', 'http://build.chromium.org/p/chromium.perf_av/');
+CHROMIUM_WEBKIT_BUILDER_MASTER = new BuilderMaster('ChromiumWebkit', 'http://build.chromium.org/p/chromium.webkit/');
+WEBKIT_BUILDER_MASTER = new BuilderMaster('webkit.org', 'http://build.webkit.org/');
+
+var LEGACY_BUILDER_MASTERS_TO_GROUPS = {
+    'Chromium': '@DEPS - chromium.org',
+    'ChromiumWin': '@DEPS - chromium.org',
+    'ChromiumMac': '@DEPS - chromium.org',
+    'ChromiumLinux': '@DEPS - chromium.org',
+    'ChromiumChromiumOS': '@DEPS CrOS - chromium.org',
+    'ChromiumGPU': '@DEPS - chromium.org',
+    'ChromiumGPUFYI': '@DEPS FYI - chromium.org',
+    'ChromiumPerfAv': '@DEPS - chromium.org',
+    'ChromiumWebkit': '@ToT - chromium.org',
+    'webkit.org': '@ToT - webkit.org'
+};
+
+function BuilderGroup(isToTWebKit)
+{
+    this.isToTWebKit = isToTWebKit;
+    // Map of builderName (the name shown in the waterfall) to builderPath (the
+    // path used in the builder's URL)
+    this.builders = {};
+    this.groups = 0;
+    this.expectedGroups = 0;
+}
+
+BuilderGroup.prototype.setbuilder = function(builder, flags) {
+    this.builders[builder] = builder.replace(/[ .()]/g, '_');
+    // FIXME: Remove this at some point, we don't actually use DEFAULT_BUILDER
+    //        in any meaningful way anymore.  We always just default to the
+    //        first builder in alphabetical order.
+    if (flags & BuilderGroup.DEFAULT_BUILDER)
+        this.defaultBuilder = builder;
+};
+
+BuilderGroup.prototype.append = function(builders) {
+    builders.forEach(function(builderAndFlags) {
+        var builder = builderAndFlags[0];
+        var flags = builderAndFlags[1];
+        this.setbuilder(builder, flags);
+    }, this);
+    this.groups += 1;
+};
+
+BuilderGroup.prototype.loaded = function() {
+    return this.groups >= this.expectedGroups;
+}
+
+BuilderGroup.prototype.setup = function()
+{
+    // FIXME: instead of copying these to globals, it would be better if
+    // the rest of the code read things from the BuilderGroup instance directly
+    g_defaultBuilderName = this.defaultBuilder;
+    g_builders = this.builders;
+};
+
+BuilderGroup.TOT_WEBKIT = true;
+BuilderGroup.DEPS_WEBKIT = false;
+BuilderGroup.DEFAULT_BUILDER = 1 << 1;
+
+var BUILDER_TO_MASTER = {};
+function associateBuildersWithMaster(builders, master)
+{
+    builders.forEach(function(builderAndFlags) {
+        var builder = builderAndFlags[0];
+        BUILDER_TO_MASTER[builder] = master;
+    });
+}
+
+function requestBuilderList(builderGroups, builderFilter, master, groupName, builderGroup)
+{
+    if (!builderGroups[groupName])
+        builderGroups[groupName] = builderGroup;
+    loader.request(master.builderJsonPath(),
+                   partial(onBuilderListLoad, builderGroups, builderFilter, master, groupName),
+                   partial(onErrorLoadingBuilderList, master.builderJsonPath(), builderGroups, groupName));
+    builderGroups[groupName].expectedGroups += 1;
+}
+
+function isChromiumDepsGpuTestRunner(builder)
+{
+    return true;
+}
+
+function isChromiumDepsFyiGpuTestRunner(builder)
+{
+    // FIXME: This is kind of wonky, but there's not really a better pattern.
+    return builder.indexOf('(') != -1;
+}
+
+function isChromiumTipOfTreeGpuTestRunner(builder)
+{
+    return builder.indexOf('GPU') != -1;
+}
+
+function isWebkitTestRunner(builder)
+{
+    if (builder.indexOf('EFL') != -1)
+        return builder.indexOf('Build') == -1;
+    if (builder.indexOf('Tests') != -1) {
+        // Apple Windows bots still run old-run-webkit-tests, so they don't upload data.
+        return builder.indexOf('Win') == -1 || (builder.indexOf('Qt') != -1 && builder.indexOf('Chromium') != -1);
+    }
+    return builder.indexOf('GTK') != -1 || builder == 'Qt Linux Release';
+}
+
+function isChromiumContentShellTestRunner(builder)
+{
+    return builder.indexOf('(Content Shell)') != -1;
+}
+
+function isChromiumWebkitTipOfTreeTestRunner(builder)
+{
+    // FIXME: Remove the Android check once the android tests bot is actually uploading results.
+    return builder.indexOf('WebKit') != -1 && builder.indexOf('Builder') == -1 && builder.indexOf('(deps)') == -1 &&
+        builder.indexOf('ASAN') == -1 && !isChromiumContentShellTestRunner(builder) && builder.indexOf('Android') == -1;
+}
+
+function isChromiumWebkitDepsTestRunner(builder)
+{
+    return builder.indexOf('WebKit') != -1 && builder.indexOf('Builder') == -1 && builder.indexOf('(deps)') != -1;
+}
+
+function isChromiumDepsGTestRunner(builder)
+{
+    return builder.indexOf('Builder') == -1;
+}
+
+function isChromiumDepsCrosGTestRunner(builder)
+{
+    return builder.indexOf('Tests') != -1;
+}
+
+function isChromiumTipOfTreeGTestRunner(builder)
+{
+    return !isChromiumTipOfTreeGpuTestRunner(builder) && builder.indexOf('Builder') == -1 && builder.indexOf('Perf') == -1 &&
+         builder.indexOf('WebKit') == -1 && builder.indexOf('Valgrind') == -1 && builder.indexOf('Chrome Frame') == -1;
+}
+
+function isChromiumDepsAVTestRunner(builder)
+{
+    return builder.indexOf('Builder') == -1;
+}
+
+function generateBuildersFromBuilderList(builderList, filter)
+{
+    return builderList.filter(filter).map(function(tester, index) {
+        var builder = [tester];
+        if (!index)
+            builder.push(BuilderGroup.DEFAULT_BUILDER);
+        return builder;
+    });
+}
+
+function onBuilderListLoad(builderGroups, builderFilter, master, groupName, xhr)
+{
+    var builders = generateBuildersFromBuilderList(Object.keys(JSON.parse(xhr.responseText)), builderFilter);
+    associateBuildersWithMaster(builders, master);
+    builderGroups[groupName].append(builders);
+    if (builderGroups[groupName].loaded())
+        g_resourceLoader.buildersListLoaded();
+}
+
+function onErrorLoadingBuilderList(url, builderGroups, groupName, xhr)
+{
+    builderGroups[groupName].groups += 1;
+    console.log('Could not load list of builders from ' + url + '. Try reloading.');
+}
+
+function loadBuildersList(groupName, testType) {
+    switch (testType) {
+    case 'gl_tests':
+    case 'gpu_tests':
+        switch(groupName) {
+        case '@DEPS - chromium.org':
+            var builderGroup = new BuilderGroup(BuilderGroup.DEPS_WEBKIT);
+            requestBuilderList(CHROMIUM_GPU_TESTS_BUILDER_GROUPS, isChromiumDepsGpuTestRunner, CHROMIUM_GPU_BUILDER_MASTER, groupName, builderGroup);
+            break;
+
+        case '@DEPS FYI - chromium.org':
+            var builderGroup = new BuilderGroup(BuilderGroup.DEPS_WEBKIT);
+            requestBuilderList(CHROMIUM_GPU_TESTS_BUILDER_GROUPS, isChromiumDepsFyiGpuTestRunner, CHROMIUM_GPU_FYI_BUILDER_MASTER, groupName, builderGroup);
+            break;
+
+        case '@ToT - chromium.org':
+            var builderGroup = new BuilderGroup(BuilderGroup.TOT_WEBKIT);
+            requestBuilderList(CHROMIUM_GPU_TESTS_BUILDER_GROUPS, isChromiumTipOfTreeGpuTestRunner, CHROMIUM_WEBKIT_BUILDER_MASTER, groupName, builderGroup);
+            break;
+        }
+        break;
+
+    case 'layout-tests':
+        switch(groupName) {
+        case 'Content Shell @ToT - chromium.org':
+            var builderGroup = new BuilderGroup(BuilderGroup.TOT_WEBKIT);
+            requestBuilderList(LAYOUT_TESTS_BUILDER_GROUPS, isChromiumContentShellTestRunner, CHROMIUM_WEBKIT_BUILDER_MASTER, groupName, builderGroup);
+            break;
+
+        case '@ToT - chromium.org':
+            var builderGroup = new BuilderGroup(BuilderGroup.TOT_WEBKIT);
+            requestBuilderList(LAYOUT_TESTS_BUILDER_GROUPS, isChromiumWebkitTipOfTreeTestRunner, CHROMIUM_WEBKIT_BUILDER_MASTER, groupName, builderGroup);
+            break;
+
+        case '@ToT - webkit.org':
+            var builderGroup = new BuilderGroup(BuilderGroup.TOT_WEBKIT);
+            requestBuilderList(LAYOUT_TESTS_BUILDER_GROUPS, isWebkitTestRunner, WEBKIT_BUILDER_MASTER, groupName, builderGroup);
+            break;
+
+        case '@DEPS - chromium.org':
+            var builderGroup = new BuilderGroup(BuilderGroup.DEPS_WEBKIT);
+            requestBuilderList(LAYOUT_TESTS_BUILDER_GROUPS, isChromiumWebkitDepsTestRunner, CHROMIUM_WEBKIT_BUILDER_MASTER, groupName, builderGroup);
+            requestBuilderList(LAYOUT_TESTS_BUILDER_GROUPS, isChromiumDepsAVTestRunner, CHROMIUM_PERF_AV_BUILDER_MASTER, groupName, builderGroup);
+            break;
+        }
+        break;
+   
+    case 'test_shell_tests':
+    case 'webkit_unit_tests':
+        switch(groupName) {
+        case '@ToT - chromium.org':
+            var builderGroup = new BuilderGroup(BuilderGroup.TOT_WEBKIT);
+            requestBuilderList(TEST_SHELL_TESTS_BUILDER_GROUPS, isChromiumWebkitTipOfTreeTestRunner, CHROMIUM_WEBKIT_BUILDER_MASTER, groupName, builderGroup);
+            break;
+
+        case '@DEPS - chromium.org':
+            var builderGroup = new BuilderGroup(BuilderGroup.DEPS_WEBKIT);
+            requestBuilderList(TEST_SHELL_TESTS_BUILDER_GROUPS, isChromiumWebkitDepsTestRunner, CHROMIUM_WEBKIT_BUILDER_MASTER, groupName, builderGroup);
+            requestBuilderList(TEST_SHELL_TESTS_BUILDER_GROUPS, isChromiumDepsAVTestRunner, CHROMIUM_PERF_AV_BUILDER_MASTER, groupName, builderGroup);
+            break;
+        }
+        break;    
+
+    default:
+        switch(groupName) {
+        case '@DEPS - chromium.org':
+            var builderGroup = new BuilderGroup(BuilderGroup.DEPS_WEBKIT);
+            requestBuilderList(CHROMIUM_GTESTS_BUILDER_GROUPS, isChromiumDepsGTestRunner, CHROMIUM_WIN_BUILDER_MASTER, groupName, builderGroup);
+            requestBuilderList(CHROMIUM_GTESTS_BUILDER_GROUPS, isChromiumDepsGTestRunner, CHROMIUM_MAC_BUILDER_MASTER, groupName, builderGroup);
+            requestBuilderList(CHROMIUM_GTESTS_BUILDER_GROUPS, isChromiumDepsGTestRunner, CHROMIUM_LINUX_BUILDER_MASTER, groupName, builderGroup);
+            break;
+
+        case '@DEPS CrOS - chromium.org':
+            var builderGroup = new BuilderGroup(BuilderGroup.DEPS_WEBKIT);
+            requestBuilderList(CHROMIUM_GTESTS_BUILDER_GROUPS, isChromiumDepsCrosGTestRunner, CHROMIUMOS_BUILDER_MASTER, groupName, builderGroup);
+            break;
+
+        case '@ToT - chromium.org':
+            var builderGroup = new BuilderGroup(BuilderGroup.TOT_WEBKIT);
+            requestBuilderList(CHROMIUM_GTESTS_BUILDER_GROUPS, isChromiumTipOfTreeGTestRunner, CHROMIUM_WEBKIT_BUILDER_MASTER, groupName, builderGroup);
+            break;
+        }
+        break;
+    }
+}
+
+var TEST_SHELL_TESTS_BUILDER_GROUPS = {
+    '@ToT - chromium.org': null,
+    '@DEPS - chromium.org': null,
+};
+
+var LAYOUT_TESTS_BUILDER_GROUPS = {
+    '@ToT - chromium.org': null,
+    '@ToT - webkit.org': null,
+    '@DEPS - chromium.org': null,
+    'Content Shell @ToT - chromium.org': null,
+};
+
+var CHROMIUM_GPU_TESTS_BUILDER_GROUPS = {
+    '@DEPS - chromium.org': null,
+    '@DEPS FYI - chromium.org': null,
+    '@ToT - chromium.org': null,
+};
+
+var CHROMIUM_GTESTS_BUILDER_GROUPS = {
+    '@DEPS - chromium.org': null,
+    '@DEPS CrOS - chromium.org': null,
+    '@ToT - chromium.org': null,
+};
diff --git a/Tools/TestResultServer/static-dashboards/dashboard_base.js b/Tools/TestResultServer/static-dashboards/dashboard_base.js
new file mode 100644
index 0000000..6ec0c8e
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/dashboard_base.js
@@ -0,0 +1,915 @@
+// Copyright (C) 2012 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.
+
+// @fileoverview Base JS file for pages that want to parse the results JSON
+// from the testing bots. This deals with generic utility functions, visible
+// history, popups and appending the script elements for the JSON files.
+//
+// The calling page is expected to implement the following "abstract"
+// functions/objects:
+var g_pageLoadStartTime = Date.now();
+var g_resourceLoader;
+
+// Generates the contents of the dashboard. The page should override this with
+// a function that generates the page assuming all resources have loaded.
+function generatePage() {}
+
+// Takes a key and a value and sets the g_currentState[key] = value iff key is
+// a valid hash parameter and the value is a valid value for that key.
+//
+// @return {boolean} Whether the key what inserted into the g_currentState.
+function handleValidHashParameter(key, value)
+{
+    return false;
+}
+
+// Default hash parameters for the page. The page should override this to create
+// default states.
+var g_defaultDashboardSpecificStateValues = {};
+
+
+// The page should override this to modify page state due to
+// changing query parameters.
+// @param {Object} params New or modified query params as key: value.
+// @return {boolean} Whether changing this parameter should cause generatePage to be called.
+function handleQueryParameterChange(params)
+{
+    return true;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// CONSTANTS
+//////////////////////////////////////////////////////////////////////////////
+var GTEST_EXPECTATIONS_MAP_ = {
+    'P': 'PASS',
+    'F': 'FAIL',
+    'N': 'NO DATA',
+    'X': 'SKIPPED'
+};
+
+var LAYOUT_TEST_EXPECTATIONS_MAP_ = {
+    'P': 'PASS',
+    'N': 'NO DATA',
+    'X': 'SKIP',
+    'T': 'TIMEOUT',
+    'F': 'TEXT',
+    'C': 'CRASH',
+    'I': 'IMAGE',
+    'Z': 'IMAGE+TEXT',
+    // We used to glob a bunch of expectations into "O" as OTHER. Expectations
+    // are more precise now though and it just means MISSING.
+    'O': 'MISSING'
+};
+
+var FAILURE_EXPECTATIONS_ = {
+    'T': 1,
+    'F': 1,
+    'C': 1,
+    'I': 1,
+    'Z': 1
+};
+
+// Keys in the JSON files.
+var WONTFIX_COUNTS_KEY = 'wontfixCounts';
+var FIXABLE_COUNTS_KEY = 'fixableCounts';
+var DEFERRED_COUNTS_KEY = 'deferredCounts';
+var WONTFIX_DESCRIPTION = 'Tests never to be fixed (WONTFIX)';
+var FIXABLE_DESCRIPTION = 'All tests for this release';
+var DEFERRED_DESCRIPTION = 'All deferred tests (DEFER)';
+var FIXABLE_COUNT_KEY = 'fixableCount';
+var ALL_FIXABLE_COUNT_KEY = 'allFixableCount';
+var CHROME_REVISIONS_KEY = 'chromeRevision';
+var WEBKIT_REVISIONS_KEY = 'webkitRevision';
+var TIMESTAMPS_KEY = 'secondsSinceEpoch';
+var BUILD_NUMBERS_KEY = 'buildNumbers';
+var TESTS_KEY = 'tests';
+var ONE_DAY_SECONDS = 60 * 60 * 24;
+var ONE_WEEK_SECONDS = ONE_DAY_SECONDS * 7;
+
+// These should match the testtype uploaded to test-results.appspot.com.
+// See http://test-results.appspot.com/testfile.
+var TEST_TYPES = [
+    'base_unittests',
+    'browser_tests',
+    'cacheinvalidation_unittests',
+    'compositor_unittests',
+    'content_browsertests',
+    'content_unittests',
+    'courgette_unittests',
+    'crypto_unittests',
+    'googleurl_unittests',
+    'gfx_unittests',
+    'gl_tests',
+    'gpu_tests',
+    'gpu_unittests',
+    'installer_util_unittests',
+    'interactive_ui_tests',
+    'ipc_tests',
+    'jingle_unittests',
+    'layout-tests',
+    'media_unittests',
+    'mini_installer_test',
+    'net_unittests',
+    'printing_unittests',
+    'remoting_unittests',
+    'safe_browsing_tests',
+    'sql_unittests',
+    'sync_unit_tests',
+    'sync_integration_tests',
+    'test_shell_tests',
+    'ui_tests',
+    'unit_tests',
+    'views_unittests',
+    'webkit_unit_tests',
+];
+
+var RELOAD_REQUIRING_PARAMETERS = ['showAllRuns', 'group', 'testType'];
+
+// Enum for indexing into the run-length encoded results in the JSON files.
+// 0 is where the count is length is stored. 1 is the value.
+var RLE = {
+    LENGTH: 0,
+    VALUE: 1
+}
+
+function isFailingResult(value)
+{
+    return 'FSTOCIZ'.indexOf(value) != -1;
+}
+
+// Takes a key and a value and sets the g_currentState[key] = value iff key is
+// a valid hash parameter and the value is a valid value for that key. Handles
+// cross-dashboard parameters then falls back to calling
+// handleValidHashParameter for dashboard-specific parameters.
+//
+// @return {boolean} Whether the key what inserted into the g_currentState.
+function handleValidHashParameterWrapper(key, value)
+{
+    switch(key) {
+    case 'testType':
+        validateParameter(g_crossDashboardState, key, value,
+            function() { return TEST_TYPES.indexOf(value) != -1; });
+        return true;
+
+    case 'group':
+        validateParameter(g_crossDashboardState, key, value,
+            function() {
+              return value in LAYOUT_TESTS_BUILDER_GROUPS ||
+                  value in CHROMIUM_GPU_TESTS_BUILDER_GROUPS ||
+                  value in CHROMIUM_GTESTS_BUILDER_GROUPS;
+            });
+        return true;
+
+    // FIXME: This should probably be stored on g_crossDashboardState like everything else in this function.
+    case 'builder':
+        validateParameter(g_currentState, key, value,
+            function() { return value in g_builders; });
+        return true;
+
+    case 'useTestData':
+    case 'showAllRuns':
+        g_crossDashboardState[key] = value == 'true';
+        return true;
+
+    case 'buildDir':
+        if (value === 'Debug' || value == 'Release') {
+            g_crossDashboardState['testType'] = 'layout-tests';
+            g_crossDashboardState[key] = value;
+            return true;
+        } else
+            return false;
+
+    default:
+        return handleValidHashParameter(key, value);
+    }
+}
+
+var g_defaultCrossDashboardStateValues = {
+    group: '@ToT - chromium.org',
+    showAllRuns: false,
+    testType: 'layout-tests',
+    buildDir: '',
+    useTestData: false,
+}
+
+// Generic utility functions.
+function $(id)
+{
+    return document.getElementById(id);
+}
+
+function stringContains(a, b)
+{
+    return a.indexOf(b) != -1;
+}
+
+function caseInsensitiveContains(a, b)
+{
+    return a.match(new RegExp(b, 'i'));
+}
+
+function startsWith(a, b)
+{
+    return a.indexOf(b) == 0;
+}
+
+function endsWith(a, b)
+{
+    return a.lastIndexOf(b) == a.length - b.length;
+}
+
+function isValidName(str)
+{
+    return str.match(/[A-Za-z0-9\-\_,]/);
+}
+
+function trimString(str)
+{
+    return str.replace(/^\s+|\s+$/g, '');
+}
+
+function collapseWhitespace(str)
+{
+    return str.replace(/\s+/g, ' ');
+}
+
+function validateParameter(state, key, value, validateFn)
+{
+    if (validateFn())
+        state[key] = value;
+    else
+        console.log(key + ' value is not valid: ' + value);
+}
+
+function queryHashAsMap()
+{
+    var hash = window.location.hash;
+    var paramsList = hash ? hash.substring(1).split('&') : [];
+    var paramsMap = {};
+    var invalidKeys = [];
+    for (var i = 0; i < paramsList.length; i++) {
+        var thisParam = paramsList[i].split('=');
+        if (thisParam.length != 2) {
+            console.log('Invalid query parameter: ' + paramsList[i]);
+            continue;
+        }
+
+        paramsMap[thisParam[0]] = decodeURIComponent(thisParam[1]);
+    }
+
+    // FIXME: remove support for mapping from the master parameter to the group
+    // one once the waterfall starts to pass in the builder name instead.
+    if (paramsMap.master) {
+        paramsMap.group = LEGACY_BUILDER_MASTERS_TO_GROUPS[paramsMap.master];
+        if (!paramsMap.group)
+            console.log('ERROR: Unknown master name: ' + paramsMap.master);
+        window.location.hash = window.location.hash.replace('master=' + paramsMap.master, 'group=' + paramsMap.group);
+        delete paramsMap.master;
+    }
+
+    return paramsMap;
+}
+
+function parseParameter(parameters, key)
+{
+    if (!(key in parameters))
+        return;
+    var value = parameters[key];
+    if (!handleValidHashParameterWrapper(key, value))
+        console.log("Invalid query parameter: " + key + '=' + value);
+}
+
+function parseCrossDashboardParameters()
+{
+    g_crossDashboardState = {};
+    var parameters = queryHashAsMap();
+    for (parameterName in g_defaultCrossDashboardStateValues)
+        parseParameter(parameters, parameterName);
+
+    fillMissingValues(g_crossDashboardState, g_defaultCrossDashboardStateValues);
+    if (currentBuilderGroup() === undefined)
+        g_crossDashboardState.group = g_defaultCrossDashboardStateValues.group;
+}
+
+function parseDashboardSpecificParameters()
+{
+    g_currentState = {};
+    var parameters = queryHashAsMap();
+    for (parameterName in g_defaultDashboardSpecificStateValues)
+        parseParameter(parameters, parameterName);
+}
+
+function parseParameters()
+{
+    var oldCrossDashboardState = g_crossDashboardState;
+    var oldDashboardSpecificState = g_currentState;
+
+    parseCrossDashboardParameters();
+    parseDashboardSpecificParameters();
+    parseParameter(queryHashAsMap(), 'builder');
+
+    var crossDashboardDiffState = diffStates(oldCrossDashboardState, g_crossDashboardState);
+    var dashboardSpecificDiffState = diffStates(oldDashboardSpecificState, g_currentState);
+
+    fillMissingValues(g_currentState, g_defaultDashboardSpecificStateValues);
+    if (!g_crossDashboardState.useTestData)
+        fillMissingValues(g_currentState, {'builder': g_defaultBuilderName});
+
+    // FIXME: dashboard_base shouldn't know anything about specific dashboard specific keys.
+    if (dashboardSpecificDiffState.builder)
+        delete g_currentState.tests;
+    if (g_currentState.tests)
+        delete g_currentState.builder;
+
+    // Some parameters require loading different JSON files when the value changes. Do a reload.
+    if (Object.keys(oldCrossDashboardState).length) {
+        for (var key in g_crossDashboardState) {
+            if (oldCrossDashboardState[key] != g_crossDashboardState[key] && RELOAD_REQUIRING_PARAMETERS.indexOf(key) != -1)
+                window.location.reload();
+        }
+    }
+
+    return dashboardSpecificDiffState;
+}
+
+function diffStates(oldState, newState)
+{
+    // If there is no old state, everything in the current state is new.
+    if (!oldState)
+        return newState;
+
+    var changedParams = {};
+    for (curKey in newState) {
+        var oldVal = oldState[curKey];
+        var newVal = newState[curKey];
+        // Add new keys or changed values.
+        if (!oldVal || oldVal != newVal)
+            changedParams[curKey] = newVal;
+    }
+    return changedParams;
+}
+
+function defaultValue(key)
+{
+    if (key in g_defaultDashboardSpecificStateValues)
+        return g_defaultDashboardSpecificStateValues[key];
+    return g_defaultCrossDashboardStateValues[key];
+}
+
+function fillMissingValues(to, from)
+{
+    for (var state in from) {
+        if (!(state in to))
+            to[state] = from[state];
+    }
+}
+
+// FIXME: Rename this to g_dashboardSpecificState;
+var g_currentState = {};
+var g_crossDashboardState = {};
+parseCrossDashboardParameters();
+
+function isLayoutTestResults()
+{
+    return g_crossDashboardState.testType == 'layout-tests';
+}
+
+function isGPUTestResults()
+{
+    return g_crossDashboardState.testType == 'gpu_tests';
+}
+
+function currentBuilderGroupCategory()
+{
+    switch (g_crossDashboardState.testType) {
+    case 'gl_tests':
+    case 'gpu_tests':
+        return CHROMIUM_GPU_TESTS_BUILDER_GROUPS;
+    case 'layout-tests':
+        return LAYOUT_TESTS_BUILDER_GROUPS;
+    case 'test_shell_tests':
+    case 'webkit_unit_tests':
+        return TEST_SHELL_TESTS_BUILDER_GROUPS;
+    default:
+        return CHROMIUM_GTESTS_BUILDER_GROUPS;
+    }
+}
+
+function currentBuilderGroup()
+{
+    return currentBuilderGroupCategory()[g_crossDashboardState.group]
+}
+
+function builderMaster(builderName)
+{
+    return BUILDER_TO_MASTER[builderName];
+}
+
+function isTipOfTreeWebKitBuilder()
+{
+    return currentBuilderGroup().isToTWebKit;
+}
+
+var g_defaultBuilderName, g_builders;
+function initBuilders()
+{
+    if (g_crossDashboardState.buildDir) {
+        // If buildDir is set, point to the results.json in the local tree. Useful for debugging changes to the python JSON generator.
+        g_defaultBuilderName = 'DUMMY_BUILDER_NAME';
+        g_builders = {'DUMMY_BUILDER_NAME': ''};
+        var loc = document.location.toString();
+        var offset = loc.indexOf('webkit/');
+    } else
+        currentBuilderGroup().setup();
+}
+
+var g_resultsByBuilder = {};
+var g_expectationsByPlatform = {};
+var g_staleBuilders = [];
+var g_buildersThatFailedToLoad = [];
+
+// TODO(aboxhall): figure out whether this is a performance bottleneck and
+// change calling code to understand the trie structure instead if necessary.
+function flattenTrie(trie, prefix)
+{
+    var result = {};
+    for (var name in trie) {
+        var fullName = prefix ? prefix + "/" + name : name;
+        var data = trie[name];
+        if ("results" in data)
+            result[fullName] = data;
+        else {
+            var partialResult = flattenTrie(data, fullName);
+            for (var key in partialResult) {
+                result[key] = partialResult[key];
+            }
+        }
+    }
+    return result;
+}
+
+function isTreeMap()
+{
+    return endsWith(window.location.pathname, 'treemap.html');
+}
+
+function isFlakinessDashboard()
+{
+    return endsWith(window.location.pathname, 'flakiness_dashboard.html');
+}
+
+var g_hasDoneInitialPageGeneration = false;
+// String of error messages to display to the user.
+var g_errorMessages = '';
+
+// Record a new error message.
+// @param {string} errorMsg The message to show to the user.
+function addError(errorMsg)
+{
+    g_errorMessages += errorMsg + '<br>';
+}
+
+// Clear out error and warning messages.
+function clearErrors()
+{
+    g_errorMessages = '';
+}
+
+// If there are errors, show big and red UI for errors so as to be noticed.
+function showErrors()
+{
+    var errors = $('errors');
+
+    if (!g_errorMessages) {
+        if (errors)
+            errors.parentNode.removeChild(errors);
+        return;
+    }
+
+    if (!errors) {
+        errors = document.createElement('H2');
+        errors.style.color = 'red';
+        errors.id = 'errors';
+        document.body.appendChild(errors);
+    }
+
+    errors.innerHTML = g_errorMessages;
+}
+
+function addBuilderLoadErrors()
+{
+    if (g_hasDoneInitialPageGeneration)
+        return;
+
+    if (g_buildersThatFailedToLoad.length)
+        addError('ERROR: Failed to get data from ' + g_buildersThatFailedToLoad.toString() + '.');
+
+    if (g_staleBuilders.length)
+        addError('ERROR: Data from ' + g_staleBuilders.toString() + ' is more than 1 day stale.');
+}
+
+function resourceLoadingComplete()
+{
+    g_resourceLoader = null;
+    handleLocationChange();
+}
+
+function handleLocationChange()
+{
+    if (g_resourceLoader)
+        return;
+
+    addBuilderLoadErrors();
+    g_hasDoneInitialPageGeneration = true;
+
+    var params = parseParameters();
+    var shouldGeneratePage = true;
+    if (Object.keys(params).length)
+        shouldGeneratePage = handleQueryParameterChange(params);
+
+    var newHash = permaLinkURLHash();
+    var winHash = window.location.hash || "#";
+    // Make sure the location is the same as the state we are using internally.
+    // These get out of sync if processQueryParamChange changed state.
+    if (newHash != winHash) {
+        // This will cause another hashchange, and when we loop
+        // back through here next time, we'll go through generatePage.
+        window.location.hash = newHash;
+    } else if (shouldGeneratePage)
+        generatePage();
+}
+
+window.onhashchange = handleLocationChange;
+
+function combinedDashboardState()
+{
+    var combinedState = Object.create(g_currentState);
+    for (var key in g_crossDashboardState)
+        combinedState[key] = g_crossDashboardState[key];
+    return combinedState;    
+}
+
+// Sets the page state. Takes varargs of key, value pairs.
+function setQueryParameter(var_args)
+{
+    var state = combinedDashboardState();
+    for (var i = 0; i < arguments.length; i += 2) {
+        var key = arguments[i];
+        state[key] = arguments[i + 1];
+    }
+    // Note: We use window.location.hash rather that window.location.replace
+    // because of bugs in Chrome where extra entries were getting created
+    // when back button was pressed and full page navigation was occuring.
+    // FIXME: file those bugs.
+    window.location.hash = permaLinkURLHash(state);
+}
+
+function permaLinkURLHash(opt_state)
+{
+    var state = opt_state || combinedDashboardState();
+    return '#' + joinParameters(state);
+}
+
+function joinParameters(stateObject)
+{
+    var state = [];
+    for (var key in stateObject) {
+        var value = stateObject[key];
+        if (value != defaultValue(key))
+            state.push(key + '=' + encodeURIComponent(value));
+    }
+    return state.join('&');
+}
+
+function logTime(msg, startTime)
+{
+    console.log(msg + ': ' + (Date.now() - startTime));
+}
+
+function hidePopup()
+{
+    var popup = $('popup');
+    if (popup)
+        popup.parentNode.removeChild(popup);
+}
+
+function showPopup(target, html)
+{
+    var popup = $('popup');
+    if (!popup) {
+        popup = document.createElement('div');
+        popup.id = 'popup';
+        document.body.appendChild(popup);
+    }
+
+    // Set html first so that we can get accurate size metrics on the popup.
+    popup.innerHTML = html;
+
+    var targetRect = target.getBoundingClientRect();
+
+    var x = Math.min(targetRect.left - 10, document.documentElement.clientWidth - popup.offsetWidth);
+    x = Math.max(0, x);
+    popup.style.left = x + document.body.scrollLeft + 'px';
+
+    var y = targetRect.top + targetRect.height;
+    if (y + popup.offsetHeight > document.documentElement.clientHeight)
+        y = targetRect.top - popup.offsetHeight;
+    y = Math.max(0, y);
+    popup.style.top = y + document.body.scrollTop + 'px';
+}
+
+// Create a new function with some of its arguements
+// pre-filled.
+// Taken from goog.partial in the Closure library.
+// @param {Function} fn A function to partially apply.
+// @param {...*} var_args Additional arguments that are partially
+//         applied to fn.
+// @return {!Function} A partially-applied form of the function bind() was
+//         invoked as a method of.
+function partial(fn, var_args)
+{
+    var args = Array.prototype.slice.call(arguments, 1);
+    return function() {
+        // Prepend the bound arguments to the current arguments.
+        var newArgs = Array.prototype.slice.call(arguments);
+        newArgs.unshift.apply(newArgs, args);
+        return fn.apply(this, newArgs);
+    };
+};
+
+// Returns the appropriate expectatiosn map for the current testType.
+function expectationsMap()
+{
+    return isLayoutTestResults() ? LAYOUT_TEST_EXPECTATIONS_MAP_ : GTEST_EXPECTATIONS_MAP_;
+}
+
+function toggleQueryParameter(param)
+{
+    setQueryParameter(param, !queryParameterValue(param));
+}
+
+function queryParameterValue(parameter)
+{
+    return g_currentState[parameter] || g_crossDashboardState[parameter];
+}
+
+function checkboxHTML(queryParameter, label, isChecked, opt_extraJavaScript)
+{
+    var js = opt_extraJavaScript || '';
+    return '<label style="padding-left: 2em">' +
+        '<input type="checkbox" onchange="toggleQueryParameter(\'' + queryParameter + '\');' + js + '" ' +
+            (isChecked ? 'checked' : '') + '>' + label +
+        '</label> ';
+}
+
+function selectHTML(label, queryParameter, options)
+{
+    var html = '<label style="padding-left: 2em">' + label + ': ' +
+        '<select onchange="setQueryParameter(\'' + queryParameter + '\', this[this.selectedIndex].value)">';
+
+    for (var i = 0; i < options.length; i++) {
+        var value = options[i];
+        html += '<option value="' + value + '" ' +
+            (queryParameterValue(queryParameter) == value ? 'selected' : '') +
+            '>' + value + '</option>'
+    }
+    html += '</select></label> ';
+    return html;
+}
+
+// Returns the HTML for the select element to switch to different testTypes.
+function htmlForTestTypeSwitcher(opt_noBuilderMenu, opt_extraHtml, opt_includeNoneBuilder)
+{
+    var html = '<div style="border-bottom:1px dashed">';
+    html += '' +
+        htmlForDashboardLink('Stats', 'aggregate_results.html') +
+        htmlForDashboardLink('Timeline', 'timeline_explorer.html') +
+        htmlForDashboardLink('Results', 'flakiness_dashboard.html') +
+        htmlForDashboardLink('Treemap', 'treemap.html');
+
+    html += selectHTML('Test type', 'testType', TEST_TYPES);
+
+    if (!opt_noBuilderMenu) {
+        var buildersForMenu = Object.keys(g_builders);
+        if (opt_includeNoneBuilder)
+            buildersForMenu.unshift('--------------');
+        html += selectHTML('Builder', 'builder', buildersForMenu);
+    }
+
+    html += selectHTML('Group', 'group',
+        Object.keys(currentBuilderGroupCategory()));
+
+    if (!isTreeMap())
+        html += checkboxHTML('showAllRuns', 'Show all runs', g_crossDashboardState.showAllRuns);
+
+    if (opt_extraHtml)
+        html += opt_extraHtml;
+    return html + '</div>';
+}
+
+function selectBuilder(builder)
+{
+    setQueryParameter('builder', builder);
+}
+
+function loadDashboard(fileName)
+{
+    var pathName = window.location.pathname;
+    pathName = pathName.substring(0, pathName.lastIndexOf('/') + 1);
+    window.location = pathName + fileName + window.location.hash;
+}
+
+function htmlForTopLink(html, onClick, isSelected)
+{
+    var cssText = isSelected ? 'font-weight: bold;' : 'color:blue;text-decoration:underline;cursor:pointer;';
+    cssText += 'margin: 0 5px;';
+    return '<span style="' + cssText + '" onclick="' + onClick + '">' + html + '</span>';
+}
+
+function htmlForDashboardLink(html, fileName)
+{
+    var pathName = window.location.pathname;
+    var currentFileName = pathName.substring(pathName.lastIndexOf('/') + 1);
+    var isSelected = currentFileName == fileName;
+    var onClick = 'loadDashboard(\'' + fileName + '\')';
+    return htmlForTopLink(html, onClick, isSelected);
+}
+
+function revisionLink(results, index, key, singleUrlTemplate, rangeUrlTemplate)
+{
+    var currentRevision = parseInt(results[key][index], 10);
+    var previousRevision = parseInt(results[key][index + 1], 10);
+
+    function singleUrl()
+    {
+        return singleUrlTemplate.replace('<rev>', currentRevision);
+    }
+
+    function rangeUrl()
+    {
+        return rangeUrlTemplate.replace('<rev1>', currentRevision).replace('<rev2>', previousRevision + 1);
+    }
+
+    if (currentRevision == previousRevision)
+        return 'At <a href="' + singleUrl() + '">r' + currentRevision    + '</a>';
+    else if (currentRevision - previousRevision == 1)
+        return '<a href="' + singleUrl() + '">r' + currentRevision    + '</a>';
+    else
+        return '<a href="' + rangeUrl() + '">r' + (previousRevision + 1) + ' to r' + currentRevision + '</a>';
+}
+
+function chromiumRevisionLink(results, index)
+{
+    return revisionLink(
+        results,
+        index,
+        CHROME_REVISIONS_KEY,
+        'http://src.chromium.org/viewvc/chrome?view=rev&revision=<rev>',
+        'http://build.chromium.org/f/chromium/perf/dashboard/ui/changelog.html?url=/trunk/src&range=<rev2>:<rev1>&mode=html');
+}
+
+function webKitRevisionLink(results, index)
+{
+    return revisionLink(
+        results,
+        index,
+        WEBKIT_REVISIONS_KEY,
+        'http://trac.webkit.org/changeset/<rev>',
+        'http://trac.webkit.org/log/trunk/?rev=<rev1>&stop_rev=<rev2>&limit=100&verbose=on');
+}
+
+// "Decompresses" the RLE-encoding of test results so that we can query it
+// by build index and test name.
+//
+// @param {Object} results results for the current builder
+// @return Object with these properties:
+//     - testNames: array mapping test index to test names.
+//     - resultsByBuild: array of builds, for each build a (sparse) array of test results by test index.
+//     - flakyTests: array with the boolean value true at test indices that are considered flaky (more than one single-build failure).
+//     - flakyDeltasByBuild: array of builds, for each build a count of flaky test results by expectation, as well as a total.
+function decompressResults(builderResults)
+{
+    var builderTestResults = builderResults[TESTS_KEY];
+    var buildCount = builderResults[FIXABLE_COUNTS_KEY].length;
+    var resultsByBuild = new Array(buildCount);
+    var flakyDeltasByBuild = new Array(buildCount);
+
+    // Pre-sizing the test result arrays for each build saves us ~250ms
+    var testCount = 0;
+    for (var testName in builderTestResults)
+        testCount++;
+    for (var i = 0; i < buildCount; i++) {
+        resultsByBuild[i] = new Array(testCount);
+        resultsByBuild[i][testCount - 1] = undefined;
+        flakyDeltasByBuild[i] = {};
+    }
+
+    // Using indices instead of the full test names for each build saves us
+    // ~1500ms
+    var testIndex = 0;
+    var testNames = new Array(testCount);
+    var flakyTests = new Array(testCount);
+
+    // Decompress and "invert" test results (by build instead of by test) and
+    // determine which are flaky.
+    for (var testName in builderTestResults) {
+        var oneBuildFailureCount = 0;
+
+        testNames[testIndex] = testName;
+        var testResults = builderTestResults[testName].results;
+        for (var i = 0, rleResult, currentBuildIndex = 0; (rleResult = testResults[i]) && currentBuildIndex < buildCount; i++) {
+            var count = rleResult[RLE.LENGTH];
+            var value = rleResult[RLE.VALUE];
+
+            if (count == 1 && value in FAILURE_EXPECTATIONS_)
+                oneBuildFailureCount++;
+
+            for (var j = 0; j < count; j++) {
+                resultsByBuild[currentBuildIndex++][testIndex] = value;
+                if (currentBuildIndex == buildCount)
+                    break;
+            }
+        }
+
+        if (oneBuildFailureCount > 2)
+            flakyTests[testIndex] = true;
+
+        testIndex++;
+    }
+
+    // Now that we know which tests are flaky, count the test results that are
+    // from flaky tests for each build.
+    testIndex = 0;
+    for (var testName in builderTestResults) {
+        if (!flakyTests[testIndex++])
+            continue;
+
+        var testResults = builderTestResults[testName].results;
+        for (var i = 0, rleResult, currentBuildIndex = 0; (rleResult = testResults[i]) && currentBuildIndex < buildCount; i++) {
+            var count = rleResult[RLE.LENGTH];
+            var value = rleResult[RLE.VALUE];
+
+            for (var j = 0; j < count; j++) {
+                var buildTestResults = flakyDeltasByBuild[currentBuildIndex++];
+                function addFlakyDelta(key)
+                {
+                    if (!(key in buildTestResults))
+                        buildTestResults[key] = 0;
+                    buildTestResults[key]++;
+                }
+                addFlakyDelta(value);
+                if (value != 'P' && value != 'N')
+                    addFlakyDelta('total');
+                if (currentBuildIndex == buildCount)
+                    break;
+            }
+        }
+    }
+
+    return {
+        testNames: testNames,
+        resultsByBuild: resultsByBuild,
+        flakyTests: flakyTests,
+        flakyDeltasByBuild: flakyDeltasByBuild
+    };
+}
+
+document.addEventListener('mousedown', function(e) {
+    // Clear the open popup, unless the click was inside the popup.
+    var popup = $('popup');
+    if (popup && e.target != popup && !(popup.compareDocumentPosition(e.target) & 16))
+        hidePopup();
+}, false);
+
+window.addEventListener('load', function() {
+    // This doesn't seem totally accurate as there is a race between
+    // onload firing and the last script tag being executed.
+    logTime('Time to load JS', g_pageLoadStartTime);
+    g_resourceLoader = new loader.Loader();
+    g_resourceLoader.load();
+}, false);
diff --git a/Tools/TestResultServer/static-dashboards/dygraph-combined.js b/Tools/TestResultServer/static-dashboards/dygraph-combined.js
new file mode 100644
index 0000000..6a88059
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/dygraph-combined.js
@@ -0,0 +1 @@
+DygraphLayout=function(b,a){this.dygraph_=b;this.options={};Dygraph.update(this.options,a?a:{});this.datasets=new Array();this.annotations=new Array()};DygraphLayout.prototype.attr_=function(a){return this.dygraph_.attr_(a)};DygraphLayout.prototype.addDataset=function(a,b){this.datasets[a]=b};DygraphLayout.prototype.setAnnotations=function(d){var e=this.attr_("xValueParser");for(var c=0;c<d.length;c++){var b={};if(!d[c].xval&&!d[c].x){this.dygraph_.error("Annotations must have an 'x' property");return}if(d[c].icon&&!(d[c].hasOwnProperty("width")&&d[c].hasOwnProperty("height"))){this.dygraph_.error("Must set width and height when setting annotation.icon property");return}Dygraph.update(b,d[c]);if(!b.xval){b.xval=e(b.x)}this.annotations.push(b)}};DygraphLayout.prototype.evaluate=function(){this._evaluateLimits();this._evaluateLineCharts();this._evaluateLineTicks();this._evaluateAnnotations()};DygraphLayout.prototype._evaluateLimits=function(){this.minxval=this.maxxval=null;if(this.options.dateWindow){this.minxval=this.options.dateWindow[0];this.maxxval=this.options.dateWindow[1]}else{for(var c in this.datasets){if(!this.datasets.hasOwnProperty(c)){continue}var d=this.datasets[c];var b=d[0][0];if(!this.minxval||b<this.minxval){this.minxval=b}var a=d[d.length-1][0];if(!this.maxxval||a>this.maxxval){this.maxxval=a}}}this.xrange=this.maxxval-this.minxval;this.xscale=(this.xrange!=0?1/this.xrange:1);this.minyval=this.options.yAxis[0];this.maxyval=this.options.yAxis[1];this.yrange=this.maxyval-this.minyval;this.yscale=(this.yrange!=0?1/this.yrange:1)};DygraphLayout.prototype._evaluateLineCharts=function(){this.points=new Array();for(var e in this.datasets){if(!this.datasets.hasOwnProperty(e)){continue}var d=this.datasets[e];for(var b=0;b<d.length;b++){var c=d[b];var a={x:((parseFloat(c[0])-this.minxval)*this.xscale),y:1-((parseFloat(c[1])-this.minyval)*this.yscale),xval:parseFloat(c[0]),yval:parseFloat(c[1]),name:e};if(a.y<=0){a.y=0}if(a.y>=1){a.y=1}this.points.push(a)}}};DygraphLayout.prototype._evaluateLineTicks=function(){this.xticks=new Array();for(var c=0;c<this.options.xTicks.length;c++){var b=this.options.xTicks[c];var a=b.label;var d=this.xscale*(b.v-this.minxval);if((d>=0)&&(d<=1)){this.xticks.push([d,a])}}this.yticks=new Array();for(var c=0;c<this.options.yTicks.length;c++){var b=this.options.yTicks[c];var a=b.label;var d=1-(this.yscale*(b.v-this.minyval));if((d>=0)&&(d<=1)){this.yticks.push([d,a])}}};DygraphLayout.prototype.evaluateWithError=function(){this.evaluate();if(!this.options.errorBars){return}var d=0;for(var g in this.datasets){if(!this.datasets.hasOwnProperty(g)){continue}var c=0;var f=this.datasets[g];for(var c=0;c<f.length;c++,d++){var e=f[c];var a=parseFloat(e[0]);var b=parseFloat(e[1]);if(a==this.points[d].xval&&b==this.points[d].yval){this.points[d].errorMinus=parseFloat(e[2]);this.points[d].errorPlus=parseFloat(e[3])}}}};DygraphLayout.prototype._evaluateAnnotations=function(){var f={};for(var d=0;d<this.annotations.length;d++){var b=this.annotations[d];f[b.xval+","+b.series]=b}this.annotated_points=[];for(var d=0;d<this.points.length;d++){var e=this.points[d];var c=e.xval+","+e.name;if(c in f){e.annotation=f[c];this.annotated_points.push(e)}}};DygraphLayout.prototype.removeAllDatasets=function(){delete this.datasets;this.datasets=new Array()};DygraphLayout.prototype.updateOptions=function(a){Dygraph.update(this.options,a?a:{})};DygraphCanvasRenderer=function(c,b,d,a){this.dygraph_=c;this.options={strokeWidth:0.5,drawXAxis:true,drawYAxis:true,axisLineColor:"black",axisLineWidth:0.5,axisTickSize:3,axisLabelColor:"black",axisLabelFont:"Arial",axisLabelFontSize:9,axisLabelWidth:50,drawYGrid:true,drawXGrid:true,gridLineColor:"rgb(128,128,128)",fillAlpha:0.15,underlayCallback:null};Dygraph.update(this.options,a);this.layout=d;this.element=b;this.container=this.element.parentNode;this.height=this.element.height;this.width=this.element.width;if(!this.isIE&&!(DygraphCanvasRenderer.isSupported(this.element))){throw"Canvas is not supported."}this.xlabels=new Array();this.ylabels=new Array();this.annotations=new Array();this.area={x:this.options.yAxisLabelWidth+2*this.options.axisTickSize,y:0};this.area.w=this.width-this.area.x-this.options.rightGap;this.area.h=this.height-this.options.axisLabelFontSize-2*this.options.axisTickSize;this.container.style.position="relative";this.container.style.width=this.width+"px"};DygraphCanvasRenderer.prototype.clear=function(){if(this.isIE){try{if(this.clearDelay){this.clearDelay.cancel();this.clearDelay=null}var b=this.element.getContext("2d")}catch(d){this.clearDelay=MochiKit.Async.wait(this.IEDelay);this.clearDelay.addCallback(bind(this.clear,this));return}}var b=this.element.getContext("2d");b.clearRect(0,0,this.width,this.height);for(var a=0;a<this.xlabels.length;a++){var c=this.xlabels[a];c.parentNode.removeChild(c)}for(var a=0;a<this.ylabels.length;a++){var c=this.ylabels[a];c.parentNode.removeChild(c)}for(var a=0;a<this.annotations.length;a++){var c=this.annotations[a];c.parentNode.removeChild(c)}this.xlabels=new Array();this.ylabels=new Array();this.annotations=new Array()};DygraphCanvasRenderer.isSupported=function(g){var b=null;try{if(typeof(g)=="undefined"||g==null){b=document.createElement("canvas")}else{b=g}var c=b.getContext("2d")}catch(d){var f=navigator.appVersion.match(/MSIE (\d\.\d)/);var a=(navigator.userAgent.toLowerCase().indexOf("opera")!=-1);if((!f)||(f[1]<6)||(a)){return false}return true}return true};DygraphCanvasRenderer.prototype.render=function(){var b=this.element.getContext("2d");if(this.options.underlayCallback){this.options.underlayCallback(b,this.area,this.layout,this.dygraph_)}if(this.options.drawYGrid){var d=this.layout.yticks;b.save();b.strokeStyle=this.options.gridLineColor;b.lineWidth=this.options.axisLineWidth;for(var c=0;c<d.length;c++){var a=this.area.x;var e=this.area.y+d[c][0]*this.area.h;b.beginPath();b.moveTo(a,e);b.lineTo(a+this.area.w,e);b.closePath();b.stroke()}}if(this.options.drawXGrid){var d=this.layout.xticks;b.save();b.strokeStyle=this.options.gridLineColor;b.lineWidth=this.options.axisLineWidth;for(var c=0;c<d.length;c++){var a=this.area.x+d[c][0]*this.area.w;var e=this.area.y+this.area.h;b.beginPath();b.moveTo(a,e);b.lineTo(a,this.area.y);b.closePath();b.stroke()}}this._renderLineChart();this._renderAxis();this._renderAnnotations()};DygraphCanvasRenderer.prototype._renderAxis=function(){if(!this.options.drawXAxis&&!this.options.drawYAxis){return}var b=this.element.getContext("2d");var g={position:"absolute",fontSize:this.options.axisLabelFontSize+"px",zIndex:10,color:this.options.axisLabelColor,width:this.options.axisLabelWidth+"px",overflow:"hidden"};var d=function(o){var q=document.createElement("div");for(var p in g){if(g.hasOwnProperty(p)){q.style[p]=g[p]}}q.appendChild(document.createTextNode(o));return q};b.save();b.strokeStyle=this.options.axisLineColor;b.lineWidth=this.options.axisLineWidth;if(this.options.drawYAxis){if(this.layout.yticks&&this.layout.yticks.length>0){for(var e=0;e<this.layout.yticks.length;e++){var f=this.layout.yticks[e];if(typeof(f)=="function"){return}var l=this.area.x;var j=this.area.y+f[0]*this.area.h;b.beginPath();b.moveTo(l,j);b.lineTo(l-this.options.axisTickSize,j);b.closePath();b.stroke();var k=d(f[1]);var h=(j-this.options.axisLabelFontSize/2);if(h<0){h=0}if(h+this.options.axisLabelFontSize+3>this.height){k.style.bottom="0px"}else{k.style.top=h+"px"}k.style.left="0px";k.style.textAlign="right";k.style.width=this.options.yAxisLabelWidth+"px";this.container.appendChild(k);this.ylabels.push(k)}var m=this.ylabels[0];var n=this.options.axisLabelFontSize;var a=parseInt(m.style.top)+n;if(a>this.height-n){m.style.top=(parseInt(m.style.top)-n/2)+"px"}}b.beginPath();b.moveTo(this.area.x,this.area.y);b.lineTo(this.area.x,this.area.y+this.area.h);b.closePath();b.stroke()}if(this.options.drawXAxis){if(this.layout.xticks){for(var e=0;e<this.layout.xticks.length;e++){var f=this.layout.xticks[e];if(typeof(dataset)=="function"){return}var l=this.area.x+f[0]*this.area.w;var j=this.area.y+this.area.h;b.beginPath();b.moveTo(l,j);b.lineTo(l,j+this.options.axisTickSize);b.closePath();b.stroke();var k=d(f[1]);k.style.textAlign="center";k.style.bottom="0px";var c=(l-this.options.axisLabelWidth/2);if(c+this.options.axisLabelWidth>this.width){c=this.width-this.options.xAxisLabelWidth;k.style.textAlign="right"}if(c<0){c=0;k.style.textAlign="left"}k.style.left=c+"px";k.style.width=this.options.xAxisLabelWidth+"px";this.container.appendChild(k);this.xlabels.push(k)}}b.beginPath();b.moveTo(this.area.x,this.area.y+this.area.h);b.lineTo(this.area.x+this.area.w,this.area.y+this.area.h);b.closePath();b.stroke()}b.restore()};DygraphCanvasRenderer.prototype._renderAnnotations=function(){var h={position:"absolute",fontSize:this.options.axisLabelFontSize+"px",zIndex:10,overflow:"hidden"};var j=function(q,r,s,a){return function(t){var p=s.annotation;if(p.hasOwnProperty(q)){p[q](p,s,a.dygraph_,t)}else{if(a.dygraph_.attr_(r)){a.dygraph_.attr_(r)(p,s,a.dygraph_,t)}}}};var m=this.layout.annotated_points;for(var g=0;g<m.length;g++){var e=m[g];if(e.canvasx<this.area.x||e.canvasx>this.area.x+this.area.w){continue}var k=e.annotation;var l=6;if(k.hasOwnProperty("tickHeight")){l=k.tickHeight}var c=document.createElement("div");for(var b in h){if(h.hasOwnProperty(b)){c.style[b]=h[b]}}if(!k.hasOwnProperty("icon")){c.className="dygraphDefaultAnnotation"}if(k.hasOwnProperty("cssClass")){c.className+=" "+k.cssClass}var d=k.hasOwnProperty("width")?k.width:16;var n=k.hasOwnProperty("height")?k.height:16;if(k.hasOwnProperty("icon")){var f=document.createElement("img");f.src=k.icon;f.width=d;f.height=n;c.appendChild(f)}else{if(e.annotation.hasOwnProperty("shortText")){c.appendChild(document.createTextNode(e.annotation.shortText))}}c.style.left=(e.canvasx-d/2)+"px";if(k.attachAtBottom){c.style.top=(this.area.h-n-l)+"px"}else{c.style.top=(e.canvasy-n-l)+"px"}c.style.width=d+"px";c.style.height=n+"px";c.title=e.annotation.text;c.style.color=this.colors[e.name];c.style.borderColor=this.colors[e.name];k.div=c;Dygraph.addEvent(c,"click",j("clickHandler","annotationClickHandler",e,this));Dygraph.addEvent(c,"mouseover",j("mouseOverHandler","annotationMouseOverHandler",e,this));Dygraph.addEvent(c,"mouseout",j("mouseOutHandler","annotationMouseOutHandler",e,this));Dygraph.addEvent(c,"dblclick",j("dblClickHandler","annotationDblClickHandler",e,this));this.container.appendChild(c);this.annotations.push(c);var o=this.element.getContext("2d");o.strokeStyle=this.colors[e.name];o.beginPath();if(!k.attachAtBottom){o.moveTo(e.canvasx,e.canvasy);o.lineTo(e.canvasx,e.canvasy-2-l)}else{o.moveTo(e.canvasx,this.area.h);o.lineTo(e.canvasx,this.area.h-2-l)}o.closePath();o.stroke()}};DygraphCanvasRenderer.prototype._renderLineChart=function(){var c=this.element.getContext("2d");var f=this.options.colorScheme.length;var o=this.options.colorScheme;var z=this.options.fillAlpha;var E=this.layout.options.errorBars;var t=this.layout.options.fillGraph;var d=this.layout.options.stackedGraph;var l=this.layout.options.stepPlot;var G=[];for(var H in this.layout.datasets){if(this.layout.datasets.hasOwnProperty(H)){G.push(H)}}var A=G.length;this.colors={};for(var C=0;C<A;C++){this.colors[G[C]]=o[C%f]}for(var C=0;C<this.layout.points.length;C++){var v=this.layout.points[C];v.canvasx=this.area.w*v.x+this.area.x;v.canvasy=this.area.h*v.y+this.area.y}var p=function(j){return j&&!isNaN(j)};var u=c;if(E){if(t){this.dygraph_.warn("Can't use fillGraph option with error bars")}for(var C=0;C<A;C++){var k=G[C];var x=this.colors[k];u.save();var h=NaN;var e=NaN;var g=[-1,-1];var D=this.layout.yscale;var a=new RGBColor(x);var F="rgba("+a.r+","+a.g+","+a.b+","+z+")";u.fillStyle=F;u.beginPath();for(var y=0;y<this.layout.points.length;y++){var v=this.layout.points[y];if(v.name==k){if(!p(v.y)){h=NaN;continue}if(l){var r=[e-v.errorPlus*D,e+v.errorMinus*D];e=v.y}else{var r=[v.y-v.errorPlus*D,v.y+v.errorMinus*D]}r[0]=this.area.h*r[0]+this.area.y;r[1]=this.area.h*r[1]+this.area.y;if(!isNaN(h)){if(l){u.moveTo(h,r[0])}else{u.moveTo(h,g[0])}u.lineTo(v.canvasx,r[0]);u.lineTo(v.canvasx,r[1]);if(l){u.lineTo(h,r[1])}else{u.lineTo(h,g[1])}u.closePath()}g=r;h=v.canvasx}}u.fill()}}else{if(t){var b=1+this.layout.minyval*this.layout.yscale;if(b<0){b=0}else{if(b>1){b=1}}b=this.area.h*b+this.area.y;var q=[];for(var C=A-1;C>=0;C--){var k=G[C];var x=this.colors[k];u.save();var h=NaN;var g=[-1,-1];var D=this.layout.yscale;var a=new RGBColor(x);var F="rgba("+a.r+","+a.g+","+a.b+","+z+")";u.fillStyle=F;u.beginPath();for(var y=0;y<this.layout.points.length;y++){var v=this.layout.points[y];if(v.name==k){if(!p(v.y)){h=NaN;continue}var r;if(d){lastY=q[v.canvasx];if(lastY===undefined){lastY=b}q[v.canvasx]=v.canvasy;r=[v.canvasy,lastY]}else{r=[v.canvasy,b]}if(!isNaN(h)){u.moveTo(h,g[0]);if(l){u.lineTo(v.canvasx,g[0])}else{u.lineTo(v.canvasx,r[0])}u.lineTo(v.canvasx,r[1]);u.lineTo(h,g[1]);u.closePath()}g=r;h=v.canvasx}}u.fill()}}}for(var C=0;C<A;C++){var k=G[C];var x=this.colors[k];var s=this.dygraph_.attr_("strokeWidth",k);c.save();var v=this.layout.points[0];var m=this.dygraph_.attr_("pointSize",k);var h=null,e=null;var w=this.dygraph_.attr_("drawPoints",k);var B=this.layout.points;for(var y=0;y<B.length;y++){var v=B[y];if(v.name==k){if(!p(v.canvasy)){h=e=null}else{var n=(!h&&(y==B.length-1||!p(B[y+1].canvasy)));if(!h){h=v.canvasx;e=v.canvasy}else{if(s){u.beginPath();u.strokeStyle=x;u.lineWidth=s;u.moveTo(h,e);if(l){u.lineTo(v.canvasx,e)}h=v.canvasx;e=v.canvasy;u.lineTo(h,e);u.stroke()}}if(w||n){u.beginPath();u.fillStyle=x;u.arc(v.canvasx,v.canvasy,m,0,2*Math.PI,false);u.fill()}}}}}c.restore()};Dygraph=function(c,b,a){if(arguments.length>0){if(arguments.length==4){this.warn("Using deprecated four-argument dygraph constructor");this.__old_init__(c,b,arguments[2],arguments[3])}else{this.__init__(c,b,a)}}};Dygraph.NAME="Dygraph";Dygraph.VERSION="1.2";Dygraph.__repr__=function(){return"["+this.NAME+" "+this.VERSION+"]"};Dygraph.toString=function(){return this.__repr__()};Dygraph.DEFAULT_ROLL_PERIOD=1;Dygraph.DEFAULT_WIDTH=480;Dygraph.DEFAULT_HEIGHT=320;Dygraph.AXIS_LINE_WIDTH=0.3;Dygraph.DEFAULT_ATTRS={highlightCircleSize:3,pixelsPerXLabel:60,pixelsPerYLabel:30,labelsDivWidth:250,labelsDivStyles:{},labelsSeparateLines:false,labelsShowZeroValues:true,labelsKMB:false,labelsKMG2:false,showLabelsOnHighlight:true,yValueFormatter:function(a){return Dygraph.round_(a,2)},strokeWidth:1,axisTickSize:3,axisLabelFontSize:14,xAxisLabelWidth:50,yAxisLabelWidth:50,xAxisLabelFormatter:Dygraph.dateAxisFormatter,rightGap:5,showRoller:false,xValueFormatter:Dygraph.dateString_,xValueParser:Dygraph.dateParser,xTicker:Dygraph.dateTicker,delimiter:",",logScale:false,sigma:2,errorBars:false,fractions:false,wilsonInterval:true,customBars:false,fillGraph:false,fillAlpha:0.15,connectSeparatedPoints:false,stackedGraph:false,hideOverlayOnMouseOut:true,stepPlot:false};Dygraph.DEBUG=1;Dygraph.INFO=2;Dygraph.WARNING=3;Dygraph.ERROR=3;Dygraph.addedAnnotationCSS=false;Dygraph.prototype.__old_init__=function(f,d,e,b){if(e!=null){var a=["Date"];for(var c=0;c<e.length;c++){a.push(e[c])}Dygraph.update(b,{labels:a})}this.__init__(f,d,b)};Dygraph.prototype.__init__=function(c,b,a){if(a==null){a={}}this.maindiv_=c;this.file_=b;this.rollPeriod_=a.rollPeriod||Dygraph.DEFAULT_ROLL_PERIOD;this.previousVerticalX_=-1;this.fractions_=a.fractions||false;this.dateWindow_=a.dateWindow||null;this.valueRange_=a.valueRange||null;this.wilsonInterval_=a.wilsonInterval||true;this.is_initial_draw_=true;this.annotations_=[];c.innerHTML="";if(c.style.width==""){c.style.width=a.width||Dygraph.DEFAULT_WIDTH+"px"}if(c.style.height==""){c.style.height=a.height||Dygraph.DEFAULT_HEIGHT+"px"}this.width_=parseInt(c.style.width,10);this.height_=parseInt(c.style.height,10);if(c.style.width.indexOf("%")==c.style.width.length-1){this.width_=c.offsetWidth}if(c.style.height.indexOf("%")==c.style.height.length-1){this.height_=c.offsetHeight}if(this.width_==0){this.error("dygraph has zero width. Please specify a width in pixels.")}if(this.height_==0){this.error("dygraph has zero height. Please specify a height in pixels.")}if(a.stackedGraph){a.fillGraph=true}this.user_attrs_={};Dygraph.update(this.user_attrs_,a);this.attrs_={};Dygraph.update(this.attrs_,Dygraph.DEFAULT_ATTRS);this.boundaryIds_=[];this.labelsFromCSV_=(this.attr_("labels")==null);Dygraph.addAnnotationRule();this.createInterface_();this.start_()};Dygraph.prototype.attr_=function(b,a){if(a&&typeof(this.user_attrs_[a])!="undefined"&&this.user_attrs_[a]!=null&&typeof(this.user_attrs_[a][b])!="undefined"){return this.user_attrs_[a][b]}else{if(typeof(this.user_attrs_[b])!="undefined"){return this.user_attrs_[b]}else{if(typeof(this.attrs_[b])!="undefined"){return this.attrs_[b]}else{return null}}}};Dygraph.prototype.log=function(a,b){if(typeof(console)!="undefined"){switch(a){case Dygraph.DEBUG:console.debug("dygraphs: "+b);break;case Dygraph.INFO:console.info("dygraphs: "+b);break;case Dygraph.WARNING:console.warn("dygraphs: "+b);break;case Dygraph.ERROR:console.error("dygraphs: "+b);break}}};Dygraph.prototype.info=function(a){this.log(Dygraph.INFO,a)};Dygraph.prototype.warn=function(a){this.log(Dygraph.WARNING,a)};Dygraph.prototype.error=function(a){this.log(Dygraph.ERROR,a)};Dygraph.prototype.rollPeriod=function(){return this.rollPeriod_};Dygraph.prototype.xAxisRange=function(){if(this.dateWindow_){return this.dateWindow_}var b=this.rawData_[0][0];var a=this.rawData_[this.rawData_.length-1][0];return[b,a]};Dygraph.prototype.yAxisRange=function(){return this.displayedYRange_};Dygraph.prototype.toDomCoords=function(b,f){var c=[null,null];var d=this.plotter_.area;if(b!==null){var a=this.xAxisRange();c[0]=d.x+(b-a[0])/(a[1]-a[0])*d.w}if(f!==null){var e=this.yAxisRange();c[1]=d.y+(e[1]-f)/(e[1]-e[0])*d.h}return c};Dygraph.prototype.toDataCoords=function(b,f){var c=[null,null];var d=this.plotter_.area;if(b!==null){var a=this.xAxisRange();c[0]=a[0]+(b-d.x)/d.w*(a[1]-a[0])}if(f!==null){var e=this.yAxisRange();c[1]=e[0]+(d.h-f)/d.h*(e[1]-e[0])}return c};Dygraph.prototype.numColumns=function(){return this.rawData_[0].length};Dygraph.prototype.numRows=function(){return this.rawData_.length};Dygraph.prototype.getValue=function(b,a){if(b<0||b>this.rawData_.length){return null}if(a<0||a>this.rawData_[b].length){return null}return this.rawData_[b][a]};Dygraph.addEvent=function(c,a,b){var d=function(f){if(!f){var f=window.event}b(f)};if(window.addEventListener){c.addEventListener(a,d,false)}else{c.attachEvent("on"+a,d)}};Dygraph.clipCanvas_=function(b,c){var a=b.getContext("2d");a.beginPath();a.rect(c.left,c.top,c.width,c.height);a.clip()};Dygraph.prototype.createInterface_=function(){var a=this.maindiv_;this.graphDiv=document.createElement("div");this.graphDiv.style.width=this.width_+"px";this.graphDiv.style.height=this.height_+"px";a.appendChild(this.graphDiv);var c={top:0,left:this.attr_("yAxisLabelWidth")+2*this.attr_("axisTickSize")};c.width=this.width_-c.left-this.attr_("rightGap");c.height=this.height_-this.attr_("axisLabelFontSize")-2*this.attr_("axisTickSize");this.clippingArea_=c;this.canvas_=Dygraph.createCanvas();this.canvas_.style.position="absolute";this.canvas_.width=this.width_;this.canvas_.height=this.height_;this.canvas_.style.width=this.width_+"px";this.canvas_.style.height=this.height_+"px";this.hidden_=this.createPlotKitCanvas_(this.canvas_);this.graphDiv.appendChild(this.hidden_);this.graphDiv.appendChild(this.canvas_);this.mouseEventElement_=this.canvas_;Dygraph.clipCanvas_(this.hidden_,this.clippingArea_);Dygraph.clipCanvas_(this.canvas_,this.clippingArea_);var b=this;Dygraph.addEvent(this.mouseEventElement_,"mousemove",function(d){b.mouseMove_(d)});Dygraph.addEvent(this.mouseEventElement_,"mouseout",function(d){b.mouseOut_(d)});this.layoutOptions_={xOriginIsZero:false};Dygraph.update(this.layoutOptions_,this.attrs_);Dygraph.update(this.layoutOptions_,this.user_attrs_);Dygraph.update(this.layoutOptions_,{errorBars:(this.attr_("errorBars")||this.attr_("customBars"))});this.layout_=new DygraphLayout(this,this.layoutOptions_);this.renderOptions_={colorScheme:this.colors_,strokeColor:null,axisLineWidth:Dygraph.AXIS_LINE_WIDTH};Dygraph.update(this.renderOptions_,this.attrs_);Dygraph.update(this.renderOptions_,this.user_attrs_);this.plotter_=new DygraphCanvasRenderer(this,this.hidden_,this.layout_,this.renderOptions_);this.createStatusMessage_();this.createRollInterface_();this.createDragInterface_()};Dygraph.prototype.destroy=function(){var a=function(c){while(c.hasChildNodes()){a(c.firstChild);c.removeChild(c.firstChild)}};a(this.maindiv_);var b=function(c){for(var d in c){if(typeof(c[d])==="object"){c[d]=null}}};b(this.layout_);b(this.plotter_);b(this)};Dygraph.prototype.createPlotKitCanvas_=function(a){var b=Dygraph.createCanvas();b.style.position="absolute";b.style.top=a.style.top;b.style.left=a.style.left;b.width=this.width_;b.height=this.height_;b.style.width=this.width_+"px";b.style.height=this.height_+"px";return b};Dygraph.hsvToRGB=function(h,g,k){var c;var d;var l;if(g===0){c=k;d=k;l=k}else{var e=Math.floor(h*6);var j=(h*6)-e;var b=k*(1-g);var a=k*(1-(g*j));var m=k*(1-(g*(1-j)));switch(e){case 1:c=a;d=k;l=b;break;case 2:c=b;d=k;l=m;break;case 3:c=b;d=a;l=k;break;case 4:c=m;d=b;l=k;break;case 5:c=k;d=b;l=a;break;case 6:case 0:c=k;d=m;l=b;break}}c=Math.floor(255*c+0.5);d=Math.floor(255*d+0.5);l=Math.floor(255*l+0.5);return"rgb("+c+","+d+","+l+")"};Dygraph.prototype.setColors_=function(){var e=this.attr_("labels").length-1;this.colors_=[];var a=this.attr_("colors");if(!a){var c=this.attr_("colorSaturation")||1;var b=this.attr_("colorValue")||0.5;var j=Math.ceil(e/2);for(var d=1;d<=e;d++){if(!this.visibility()[d-1]){continue}var g=d%2?Math.ceil(d/2):(j+d/2);var f=(1*g/(1+e));this.colors_.push(Dygraph.hsvToRGB(f,c,b))}}else{for(var d=0;d<e;d++){if(!this.visibility()[d]){continue}var h=a[d%a.length];this.colors_.push(h)}}this.renderOptions_.colorScheme=this.colors_;Dygraph.update(this.plotter_.options,this.renderOptions_);Dygraph.update(this.layoutOptions_,this.user_attrs_);Dygraph.update(this.layoutOptions_,this.attrs_)};Dygraph.prototype.getColors=function(){return this.colors_};Dygraph.findPosX=function(a){var b=0;if(a.offsetParent){while(1){b+=a.offsetLeft;if(!a.offsetParent){break}a=a.offsetParent}}else{if(a.x){b+=a.x}}return b};Dygraph.findPosY=function(b){var a=0;if(b.offsetParent){while(1){a+=b.offsetTop;if(!b.offsetParent){break}b=b.offsetParent}}else{if(b.y){a+=b.y}}return a};Dygraph.prototype.createStatusMessage_=function(){var d=this.user_attrs_.labelsDiv;if(d&&null!=d&&(typeof(d)=="string"||d instanceof String)){this.user_attrs_.labelsDiv=document.getElementById(d)}if(!this.attr_("labelsDiv")){var a=this.attr_("labelsDivWidth");var c={position:"absolute",fontSize:"14px",zIndex:10,width:a+"px",top:"0px",left:(this.width_-a-2)+"px",background:"white",textAlign:"left",overflow:"hidden"};Dygraph.update(c,this.attr_("labelsDivStyles"));var e=document.createElement("div");for(var b in c){if(c.hasOwnProperty(b)){e.style[b]=c[b]}}this.graphDiv.appendChild(e);this.attrs_.labelsDiv=e}};Dygraph.prototype.createRollInterface_=function(){var f=this.attr_("showRoller")?"block":"none";var b={position:"absolute",zIndex:10,top:(this.plotter_.area.h-25)+"px",left:(this.plotter_.area.x+1)+"px",display:f};var e=document.createElement("input");e.type="text";e.size="2";e.value=this.rollPeriod_;for(var a in b){if(b.hasOwnProperty(a)){e.style[a]=b[a]}}var d=this.graphDiv;d.appendChild(e);var c=this;e.onchange=function(){c.adjustRoll(e.value)};return e};Dygraph.pageX=function(c){if(c.pageX){return(!c.pageX||c.pageX<0)?0:c.pageX}else{var d=document;var a=document.body;return c.clientX+(d.scrollLeft||a.scrollLeft)-(d.clientLeft||0)}};Dygraph.pageY=function(c){if(c.pageY){return(!c.pageY||c.pageY<0)?0:c.pageY}else{var d=document;var a=document.body;return c.clientY+(d.scrollTop||a.scrollTop)-(d.clientTop||0)}};Dygraph.prototype.createDragInterface_=function(){var o=this;var c=false;var e=false;var b=null;var a=null;var n=null;var l=null;var f=null;var m=null;var k=null;var g=0;var d=0;var j=function(p){return Dygraph.pageX(p)-g};var h=function(p){return Dygraph.pageY(p)-d};Dygraph.addEvent(this.mouseEventElement_,"mousemove",function(p){if(c){n=j(p);l=h(p);o.drawZoomRect_(b,n,f);f=n}else{if(e){n=j(p);l=h(p);o.dateWindow_[0]=m-(n/o.width_)*k;o.dateWindow_[1]=o.dateWindow_[0]+k;o.drawGraph_(o.rawData_)}}});Dygraph.addEvent(this.mouseEventElement_,"mousedown",function(p){g=Dygraph.findPosX(o.canvas_);d=Dygraph.findPosY(o.canvas_);b=j(p);a=h(p);if(p.altKey||p.shiftKey){if(!o.dateWindow_){return}e=true;k=o.dateWindow_[1]-o.dateWindow_[0];m=(b/o.width_)*k+o.dateWindow_[0]}else{c=true}});Dygraph.addEvent(document,"mouseup",function(p){if(c||e){c=false;b=null;a=null}if(e){e=false;m=null;k=null}});Dygraph.addEvent(this.mouseEventElement_,"mouseout",function(p){if(c){n=null;l=null}});Dygraph.addEvent(this.mouseEventElement_,"mouseup",function(r){if(c){c=false;n=j(r);l=h(r);var u=Math.abs(n-b);var s=Math.abs(l-a);if(u<2&&s<2&&o.lastx_!=undefined&&o.lastx_!=-1){if(o.attr_("clickCallback")!=null){o.attr_("clickCallback")(r,o.lastx_,o.selPoints_)}if(o.attr_("pointClickCallback")){var x=-1;var y=0;for(var v=0;v<o.selPoints_.length;v++){var t=o.selPoints_[v];var q=Math.pow(t.canvasx-n,2)+Math.pow(t.canvasy-l,2);if(x==-1||q<y){y=q;x=v}}var w=o.attr_("highlightCircleSize")+2;if(y<=5*5){o.attr_("pointClickCallback")(r,o.selPoints_[x])}}}if(u>=10){o.doZoom_(Math.min(b,n),Math.max(b,n))}else{o.canvas_.getContext("2d").clearRect(0,0,o.canvas_.width,o.canvas_.height)}b=null;a=null}if(e){e=false;m=null;k=null}});Dygraph.addEvent(this.mouseEventElement_,"dblclick",function(p){if(o.dateWindow_==null){return}o.dateWindow_=null;o.drawGraph_(o.rawData_);var q=o.rawData_[0][0];var r=o.rawData_[o.rawData_.length-1][0];if(o.attr_("zoomCallback")){o.attr_("zoomCallback")(q,r)}})};Dygraph.prototype.drawZoomRect_=function(c,d,b){var a=this.canvas_.getContext("2d");if(b){a.clearRect(Math.min(c,b),0,Math.abs(c-b),this.height_)}if(d&&c){a.fillStyle="rgba(128,128,128,0.33)";a.fillRect(Math.min(c,d),0,Math.abs(d-c),this.height_)}};Dygraph.prototype.doZoom_=function(d,a){var b=this.toDataCoords(d,null);var c=b[0];b=this.toDataCoords(a,null);var e=b[0];this.dateWindow_=[c,e];this.drawGraph_(this.rawData_);if(this.attr_("zoomCallback")){this.attr_("zoomCallback")(c,e)}};Dygraph.prototype.mouseMove_=function(b){var a=Dygraph.pageX(b)-Dygraph.findPosX(this.mouseEventElement_);var r=this.layout_.points;var m=-1;var j=-1;var o=1e+100;var q=-1;for(var f=0;f<r.length;f++){var h=Math.abs(r[f].canvasx-a);if(h>o){continue}o=h;q=f}if(q>=0){m=r[q].xval}if(a>r[r.length-1].canvasx){m=r[r.length-1].xval}this.selPoints_=[];var d=r.length;if(!this.attr_("stackedGraph")){for(var f=0;f<d;f++){if(r[f].xval==m){this.selPoints_.push(r[f])}}}else{var g=0;for(var f=d-1;f>=0;f--){if(r[f].xval==m){var c={};for(var e in r[f]){c[e]=r[f][e]}c.yval-=g;g+=c.yval;this.selPoints_.push(c)}}this.selPoints_.reverse()}if(this.attr_("highlightCallback")){var n=this.lastx_;if(n!==null&&m!=n){this.attr_("highlightCallback")(b,m,this.selPoints_)}}this.lastx_=m;this.updateSelection_()};Dygraph.prototype.updateSelection_=function(){var q=this.canvas_.getContext("2d");if(this.previousVerticalX_>=0){var h=0;var j=this.attr_("labels");for(var g=1;g<j.length;g++){var b=this.attr_("highlightCircleSize",j[g]);if(b>h){h=b}}var o=this.previousVerticalX_;q.clearRect(o-h-1,0,2*h+2,this.height_)}var p=function(c){return c&&!isNaN(c)};if(this.selPoints_.length>0){var d=this.selPoints_[0].canvasx;var e=this.attr_("xValueFormatter")(this.lastx_,this)+":";var f=this.attr_("yValueFormatter");var m=this.colors_.length;if(this.attr_("showLabelsOnHighlight")){for(var g=0;g<this.selPoints_.length;g++){if(!this.attr_("labelsShowZeroValues")&&this.selPoints_[g].yval==0){continue}if(!p(this.selPoints_[g].canvasy)){continue}if(this.attr_("labelsSeparateLines")){e+="<br/>"}var n=this.selPoints_[g];var l=new RGBColor(this.colors_[g%m]);var k=f(n.yval);e+=" <b><font color='"+l.toHex()+"'>"+n.name+"</font></b>:"+k}this.attr_("labelsDiv").innerHTML=e}q.save();for(var g=0;g<this.selPoints_.length;g++){if(!p(this.selPoints_[g].canvasy)){continue}var a=this.attr_("highlightCircleSize",this.selPoints_[g].name);q.beginPath();q.fillStyle=this.plotter_.colors[this.selPoints_[g].name];q.arc(d,this.selPoints_[g].canvasy,a,0,2*Math.PI,false);q.fill()}q.restore();this.previousVerticalX_=d}};Dygraph.prototype.setSelection=function(b){this.selPoints_=[];var c=0;if(b!==false){b=b-this.boundaryIds_[0][0]}if(b!==false&&b>=0){for(var a in this.layout_.datasets){if(b<this.layout_.datasets[a].length){this.selPoints_.push(this.layout_.points[c+b])}c+=this.layout_.datasets[a].length}}if(this.selPoints_.length){this.lastx_=this.selPoints_[0].xval;this.updateSelection_()}else{this.lastx_=-1;this.clearSelection()}};Dygraph.prototype.mouseOut_=function(a){if(this.attr_("unhighlightCallback")){this.attr_("unhighlightCallback")(a)}if(this.attr_("hideOverlayOnMouseOut")){this.clearSelection()}};Dygraph.prototype.clearSelection=function(){var a=this.canvas_.getContext("2d");a.clearRect(0,0,this.width_,this.height_);this.attr_("labelsDiv").innerHTML="";this.selPoints_=[];this.lastx_=-1};Dygraph.prototype.getSelection=function(){if(!this.selPoints_||this.selPoints_.length<1){return -1}for(var a=0;a<this.layout_.points.length;a++){if(this.layout_.points[a].x==this.selPoints_[0].x){return a+this.boundaryIds_[0][0]}}return -1};Dygraph.zeropad=function(a){if(a<10){return"0"+a}else{return""+a}};Dygraph.hmsString_=function(a){var c=Dygraph.zeropad;var b=new Date(a);if(b.getSeconds()){return c(b.getHours())+":"+c(b.getMinutes())+":"+c(b.getSeconds())}else{return c(b.getHours())+":"+c(b.getMinutes())}};Dygraph.dateAxisFormatter=function(b,c){if(c>=Dygraph.MONTHLY){return b.strftime("%b %y")}else{var a=b.getHours()*3600+b.getMinutes()*60+b.getSeconds()+b.getMilliseconds();if(a==0||c>=Dygraph.DAILY){return new Date(b.getTime()+3600*1000).strftime("%d%b")}else{return Dygraph.hmsString_(b.getTime())}}};Dygraph.dateString_=function(b,k){var c=Dygraph.zeropad;var g=new Date(b);var h=""+g.getFullYear();var e=c(g.getMonth()+1);var j=c(g.getDate());var f="";var a=g.getHours()*3600+g.getMinutes()*60+g.getSeconds();if(a){f=" "+Dygraph.hmsString_(b)}return h+"/"+e+"/"+j+f};Dygraph.round_=function(c,b){var a=Math.pow(10,b);return Math.round(c*a)/a};Dygraph.prototype.loadedEvent_=function(a){this.rawData_=this.parseCSV_(a);this.drawGraph_(this.rawData_)};Dygraph.prototype.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];Dygraph.prototype.quarters=["Jan","Apr","Jul","Oct"];Dygraph.prototype.addXTicks_=function(){var a,c;if(this.dateWindow_){a=this.dateWindow_[0];c=this.dateWindow_[1]}else{a=this.rawData_[0][0];c=this.rawData_[this.rawData_.length-1][0]}var b=this.attr_("xTicker")(a,c,this);this.layout_.updateOptions({xTicks:b})};Dygraph.SECONDLY=0;Dygraph.TWO_SECONDLY=1;Dygraph.FIVE_SECONDLY=2;Dygraph.TEN_SECONDLY=3;Dygraph.THIRTY_SECONDLY=4;Dygraph.MINUTELY=5;Dygraph.TWO_MINUTELY=6;Dygraph.FIVE_MINUTELY=7;Dygraph.TEN_MINUTELY=8;Dygraph.THIRTY_MINUTELY=9;Dygraph.HOURLY=10;Dygraph.TWO_HOURLY=11;Dygraph.SIX_HOURLY=12;Dygraph.DAILY=13;Dygraph.WEEKLY=14;Dygraph.MONTHLY=15;Dygraph.QUARTERLY=16;Dygraph.BIANNUAL=17;Dygraph.ANNUAL=18;Dygraph.DECADAL=19;Dygraph.NUM_GRANULARITIES=20;Dygraph.SHORT_SPACINGS=[];Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY]=1000*1;Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY]=1000*2;Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY]=1000*5;Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY]=1000*10;Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY]=1000*30;Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY]=1000*60;Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY]=1000*60*2;Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY]=1000*60*5;Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY]=1000*60*10;Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY]=1000*60*30;Dygraph.SHORT_SPACINGS[Dygraph.HOURLY]=1000*3600;Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY]=1000*3600*2;Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY]=1000*3600*6;Dygraph.SHORT_SPACINGS[Dygraph.DAILY]=1000*86400;Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY]=1000*604800;Dygraph.prototype.NumXTicks=function(e,b,g){if(g<Dygraph.MONTHLY){var h=Dygraph.SHORT_SPACINGS[g];return Math.floor(0.5+1*(b-e)/h)}else{var f=1;var d=12;if(g==Dygraph.QUARTERLY){d=3}if(g==Dygraph.BIANNUAL){d=2}if(g==Dygraph.ANNUAL){d=1}if(g==Dygraph.DECADAL){d=1;f=10}var c=365.2524*24*3600*1000;var a=1*(b-e)/c;return Math.floor(0.5+1*a*d/f)}};Dygraph.prototype.GetXAxis=function(m,h,a){var r=this.attr_("xAxisLabelFormatter");var y=[];if(a<Dygraph.MONTHLY){var c=Dygraph.SHORT_SPACINGS[a];var u="%d%b";var v=c/1000;var w=new Date(m);if(v<=60){var f=w.getSeconds();w.setSeconds(f-f%v)}else{w.setSeconds(0);v/=60;if(v<=60){var f=w.getMinutes();w.setMinutes(f-f%v)}else{w.setMinutes(0);v/=60;if(v<=24){var f=w.getHours();w.setHours(f-f%v)}else{w.setHours(0);v/=24;if(v==7){w.setDate(w.getDate()-w.getDay())}}}}m=w.getTime();for(var k=m;k<=h;k+=c){y.push({v:k,label:r(new Date(k),a)})}}else{var e;var n=1;if(a==Dygraph.MONTHLY){e=[0,1,2,3,4,5,6,7,8,9,10,11,12]}else{if(a==Dygraph.QUARTERLY){e=[0,3,6,9]}else{if(a==Dygraph.BIANNUAL){e=[0,6]}else{if(a==Dygraph.ANNUAL){e=[0]}else{if(a==Dygraph.DECADAL){e=[0];n=10}}}}}var q=new Date(m).getFullYear();var o=new Date(h).getFullYear();var b=Dygraph.zeropad;for(var s=q;s<=o;s++){if(s%n!=0){continue}for(var p=0;p<e.length;p++){var l=s+"/"+b(1+e[p])+"/01";var k=Date.parse(l);if(k<m||k>h){continue}y.push({v:k,label:r(new Date(k),a)})}}}return y};Dygraph.dateTicker=function(a,f,d){var b=-1;for(var e=0;e<Dygraph.NUM_GRANULARITIES;e++){var c=d.NumXTicks(a,f,e);if(d.width_/c>=d.attr_("pixelsPerXLabel")){b=e;break}}if(b>=0){return d.GetXAxis(a,f,b)}else{}};Dygraph.numericTicks=function(v,u,l){if(l.attr_("labelsKMG2")){var f=[1,2,4,8]}else{var f=[1,2,5]}var x,p,a,q;var h=l.attr_("pixelsPerYLabel");for(var t=-10;t<50;t++){if(l.attr_("labelsKMG2")){var c=Math.pow(16,t)}else{var c=Math.pow(10,t)}for(var s=0;s<f.length;s++){x=c*f[s];p=Math.floor(v/x)*x;a=Math.ceil(u/x)*x;q=Math.abs(a-p)/x;var d=l.height_/q;if(d>h){break}}if(d>h){break}}var w=[];var r;var o=[];if(l.attr_("labelsKMB")){r=1000;o=["K","M","B","T"]}if(l.attr_("labelsKMG2")){if(r){l.warn("Setting both labelsKMB and labelsKMG2. Pick one!")}r=1024;o=["k","M","G","T"]}if(p>a){x*=-1}for(var t=0;t<q;t++){var g=p+t*x;var b=Math.abs(g);var e=Dygraph.round_(g,2);if(o.length){var m=r*r*r*r;for(var s=3;s>=0;s--,m/=r){if(b>=m){e=Dygraph.round_(g/m,1)+o[s];break}}}w.push({label:e,v:g})}return w};Dygraph.prototype.addYTicks_=function(c,b){var a=Dygraph.numericTicks(c,b,this);this.layout_.updateOptions({yAxis:[c,b],yTicks:a})};Dygraph.prototype.extremeValues_=function(d){var h=null,f=null;var b=this.attr_("errorBars")||this.attr_("customBars");if(b){for(var c=0;c<d.length;c++){var g=d[c][1][0];if(!g){continue}var a=g-d[c][1][1];var e=g+d[c][1][2];if(a>g){a=g}if(e<g){e=g}if(f==null||e>f){f=e}if(h==null||a<h){h=a}}}else{for(var c=0;c<d.length;c++){var g=d[c][1];if(g===null||isNaN(g)){continue}if(f==null||g>f){f=g}if(h==null||g<h){h=g}}}return[h,f]};Dygraph.prototype.drawGraph_=function(D){var n=this.is_initial_draw_;this.is_initial_draw_=false;var z=null,y=null;this.layout_.removeAllDatasets();this.setColors_();this.attrs_.pointSize=0.5*this.attr_("highlightCircleSize");var d=[];var f=[];for(var w=D[0].length-1;w>=1;w--){if(!this.visibility()[w-1]){continue}var b=this.attr_("connectSeparatedPoints",w);var m=[];for(var u=0;u<D.length;u++){if(D[u][w]!=null||!b){var A=D[u][0];m.push([A,D[u][w]])}}m=this.rollingAverage(m,this.rollPeriod_);var p=this.attr_("errorBars")||this.attr_("customBars");if(this.dateWindow_){var F=this.dateWindow_[0];var g=this.dateWindow_[1];var q=[];var e=null,E=null;for(var t=0;t<m.length;t++){if(m[t][0]>=F&&e===null){e=t}if(m[t][0]<=g){E=t}}if(e===null){e=0}if(e>0){e--}if(E===null){E=m.length-1}if(E<m.length-1){E++}this.boundaryIds_[w-1]=[e,E];for(var t=e;t<=E;t++){q.push(m[t])}m=q}else{this.boundaryIds_[w-1]=[0,m.length-1]}var a=this.extremeValues_(m);var r=a[0];var o=a[1];if(z===null||r<z){z=r}if(y===null||o>y){y=o}if(p){for(var u=0;u<m.length;u++){val=[m[u][0],m[u][1][0],m[u][1][1],m[u][1][2]];m[u]=val}}else{if(this.attr_("stackedGraph")){var s=m.length;var B;for(var u=0;u<s;u++){var h=m[u][0];if(d[h]===undefined){d[h]=0}B=m[u][1];d[h]+=B;m[u]=[h,d[h]];if(!y||d[h]>y){y=d[h]}}}}f[w]=m}for(var w=1;w<f.length;w++){if(!this.visibility()[w-1]){continue}this.layout_.addDataset(this.attr_("labels")[w],f[w])}if(this.valueRange_!=null){this.addYTicks_(this.valueRange_[0],this.valueRange_[1]);this.displayedYRange_=this.valueRange_}else{if(this.attr_("includeZero")&&z>0){z=0}var v=y-z;if(v==0){v=y}var c=y+0.1*v;var C=z-0.1*v;if(C<0&&z>=0){C=0}if(c>0&&y<=0){c=0}if(this.attr_("includeZero")){if(y<0){c=0}if(z>0){C=0}}this.addYTicks_(C,c);this.displayedYRange_=[C,c]}this.addXTicks_();this.layout_.updateOptions({dateWindow:this.dateWindow_});this.layout_.evaluateWithError();this.plotter_.clear();this.plotter_.render();this.canvas_.getContext("2d").clearRect(0,0,this.canvas_.width,this.canvas_.height);if(this.attr_("drawCallback")!==null){this.attr_("drawCallback")(this,n)}};Dygraph.prototype.rollingAverage=function(m,d){if(m.length<2){return m}var d=Math.min(d,m.length-1);var b=[];var s=this.attr_("sigma");if(this.fractions_){var k=0;var h=0;var e=100;for(var x=0;x<m.length;x++){k+=m[x][1][0];h+=m[x][1][1];if(x-d>=0){k-=m[x-d][1][0];h-=m[x-d][1][1]}var B=m[x][0];var v=h?k/h:0;if(this.attr_("errorBars")){if(this.wilsonInterval_){if(h){var t=v<0?0:v,u=h;var A=s*Math.sqrt(t*(1-t)/u+s*s/(4*u*u));var a=1+s*s/h;var F=(t+s*s/(2*h)-A)/a;var o=(t+s*s/(2*h)+A)/a;b[x]=[B,[t*e,(t-F)*e,(o-t)*e]]}else{b[x]=[B,[0,0,0]]}}else{var z=h?s*Math.sqrt(v*(1-v)/h):1;b[x]=[B,[e*v,e*z,e*z]]}}else{b[x]=[B,e*v]}}}else{if(this.attr_("customBars")){var F=0;var C=0;var o=0;var g=0;for(var x=0;x<m.length;x++){var E=m[x][1];var l=E[1];b[x]=[m[x][0],[l,l-E[0],E[2]-l]];if(l!=null&&!isNaN(l)){F+=E[0];C+=l;o+=E[2];g+=1}if(x-d>=0){var r=m[x-d];if(r[1][1]!=null&&!isNaN(r[1][1])){F-=r[1][0];C-=r[1][1];o-=r[1][2];g-=1}}b[x]=[m[x][0],[1*C/g,1*(C-F)/g,1*(o-C)/g]]}}else{var q=Math.min(d-1,m.length-2);if(!this.attr_("errorBars")){if(d==1){return m}for(var x=0;x<m.length;x++){var c=0;var D=0;for(var w=Math.max(0,x-d+1);w<x+1;w++){var l=m[w][1];if(l==null||isNaN(l)){continue}D++;c+=m[w][1]}if(D){b[x]=[m[x][0],c/D]}else{b[x]=[m[x][0],null]}}}else{for(var x=0;x<m.length;x++){var c=0;var f=0;var D=0;for(var w=Math.max(0,x-d+1);w<x+1;w++){var l=m[w][1][0];if(l==null||isNaN(l)){continue}D++;c+=m[w][1][0];f+=Math.pow(m[w][1][1],2)}if(D){var z=Math.sqrt(f)/D;b[x]=[m[x][0],[c/D,s*z,s*z]]}else{b[x]=[m[x][0],[null,null,null]]}}}}}return b};Dygraph.dateParser=function(b,a){var c;var e;if(b.search("-")!=-1){c=b.replace("-","/","g");while(c.search("-")!=-1){c=c.replace("-","/")}e=Date.parse(c)}else{if(b.length==8){c=b.substr(0,4)+"/"+b.substr(4,2)+"/"+b.substr(6,2);e=Date.parse(c)}else{e=Date.parse(b)}}if(!e||isNaN(e)){a.error("Couldn't parse "+b+" as a date")}return e};Dygraph.prototype.detectTypeFromString_=function(b){var a=false;if(b.indexOf("-")>=0||b.indexOf("/")>=0||isNaN(parseFloat(b))){a=true}else{if(b.length==8&&b>"19700101"&&b<"20371231"){a=true}}if(a){this.attrs_.xValueFormatter=Dygraph.dateString_;this.attrs_.xValueParser=Dygraph.dateParser;this.attrs_.xTicker=Dygraph.dateTicker;this.attrs_.xAxisLabelFormatter=Dygraph.dateAxisFormatter}else{this.attrs_.xValueFormatter=function(c){return c};this.attrs_.xValueParser=function(c){return parseFloat(c)};this.attrs_.xTicker=Dygraph.numericTicks;this.attrs_.xAxisLabelFormatter=this.attrs_.xValueFormatter}};Dygraph.prototype.parseCSV_=function(h){var n=[];var r=h.split("\n");var b=this.attr_("delimiter");if(r[0].indexOf(b)==-1&&r[0].indexOf("\t")>=0){b="\t"}var a=0;if(this.labelsFromCSV_){a=1;this.attrs_.labels=r[0].split(b)}var k=function(j){var s=parseFloat(j);return isNaN(s)?null:s};var c;var p=false;var d=this.attr_("labels").length;var m=false;for(var g=a;g<r.length;g++){var q=r[g];if(q.length==0){continue}if(q[0]=="#"){continue}var f=q.split(b);if(f.length<2){continue}var l=[];if(!p){this.detectTypeFromString_(f[0]);c=this.attr_("xValueParser");p=true}l[0]=c(f[0],this);if(this.fractions_){for(var e=1;e<f.length;e++){var o=f[e].split("/");l[e]=[k(o[0]),k(o[1])]}}else{if(this.attr_("errorBars")){for(var e=1;e<f.length;e+=2){l[(e+1)/2]=[k(f[e]),k(f[e+1])]}}else{if(this.attr_("customBars")){for(var e=1;e<f.length;e++){var o=f[e].split(";");l[e]=[k(o[0]),k(o[1]),k(o[2])]}}else{for(var e=1;e<f.length;e++){l[e]=k(f[e])}}}}if(n.length>0&&l[0]<n[n.length-1][0]){m=true}n.push(l);if(l.length!=d){this.error("Number of columns in line "+g+" ("+l.length+") does not agree with number of labels ("+d+") "+q)}}if(m){this.warn("CSV is out of order; order it correctly to speed loading.");n.sort(function(s,j){return s[0]-j[0]})}return n};Dygraph.prototype.parseArray_=function(b){if(b.length==0){this.error("Can't plot empty data set");return null}if(b[0].length==0){this.error("Data set cannot contain an empty row");return null}if(this.attr_("labels")==null){this.warn("Using default labels. Set labels explicitly via 'labels' in the options parameter");this.attrs_.labels=["X"];for(var a=1;a<b[0].length;a++){this.attrs_.labels.push("Y"+a)}}if(Dygraph.isDateLike(b[0][0])){this.attrs_.xValueFormatter=Dygraph.dateString_;this.attrs_.xAxisLabelFormatter=Dygraph.dateAxisFormatter;this.attrs_.xTicker=Dygraph.dateTicker;var c=Dygraph.clone(b);for(var a=0;a<b.length;a++){if(c[a].length==0){this.error("Row "+(1+a)+" of data is empty");return null}if(c[a][0]==null||typeof(c[a][0].getTime)!="function"||isNaN(c[a][0].getTime())){this.error("x value in row "+(1+a)+" is not a Date");return null}c[a][0]=c[a][0].getTime()}return c}else{this.attrs_.xValueFormatter=function(d){return d};this.attrs_.xTicker=Dygraph.numericTicks;return b}};Dygraph.prototype.parseDataTable_=function(v){var g=v.getNumberOfColumns();var f=v.getNumberOfRows();var e=v.getColumnType(0);if(e=="date"||e=="datetime"){this.attrs_.xValueFormatter=Dygraph.dateString_;this.attrs_.xValueParser=Dygraph.dateParser;this.attrs_.xTicker=Dygraph.dateTicker;this.attrs_.xAxisLabelFormatter=Dygraph.dateAxisFormatter}else{if(e=="number"){this.attrs_.xValueFormatter=function(j){return j};this.attrs_.xValueParser=function(j){return parseFloat(j)};this.attrs_.xTicker=Dygraph.numericTicks;this.attrs_.xAxisLabelFormatter=this.attrs_.xValueFormatter}else{this.error("only 'date', 'datetime' and 'number' types are supported for column 1 of DataTable input (Got '"+e+"')");return null}}var l=[];var s={};var r=false;for(var p=1;p<g;p++){var b=v.getColumnType(p);if(b=="number"){l.push(p)}else{if(b=="string"&&this.attr_("displayAnnotations")){var q=l[l.length-1];if(!s.hasOwnProperty(q)){s[q]=[p]}else{s[q].push(p)}r=true}else{this.error("Only 'number' is supported as a dependent type with Gviz. 'string' is only supported if displayAnnotations is true")}}}var t=[v.getColumnLabel(0)];for(var p=0;p<l.length;p++){t.push(v.getColumnLabel(l[p]))}this.attrs_.labels=t;g=t.length;var u=[];var h=false;var a=[];for(var p=0;p<f;p++){var d=[];if(typeof(v.getValue(p,0))==="undefined"||v.getValue(p,0)===null){this.warn("Ignoring row "+p+" of DataTable because of undefined or null first column.");continue}if(e=="date"||e=="datetime"){d.push(v.getValue(p,0).getTime())}else{d.push(v.getValue(p,0))}if(!this.attr_("errorBars")){for(var n=0;n<l.length;n++){var c=l[n];d.push(v.getValue(p,c));if(r&&s.hasOwnProperty(c)&&v.getValue(p,s[c][0])!=null){var o={};o.series=v.getColumnLabel(c);o.xval=d[0];o.shortText=String.fromCharCode(65+a.length);o.text="";for(var m=0;m<s[c].length;m++){if(m){o.text+="\n"}o.text+=v.getValue(p,s[c][m])}a.push(o)}}}else{for(var n=0;n<g-1;n++){d.push([v.getValue(p,1+2*n),v.getValue(p,2+2*n)])}}if(u.length>0&&d[0]<u[u.length-1][0]){h=true}u.push(d)}if(h){this.warn("DataTable is out of order; order it correctly to speed loading.");u.sort(function(k,j){return k[0]-j[0]})}this.rawData_=u;if(a.length>0){this.setAnnotations(a,true)}};Dygraph.update=function(b,c){if(typeof(c)!="undefined"&&c!==null){for(var a in c){if(c.hasOwnProperty(a)){b[a]=c[a]}}}return b};Dygraph.isArrayLike=function(b){var a=typeof(b);if((a!="object"&&!(a=="function"&&typeof(b.item)=="function"))||b===null||typeof(b.length)!="number"||b.nodeType===3){return false}return true};Dygraph.isDateLike=function(a){if(typeof(a)!="object"||a===null||typeof(a.getTime)!="function"){return false}return true};Dygraph.clone=function(c){var b=[];for(var a=0;a<c.length;a++){if(Dygraph.isArrayLike(c[a])){b.push(Dygraph.clone(c[a]))}else{b.push(c[a])}}return b};Dygraph.prototype.start_=function(){if(typeof this.file_=="function"){this.loadedEvent_(this.file_())}else{if(Dygraph.isArrayLike(this.file_)){this.rawData_=this.parseArray_(this.file_);this.drawGraph_(this.rawData_)}else{if(typeof this.file_=="object"&&typeof this.file_.getColumnRange=="function"){this.parseDataTable_(this.file_);this.drawGraph_(this.rawData_)}else{if(typeof this.file_=="string"){if(this.file_.indexOf("\n")>=0){this.loadedEvent_(this.file_)}else{var b=new XMLHttpRequest();var a=this;b.onreadystatechange=function(){if(b.readyState==4){if(b.status==200){a.loadedEvent_(b.responseText)}}};b.open("GET",this.file_,true);b.send(null)}}else{this.error("Unknown data format: "+(typeof this.file_))}}}}};Dygraph.prototype.updateOptions=function(a){if(a.rollPeriod){this.rollPeriod_=a.rollPeriod}if(a.dateWindow){this.dateWindow_=a.dateWindow}if(a.valueRange){this.valueRange_=a.valueRange}Dygraph.update(this.user_attrs_,a);Dygraph.update(this.renderOptions_,a);this.labelsFromCSV_=(this.attr_("labels")==null);this.layout_.updateOptions({errorBars:this.attr_("errorBars")});if(a.file){this.file_=a.file;this.start_()}else{this.drawGraph_(this.rawData_)}};Dygraph.prototype.resize=function(b,a){if(this.resize_lock){return}this.resize_lock=true;if((b===null)!=(a===null)){this.warn("Dygraph.resize() should be called with zero parameters or two non-NULL parameters. Pretending it was zero.");b=a=null}this.maindiv_.innerHTML="";this.attrs_.labelsDiv=null;if(b){this.maindiv_.style.width=b+"px";this.maindiv_.style.height=a+"px";this.width_=b;this.height_=a}else{this.width_=this.maindiv_.offsetWidth;this.height_=this.maindiv_.offsetHeight}this.createInterface_();this.drawGraph_(this.rawData_);this.resize_lock=false};Dygraph.prototype.adjustRoll=function(a){this.rollPeriod_=a;this.drawGraph_(this.rawData_)};Dygraph.prototype.visibility=function(){if(!this.attr_("visibility")){this.attrs_.visibility=[]}while(this.attr_("visibility").length<this.rawData_[0].length-1){this.attr_("visibility").push(true)}return this.attr_("visibility")};Dygraph.prototype.setVisibility=function(b,c){var a=this.visibility();if(b<0&&b>=a.length){this.warn("invalid series number in setVisibility: "+b)}else{a[b]=c;this.drawGraph_(this.rawData_)}};Dygraph.prototype.setAnnotations=function(b,a){this.annotations_=b;this.layout_.setAnnotations(this.annotations_);if(!a){this.drawGraph_(this.rawData_)}};Dygraph.prototype.annotations=function(){return this.annotations_};Dygraph.prototype.indexFromSetName=function(a){var c=this.attr_("labels");for(var b=0;b<c.length;b++){if(c[b]==a){return b}}return null};Dygraph.addAnnotationRule=function(){if(Dygraph.addedAnnotationCSS){return}var a;if(document.styleSheets.length>0){a=document.styleSheets[0]}else{var c=document.createElement("style");c.type="text/css";document.getElementsByTagName("head")[0].appendChild(c);for(i=0;i<document.styleSheets.length;i++){if(document.styleSheets[i].disabled){continue}a=document.styleSheets[i]}}var b="border: 1px solid black; background-color: white; text-align: center;";if(a.insertRule){a.insertRule(".dygraphDefaultAnnotation { "+b+" }",0)}else{if(a.addRule){a.addRule(".dygraphDefaultAnnotation",b)}}Dygraph.addedAnnotationCSS=true};Dygraph.createCanvas=function(){var a=document.createElement("canvas");isIE=(/MSIE/.test(navigator.userAgent)&&!window.opera);if(isIE){a=G_vmlCanvasManager.initElement(a)}return a};Dygraph.GVizChart=function(a){this.container=a};Dygraph.GVizChart.prototype.draw=function(b,a){this.container.innerHTML="";this.date_graph=new Dygraph(this.container,b,a)};Dygraph.GVizChart.prototype.setSelection=function(b){var a=false;if(b.length){a=b[0].row}this.date_graph.setSelection(a)};Dygraph.GVizChart.prototype.getSelection=function(){var b=[];var c=this.date_graph.getSelection();if(c<0){return b}col=1;for(var a in this.date_graph.layout_.datasets){b.push({row:c,column:col});col++}return b};DateGraph=Dygraph;function RGBColor(g){this.ok=false;if(g.charAt(0)=="#"){g=g.substr(1,6)}g=g.replace(/ /g,"");g=g.toLowerCase();var a={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"00ffff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000000",blanchedalmond:"ffebcd",blue:"0000ff",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"00ffff",darkblue:"00008b",darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dodgerblue:"1e90ff",feldspar:"d19275",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"ff00ff",gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgrey:"d3d3d3",lightgreen:"90ee90",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa",lightslateblue:"8470ff",lightslategray:"778899",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"00ff00",limegreen:"32cd32",linen:"faf0e6",magenta:"ff00ff",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370d8",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080",oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"d87093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",red:"ff0000",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd",slategray:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",violetred:"d02090",wheat:"f5deb3",white:"ffffff",whitesmoke:"f5f5f5",yellow:"ffff00",yellowgreen:"9acd32"};for(var c in a){if(g==c){g=a[c]}}var h=[{re:/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,example:["rgb(123, 234, 45)","rgb(255,234,245)"],process:function(j){return[parseInt(j[1]),parseInt(j[2]),parseInt(j[3])]}},{re:/^(\w{2})(\w{2})(\w{2})$/,example:["#00ff00","336699"],process:function(j){return[parseInt(j[1],16),parseInt(j[2],16),parseInt(j[3],16)]}},{re:/^(\w{1})(\w{1})(\w{1})$/,example:["#fb0","f0f"],process:function(j){return[parseInt(j[1]+j[1],16),parseInt(j[2]+j[2],16),parseInt(j[3]+j[3],16)]}}];for(var b=0;b<h.length;b++){var e=h[b].re;var d=h[b].process;var f=e.exec(g);if(f){channels=d(f);this.r=channels[0];this.g=channels[1];this.b=channels[2];this.ok=true}}this.r=(this.r<0||isNaN(this.r))?0:((this.r>255)?255:this.r);this.g=(this.g<0||isNaN(this.g))?0:((this.g>255)?255:this.g);this.b=(this.b<0||isNaN(this.b))?0:((this.b>255)?255:this.b);this.toRGB=function(){return"rgb("+this.r+", "+this.g+", "+this.b+")"};this.toHex=function(){var l=this.r.toString(16);var k=this.g.toString(16);var j=this.b.toString(16);if(l.length==1){l="0"+l}if(k.length==1){k="0"+k}if(j.length==1){j="0"+j}return"#"+l+k+j}}Date.ext={};Date.ext.util={};Date.ext.util.xPad=function(a,c,b){if(typeof(b)=="undefined"){b=10}for(;parseInt(a,10)<b&&b>1;b/=10){a=c.toString()+a}return a.toString()};Date.prototype.locale="en-GB";if(document.getElementsByTagName("html")&&document.getElementsByTagName("html")[0].lang){Date.prototype.locale=document.getElementsByTagName("html")[0].lang}Date.ext.locales={};Date.ext.locales.en={a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a %d %b %Y %T %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%T"};Date.ext.locales["en-US"]=Date.ext.locales.en;Date.ext.locales["en-US"].c="%a %d %b %Y %r %Z";Date.ext.locales["en-US"].x="%D";Date.ext.locales["en-US"].X="%r";Date.ext.locales["en-GB"]=Date.ext.locales.en;Date.ext.locales["en-AU"]=Date.ext.locales["en-GB"];Date.ext.formats={a:function(a){return Date.ext.locales[a.locale].a[a.getDay()]},A:function(a){return Date.ext.locales[a.locale].A[a.getDay()]},b:function(a){return Date.ext.locales[a.locale].b[a.getMonth()]},B:function(a){return Date.ext.locales[a.locale].B[a.getMonth()]},c:"toLocaleString",C:function(a){return Date.ext.util.xPad(parseInt(a.getFullYear()/100,10),0)},d:["getDate","0"],e:["getDate"," "],g:function(a){return Date.ext.util.xPad(parseInt(Date.ext.util.G(a)/100,10),0)},G:function(c){var e=c.getFullYear();var b=parseInt(Date.ext.formats.V(c),10);var a=parseInt(Date.ext.formats.W(c),10);if(a>b){e++}else{if(a===0&&b>=52){e--}}return e},H:["getHours","0"],I:function(b){var a=b.getHours()%12;return Date.ext.util.xPad(a===0?12:a,0)},j:function(c){var a=c-new Date(""+c.getFullYear()+"/1/1 GMT");a+=c.getTimezoneOffset()*60000;var b=parseInt(a/60000/60/24,10)+1;return Date.ext.util.xPad(b,0,100)},m:function(a){return Date.ext.util.xPad(a.getMonth()+1,0)},M:["getMinutes","0"],p:function(a){return Date.ext.locales[a.locale].p[a.getHours()>=12?1:0]},P:function(a){return Date.ext.locales[a.locale].P[a.getHours()>=12?1:0]},S:["getSeconds","0"],u:function(a){var b=a.getDay();return b===0?7:b},U:function(e){var a=parseInt(Date.ext.formats.j(e),10);var c=6-e.getDay();var b=parseInt((a+c)/7,10);return Date.ext.util.xPad(b,0)},V:function(e){var c=parseInt(Date.ext.formats.W(e),10);var a=(new Date(""+e.getFullYear()+"/1/1")).getDay();var b=c+(a>4||a<=1?0:1);if(b==53&&(new Date(""+e.getFullYear()+"/12/31")).getDay()<4){b=1}else{if(b===0){b=Date.ext.formats.V(new Date(""+(e.getFullYear()-1)+"/12/31"))}}return Date.ext.util.xPad(b,0)},w:"getDay",W:function(e){var a=parseInt(Date.ext.formats.j(e),10);var c=7-Date.ext.formats.u(e);var b=parseInt((a+c)/7,10);return Date.ext.util.xPad(b,0,10)},y:function(a){return Date.ext.util.xPad(a.getFullYear()%100,0)},Y:"getFullYear",z:function(c){var b=c.getTimezoneOffset();var a=Date.ext.util.xPad(parseInt(Math.abs(b/60),10),0);var e=Date.ext.util.xPad(b%60,0);return(b>0?"-":"+")+a+e},Z:function(a){return a.toString().replace(/^.*\(([^)]+)\)$/,"$1")},"%":function(a){return"%"}};Date.ext.aggregates={c:"locale",D:"%m/%d/%y",h:"%b",n:"\n",r:"%I:%M:%S %p",R:"%H:%M",t:"\t",T:"%H:%M:%S",x:"locale",X:"locale"};Date.ext.aggregates.z=Date.ext.formats.z(new Date());Date.ext.aggregates.Z=Date.ext.formats.Z(new Date());Date.ext.unsupported={};Date.prototype.strftime=function(a){if(!(this.locale in Date.ext.locales)){if(this.locale.replace(/-[a-zA-Z]+$/,"") in Date.ext.locales){this.locale=this.locale.replace(/-[a-zA-Z]+$/,"")}else{this.locale="en-GB"}}var c=this;while(a.match(/%[cDhnrRtTxXzZ]/)){a=a.replace(/%([cDhnrRtTxXzZ])/g,function(e,d){var g=Date.ext.aggregates[d];return(g=="locale"?Date.ext.locales[c.locale][d]:g)})}var b=a.replace(/%([aAbBCdegGHIjmMpPSuUVwWyY%])/g,function(e,d){var g=Date.ext.formats[d];if(typeof(g)=="string"){return c[g]()}else{if(typeof(g)=="function"){return g.call(c,c)}else{if(typeof(g)=="object"&&typeof(g[0])=="string"){return Date.ext.util.xPad(c[g[0]](),g[1])}else{return d}}}});c=null;return b};
\ No newline at end of file
diff --git a/Tools/TestResultServer/static-dashboards/flakiness_dashboard.css b/Tools/TestResultServer/static-dashboards/flakiness_dashboard.css
new file mode 100644
index 0000000..063addf
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/flakiness_dashboard.css
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+body {
+    font-family: arial;
+    font-size: 13px;
+}
+h2 {
+    font-size: 16px;
+    margin-bottom: .25em;
+}
+h3 {
+    font-size: 13px;
+    margin: 0;
+}
+input {
+    margin-right: 15px;
+}
+label {
+    padding-left: 2em;
+    white-space: nowrap;
+}
+.forms {
+    display: -webkit-box;
+    -webkit-box-align: baseline;
+}
+.forms > * {
+    display: block;
+}
+.forms span {
+    padding: 0px 3px;
+}
+#tests-form {
+    display: -webkit-box;
+    -webkit-box-align: baseline;
+    -webkit-box-flex: 1;
+}
+#tests-form > * {
+    display: block;
+}
+#tests-input {
+    display: -webkit-box;
+    -webkit-box-flex: 1;
+}
diff --git a/Tools/TestResultServer/static-dashboards/flakiness_dashboard.html b/Tools/TestResultServer/static-dashboards/flakiness_dashboard.html
new file mode 100644
index 0000000..625d423
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/flakiness_dashboard.html
@@ -0,0 +1,36 @@
+<!-- Copyright (C) 2011 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.
+-->
+<!DOCTYPE HTML>
+<title>Chromium/WebKit Test History</title>
+<link rel="stylesheet" href="flakiness_dashboard.css"></link>
+<link rel="stylesheet" href="flakiness_dashboard_tests.css"></link>
+<script src="builders.js"></script>
+<script src="loader.js"></script>
+<script src="dashboard_base.js"></script>
+<script src="flakiness_dashboard.js"></script>
diff --git a/Tools/TestResultServer/static-dashboards/flakiness_dashboard.js b/Tools/TestResultServer/static-dashboards/flakiness_dashboard.js
new file mode 100644
index 0000000..7589e34
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/flakiness_dashboard.js
@@ -0,0 +1,2607 @@
+// Copyright (C) 2012 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.
+
+//////////////////////////////////////////////////////////////////////////////
+// CONSTANTS
+//////////////////////////////////////////////////////////////////////////////
+var ALL = 'ALL';
+var FORWARD = 'forward';
+var BACKWARD = 'backward';
+var GTEST_MODIFIERS = ['FLAKY', 'FAILS', 'MAYBE', 'DISABLED'];
+var TEST_URL_BASE_PATH_TRAC = 'http://trac.webkit.org/browser/trunk/LayoutTests/';
+var TEST_URL_BASE_PATH = "http://svn.webkit.org/repository/webkit/trunk/LayoutTests/";
+var EXPECTATIONS_URL_BASE_PATH = TEST_URL_BASE_PATH + "platform/";
+var TEST_RESULTS_BASE_PATH = 'http://build.chromium.org/f/chromium/layout_test_results/';
+var GPU_RESULTS_BASE_PATH = 'http://chromium-browser-gpu-tests.commondatastorage.googleapis.com/runs/'
+
+var PLATFORMS = {
+    'CHROMIUM': {
+        expectationsDirectory: 'chromium',
+        subPlatforms: {
+            'LION': { fallbackPlatforms: ['CHROMIUM'] },
+            'SNOWLEOPARD': { fallbackPlatforms: ['CHROMIUM'] },
+            'XP': { fallbackPlatforms: ['CHROMIUM'] },
+            'VISTA': { fallbackPlatforms: ['CHROMIUM'] },
+            'WIN7': { fallbackPlatforms: ['CHROMIUM'] },
+            'LUCID': { fallbackPlatforms: ['CHROMIUM'] },
+            'ANDROID': { fallbackPlatforms: ['CHROMIUM'], expectationsDirectory: 'chromium-android' }
+        },
+        platformModifierUnions: {
+            'MAC': ['CHROMIUM_LION', 'CHROMIUM_SNOWLEOPARD'],
+            'WIN': ['CHROMIUM_XP', 'CHROMIUM_VISTA', 'CHROMIUM_WIN7'],
+            'LINUX': ['CHROMIUM_LUCID']
+        }
+    },
+    'APPLE': {
+        subPlatforms: {
+            'MAC': {
+                expectationsDirectory: 'mac',
+                subPlatforms: {
+                    'LION': {
+                        expectationsDirectory: 'mac-lion',
+                        subPlatforms: {
+                            'WK1': { fallbackPlatforms: ['APPLE_MAC_LION', 'APPLE_MAC'] },
+                            'WK2': { fallbackPlatforms: ['APPLE_MAC_LION', 'APPLE_MAC', 'WK2'] }
+                        }
+                    },
+                    'SNOWLEOPARD': {
+                        expectationsDirectory: 'mac-snowleopard',
+                        subPlatforms: {
+                            'WK1': { fallbackPlatforms: ['APPLE_MAC_SNOWLEOPARD', 'APPLE_MAC'] },
+                            'WK2': { fallbackPlatforms: ['APPLE_MAC_SNOWLEOPARD', 'APPLE_MAC', 'WK2'] }
+                        }
+                    }
+                }
+            },
+            'WIN': {
+                expectationsDirectory: 'win',
+                subPlatforms: {
+                    'XP': { fallbackPlatforms: ['APPLE_WIN'] },
+                    'WIN7': { fallbackPlatforms: ['APPLE_WIN'] }
+                }
+            }
+        }
+    },
+    'GTK': {
+        expectationsDirectory: 'gtk',
+        subPlatforms: {
+            'LINUX': {
+                subPlatforms: {
+                    'WK1': { fallbackPlatforms: ['GTK'] },
+                    'WK2': { fallbackPlatforms: ['GTK', 'WK2'], expectationsDirectory: 'gtk-wk2' }
+                }
+            }
+        }
+    },
+    'QT': {
+        expectationsDirectory: 'qt',
+        subPlatforms: {
+            'LINUX': { fallbackPlatforms: ['QT'] }
+        }
+    },
+    'EFL': {
+        expectationsDirectory: 'efl',
+        subPlatforms: {
+            'LINUX': {
+                subPlatforms: {
+                    'WK1': { fallbackPlatforms: ['EFL'], expectationsDirectory: 'efl-wk1' },
+                    'WK2': { fallbackPlatforms: ['EFL', 'WK2'], expectationsDirectory: 'efl-wk2' }
+                }
+            }
+        }
+    },
+    'WK2': {
+        basePlatform: true,
+        expectationsDirectory: 'wk2'
+    }
+};
+
+var BUILD_TYPES = {'DEBUG': 'DBG', 'RELEASE': 'RELEASE'};
+var MIN_SECONDS_FOR_SLOW_TEST = 4;
+var MIN_SECONDS_FOR_SLOW_TEST_DEBUG = 2 * MIN_SECONDS_FOR_SLOW_TEST;
+var FAIL_RESULTS = ['IMAGE', 'IMAGE+TEXT', 'TEXT', 'MISSING'];
+var CHUNK_SIZE = 25;
+var MAX_RESULTS = 1500;
+
+// FIXME: Figure out how to make this not be hard-coded.
+var VIRTUAL_SUITES = {
+    'platform/chromium/virtual/gpu/fast/canvas': 'fast/canvas',
+    'platform/chromium/virtual/gpu/canvas/philip': 'canvas/philip'
+};
+
+//////////////////////////////////////////////////////////////////////////////
+// Methods and objects from dashboard_base.js to override.
+//////////////////////////////////////////////////////////////////////////////
+function generatePage()
+{
+    if (g_crossDashboardState.useTestData)
+        return;
+
+    updateDefaultBuilderState();
+    document.body.innerHTML = '<div id="loading-ui">LOADING...</div>';
+    showErrors();
+
+    // tests expands to all tests that match the CSV list.
+    // result expands to all tests that ever have the given result
+    if (g_currentState.tests || g_currentState.result)
+        generatePageForIndividualTests(individualTests());
+    else if (g_currentState.expectationsUpdate)
+        generatePageForExpectationsUpdate();
+    else
+        generatePageForBuilder(g_currentState.builder);
+
+    for (var builder in g_builders)
+        processTestResultsForBuilderAsync(builder);
+
+    postHeightChangedMessage();
+}
+
+function handleValidHashParameter(key, value)
+{
+    switch(key) {
+    case 'tests':
+        validateParameter(g_currentState, key, value,
+            function() {
+                return isValidName(value);
+            });
+        return true;
+
+    case 'result':
+        value = value.toUpperCase();
+        validateParameter(g_currentState, key, value,
+            function() {
+                for (var result in LAYOUT_TEST_EXPECTATIONS_MAP_) {
+                    if (value == LAYOUT_TEST_EXPECTATIONS_MAP_[result])
+                        return true;
+                }
+                return false;
+            });
+        return true;
+
+    case 'builder':
+        validateParameter(g_currentState, key, value,
+            function() {
+                return value in g_builders;
+            });
+        return true;
+
+    case 'sortColumn':
+        validateParameter(g_currentState, key, value,
+            function() {
+                // Get all possible headers since the actual used set of headers
+                // depends on the values in g_currentState, which are currently being set.
+                var headers = tableHeaders(true);
+                for (var i = 0; i < headers.length; i++) {
+                    if (value == sortColumnFromTableHeader(headers[i]))
+                        return true;
+                }
+                return value == 'test' || value == 'builder';
+            });
+        return true;
+
+    case 'sortOrder':
+        validateParameter(g_currentState, key, value,
+            function() {
+                return value == FORWARD || value == BACKWARD;
+            });
+        return true;
+
+    case 'resultsHeight':
+    case 'updateIndex':
+    case 'revision':
+        validateParameter(g_currentState, key, Number(value),
+            function() {
+                return value.match(/^\d+$/);
+            });
+        return true;
+
+    case 'showChrome':
+    case 'showCorrectExpectations':
+    case 'showWrongExpectations':
+    case 'showExpectations':
+    case 'showFlaky':
+    case 'showLargeExpectations':
+    case 'legacyExpectationsSemantics':
+    case 'showSkipped':
+    case 'showSlow':
+    case 'showUnexpectedPasses':
+    case 'showWontFixSkip':
+    case 'expectationsUpdate':
+        g_currentState[key] = value == 'true';
+        return true;
+
+    default:
+        return false;
+    }
+}
+
+g_defaultDashboardSpecificStateValues = {
+    sortOrder: BACKWARD,
+    sortColumn: 'flakiness',
+    showExpectations: false,
+    showFlaky: true,
+    showLargeExpectations: false,
+    legacyExpectationsSemantics: true,
+    showChrome: true,
+    showCorrectExpectations: !isLayoutTestResults(),
+    showWrongExpectations: !isLayoutTestResults(),
+    showWontFixSkip: !isLayoutTestResults(),
+    showSlow: !isLayoutTestResults(),
+    showSkipped: !isLayoutTestResults(),
+    showUnexpectedPasses: !isLayoutTestResults(),
+    expectationsUpdate: false,
+    updateIndex: 0,
+    resultsHeight: 300,
+    revision: null,
+    tests: '',
+    result: '',
+};
+
+//////////////////////////////////////////////////////////////////////////////
+// GLOBALS
+//////////////////////////////////////////////////////////////////////////////
+
+var g_perBuilderPlatformAndBuildType = {};
+var g_perBuilderFailures = {};
+// Map of builder to arrays of tests that are listed in the expectations file
+// but have for that builder.
+var g_perBuilderWithExpectationsButNoFailures = {};
+// Map of builder to arrays of paths that are skipped. This shows the raw
+// path used in TestExpectations rather than the test path since we
+// don't actually have any data here for skipped tests.
+var g_perBuilderSkippedPaths = {};
+// Maps test path to an array of {builder, testResults} objects.
+var g_testToResultsMap = {};
+// Tests that the user wants to update expectations for.
+var g_confirmedTests = {};
+
+function traversePlatformsTree(callback)
+{
+    function traverse(platformObject, parentPlatform) {
+        Object.keys(platformObject).forEach(function(platformName) {
+            var platform = platformObject[platformName];
+            platformName = parentPlatform ? parentPlatform + platformName : platformName;
+
+            if (platform.subPlatforms)
+                traverse(platform.subPlatforms, platformName + '_');
+            else if (!platform.basePlatform)
+                callback(platform, platformName);
+        });
+    }
+    traverse(PLATFORMS, null);
+}
+
+function createResultsObjectForTest(test, builder)
+{
+    return {
+        test: test,
+        builder: builder,
+        // HTML for display of the results in the flakiness column
+        html: '',
+        flips: 0,
+        slowestTime: 0,
+        slowestNonTimeoutCrashTime: 0,
+        meetsExpectations: true,
+        isWontFixSkip: false,
+        isFlaky: false,
+        // Sorted string of missing expectations
+        missing: '',
+        // String of extra expectations (i.e. expectations that never occur).
+        extra: '',
+        modifiers: '',
+        bugs: '',
+        expectations : '',
+        rawResults: '',
+        // List of all the results the test actually has.
+        actualResults: []
+    };
+}
+
+function matchingElement(stringToMatch, elementsMap)
+{
+    for (var element in elementsMap) {
+        if (stringContains(stringToMatch, elementsMap[element]))
+            return element;
+    }
+}
+
+function determineWKPlatform(builderName, basePlatform)
+{
+    var isWK2Builder = stringContains(builderName, 'WK2') || stringContains(builderName, 'WEBKIT2');
+    return basePlatform + (isWK2Builder ? '_WK2' : '_WK1');
+}
+
+function nonChromiumPlatform(builderNameUpperCase)
+{
+    if (stringContains(builderNameUpperCase, 'WINDOWS 7'))
+        return 'APPLE_WIN_WIN7';
+    if (stringContains(builderNameUpperCase, 'WINDOWS XP'))
+        return 'APPLE_WIN_XP';
+    if (stringContains(builderNameUpperCase, 'QT LINUX'))
+        return 'QT_LINUX';
+
+    if (stringContains(builderNameUpperCase, 'LION'))
+        return determineWKPlatform(builderNameUpperCase, 'APPLE_MAC_LION');
+    if (stringContains(builderNameUpperCase, 'SNOWLEOPARD'))
+        return determineWKPlatform(builderNameUpperCase, 'APPLE_MAC_SNOWLEOPARD');
+    if (stringContains(builderNameUpperCase, 'GTK LINUX'))
+        return determineWKPlatform(builderNameUpperCase, 'GTK_LINUX');
+    if (stringContains(builderNameUpperCase, 'EFL'))
+        return determineWKPlatform(builderNameUpperCase, 'EFL_LINUX');
+}
+
+function chromiumPlatform(builderNameUpperCase)
+{
+    if (stringContains(builderNameUpperCase, 'MAC')) {
+        if (stringContains(builderNameUpperCase, '10.7'))
+            return 'CHROMIUM_LION';
+        // The webkit.org 'Chromium Mac Release (Tests)' bot runs SnowLeopard.
+        return 'CHROMIUM_SNOWLEOPARD';
+    }
+    if (stringContains(builderNameUpperCase, 'WIN7'))
+        return 'CHROMIUM_WIN7';
+    if (stringContains(builderNameUpperCase, 'VISTA'))
+        return 'CHROMIUM_VISTA';
+    if (stringContains(builderNameUpperCase, 'WIN') || stringContains(builderNameUpperCase, 'XP'))
+        return 'CHROMIUM_XP';
+    if (stringContains(builderNameUpperCase, 'LINUX'))
+        return 'CHROMIUM_LUCID';
+    if (stringContains(builderNameUpperCase, 'ANDROID'))
+        return 'CHROMIUM_ANDROID';
+    // The interactive bot is XP, but doesn't have an OS in it's name.
+    if (stringContains(builderNameUpperCase, 'INTERACTIVE'))
+        return 'CHROMIUM_XP';
+}
+
+
+function platformAndBuildType(builderName)
+{
+    if (!g_perBuilderPlatformAndBuildType[builderName]) {
+        var builderNameUpperCase = builderName.toUpperCase();
+        
+        var platform = '';
+        if (isLayoutTestResults() && g_crossDashboardState.group == '@ToT - webkit.org' && !stringContains(builderNameUpperCase, 'CHROMIUM'))
+            platform = nonChromiumPlatform(builderNameUpperCase);
+        else
+            platform = chromiumPlatform(builderNameUpperCase);
+        
+        if (!platform)
+            console.error('Could not resolve platform for builder: ' + builderName);
+
+        var buildType = stringContains(builderNameUpperCase, 'DBG') || stringContains(builderNameUpperCase, 'DEBUG') ? 'DEBUG' : 'RELEASE';
+        g_perBuilderPlatformAndBuildType[builderName] = {platform: platform, buildType: buildType};
+    }
+    return g_perBuilderPlatformAndBuildType[builderName];
+}
+
+function isDebug(builderName)
+{
+    return platformAndBuildType(builderName).buildType == 'DEBUG';
+}
+
+// Returns the expectation string for the given single character result.
+// This string should match the expectations that are put into
+// test_expectations.py.
+//
+// For example, if we start explicitly listing IMAGE result failures,
+// this function should start returning 'IMAGE'.
+function expectationsFileStringForResult(result)
+{
+    // For the purposes of comparing against the expecations of a test,
+    // consider simplified diff failures as just text failures since
+    // the test_expectations file doesn't treat them specially.
+    if (result == 'S')
+        return 'TEXT';
+
+    if (result == 'N')
+        return '';
+
+    return expectationsMap()[result];
+}
+
+var TestTrie = function(builders, resultsByBuilder)
+{
+    this._trie = {};
+
+    for (var builder in builders) {
+        var testsForBuilder = resultsByBuilder[builder].tests;
+        for (var test in testsForBuilder)
+            this._addTest(test.split('/'), this._trie);
+    }
+}
+
+TestTrie.prototype.forEach = function(callback, startingTriePath)
+{
+    var testsTrie = this._trie;
+    if (startingTriePath) {
+        var splitPath = startingTriePath.split('/');
+        while (splitPath.length && testsTrie)
+            testsTrie = testsTrie[splitPath.shift()];
+    }
+
+    if (!testsTrie)
+        return;
+
+    function traverse(trie, triePath) {
+        if (trie == true)
+            callback(triePath);
+        else {
+            for (var member in trie)
+                traverse(trie[member], triePath ? triePath + '/' + member : member);
+        }
+    }
+    traverse(testsTrie, startingTriePath);
+}
+
+TestTrie.prototype._addTest = function(test, trie)
+{
+    var rootComponent = test.shift();
+    if (!test.length) {
+        if (!trie[rootComponent])
+            trie[rootComponent] = true;
+        return;
+    }
+
+    if (!trie[rootComponent] || trie[rootComponent] == true)
+        trie[rootComponent] = {};
+    this._addTest(test, trie[rootComponent]);
+}
+
+// Map of all tests to true values. This is just so we can have the list of
+// all tests across all the builders.
+var g_allTestsTrie;
+
+function getAllTestsTrie()
+{
+    if (!g_allTestsTrie)
+        g_allTestsTrie = new TestTrie(g_builders, g_resultsByBuilder);
+
+    return g_allTestsTrie;
+}
+
+// Returns an array of tests to be displayed in the individual tests view.
+// Note that a directory can be listed as a test, so we expand that into all
+// tests in the directory.
+function individualTests()
+{
+    if (g_currentState.result)
+        return allTestsWithResult(g_currentState.result);
+
+    if (!g_currentState.tests)
+        return [];
+
+    return individualTestsForSubstringList();
+}
+
+function substringList()
+{
+    // Convert windows slashes to unix slashes.
+    var tests = g_currentState.tests.replace(/\\/g, '/');
+    var separator = stringContains(tests, ' ') ? ' ' : ',';
+    var testList = tests.split(separator);
+
+    if (isLayoutTestResults())
+        return testList;
+
+    var testListWithoutModifiers = [];
+    testList.forEach(function(path) {
+        GTEST_MODIFIERS.forEach(function(modifier) {
+            path = path.replace('.' + modifier + '_', '.');
+        });
+        testListWithoutModifiers.push(path);
+    });
+    return testListWithoutModifiers;
+}
+
+function individualTestsForSubstringList()
+{
+    var testList = substringList();
+
+    // Put the tests into an object first and then move them into an array
+    // as a way of deduping.
+    var testsMap = {};
+    for (var i = 0; i < testList.length; i++) {
+        var path = testList[i];
+
+        // Ignore whitespace entries as they'd match every test.
+        if (path.match(/^\s*$/))
+            continue;
+
+        var hasAnyMatches = false;
+        getAllTestsTrie().forEach(function(triePath) {
+            if (caseInsensitiveContains(triePath, path)) {
+                testsMap[triePath] = 1;
+                hasAnyMatches = true;
+            }
+        });
+
+        // If a path doesn't match any tests, then assume it's a full path
+        // to a test that passes on all builders.
+        if (!hasAnyMatches)
+            testsMap[path] = 1;
+    }
+
+    var testsArray = [];
+    for (var test in testsMap)
+        testsArray.push(test);
+    return testsArray;
+}
+
+// Returns whether this test's slowest time is above the cutoff for
+// being a slow test.
+function isSlowTest(resultsForTest)
+{
+    var maxTime = isDebug(resultsForTest.builder) ? MIN_SECONDS_FOR_SLOW_TEST_DEBUG : MIN_SECONDS_FOR_SLOW_TEST;
+    return resultsForTest.slowestNonTimeoutCrashTime > maxTime;
+}
+
+// Returns whether this test's slowest time is *well* below the cutoff for
+// being a slow test.
+function isFastTest(resultsForTest)
+{
+    var maxTime = isDebug(resultsForTest.builder) ? MIN_SECONDS_FOR_SLOW_TEST_DEBUG : MIN_SECONDS_FOR_SLOW_TEST;
+    return resultsForTest.slowestNonTimeoutCrashTime < maxTime / 2;
+}
+
+function allTestsWithResult(result)
+{
+    processTestRunsForAllBuilders();
+    var retVal = [];
+
+    getAllTestsTrie().forEach(function(triePath) {
+        for (var i = 0; i < g_testToResultsMap[triePath].length; i++) {
+            if (g_testToResultsMap[triePath][i].actualResults.indexOf(result) != -1) {
+                retVal.push(triePath);
+                break;
+            }
+        }
+    });
+
+    return retVal;
+}
+
+
+// Adds all the tests for the given builder to the testMapToPopulate.
+function addTestsForBuilder(builder, testMapToPopulate)
+{
+    var tests = g_resultsByBuilder[builder].tests;
+    for (var test in tests) {
+        testMapToPopulate[test] = true;
+    }
+}
+
+// Map of all tests to true values by platform and build type.
+// e.g. g_allTestsByPlatformAndBuildType['XP']['DEBUG'] will have the union
+// of all tests run on the xp-debug builders.
+var g_allTestsByPlatformAndBuildType = {};
+traversePlatformsTree(function(platform, platformName) {
+    g_allTestsByPlatformAndBuildType[platformName] = {};
+});
+
+// Map of all tests to true values by platform and build type.
+// e.g. g_allTestsByPlatformAndBuildType['WIN']['DEBUG'] will have the union
+// of all tests run on the win-debug builders.
+function allTestsWithSamePlatformAndBuildType(platform, buildType)
+{
+    if (!g_allTestsByPlatformAndBuildType[platform][buildType]) {
+        var tests = {};
+        for (var thisBuilder in g_builders) {
+            var thisBuilderBuildInfo = platformAndBuildType(thisBuilder);
+            if (thisBuilderBuildInfo.buildType == buildType && thisBuilderBuildInfo.platform == platform) {
+                addTestsForBuilder(thisBuilder, tests);
+            }
+        }
+        g_allTestsByPlatformAndBuildType[platform][buildType] = tests;
+    }
+
+    return g_allTestsByPlatformAndBuildType[platform][buildType];
+}
+
+function getExpectations(test, platform, buildType)
+{
+    var testObject = g_allExpectations[test];
+    if (!testObject)
+        return null;
+
+    var platformObject = testObject[platform];
+    if (!platformObject)
+        return null;
+        
+    return platformObject[buildType];
+}
+
+function filterBugs(modifiers)
+{
+    var bugs = modifiers.match(/\b(Bug|webkit.org|crbug.com|code.google.com)\S*/g);
+    if (!bugs)
+        return {bugs: '', modifiers: modifiers};
+    for (var j = 0; j < bugs.length; j++)
+        modifiers = modifiers.replace(bugs[j], '');
+    return {bugs: bugs.join(' '), modifiers: collapseWhitespace(trimString(modifiers))};
+}
+
+function populateExpectationsData(resultsObject)
+{
+    var buildInfo = platformAndBuildType(resultsObject.builder);
+    var expectations = getExpectations(resultsObject.test, buildInfo.platform, buildInfo.buildType);
+    if (!expectations)
+        return;
+
+    resultsObject.expectations = expectations.expectations;
+    var filteredModifiers = filterBugs(expectations.modifiers);
+    resultsObject.modifiers = filteredModifiers.modifiers;
+    resultsObject.bugs = filteredModifiers.bugs;
+    resultsObject.isWontFixSkip = stringContains(expectations.modifiers, 'WONTFIX') || stringContains(expectations.modifiers, 'SKIP'); 
+}
+
+function platformObjectForName(platformName)
+{
+    var platformsList = platformName.split("_");
+    var platformObject = PLATFORMS[platformsList.shift()];
+    platformsList.forEach(function(platformName) {
+        platformObject = platformObject.subPlatforms[platformName];
+    });
+    return platformObject;
+}
+
+// Data structure to hold the processed expectations.
+// g_allExpectations[testPath][platform][buildType] gets the object that has
+// expectations and modifiers properties for this platform/buildType.
+//
+// platform and buildType both go through fallback sets of keys from most
+// specific key to least specific. For example, on Windows XP, we first
+// check the platform WIN-XP, if there's no such object, we check WIN,
+// then finally we check ALL. For build types, we check the current
+// buildType, then ALL.
+var g_allExpectations;
+
+function getParsedExpectations(data)
+{
+    var expectations = [];
+    var lines = data.split('\n');
+    lines.forEach(function(line) {
+        line = trimString(line);
+        if (!line || startsWith(line, '#'))
+            return;
+
+        // This code mimics _tokenize_line_using_new_format() in
+        // Tools/Scripts/webkitpy/layout_tests/models/test_expectations.py
+        //
+        // FIXME: consider doing more error checking here.
+        //
+        // FIXME: Clean this all up once we've fully cut over to the new syntax.
+        var tokens = line.split(/\s+/)
+        var parsed_bugs = [];
+        var parsed_modifiers = [];
+        var parsed_path;
+        var parsed_expectations = [];
+        var state = 'start';
+
+        // This clones _configuration_tokens_list in test_expectations.py.
+        // FIXME: unify with the platforms constants at the top of the file.
+        var configuration_tokens = {
+            'Release': 'RELEASE',
+            'Debug': 'DEBUG',
+            'Mac': 'MAC',
+            'Win': 'WIN',
+            'Linux': 'LINUX',
+            'SnowLeopard': 'SNOWLEOPARD',
+            'Lion': 'LION',
+            'MountainLion': 'MOUNTAINLION',
+            'Win7': 'WIN7',
+            'XP': 'XP',
+            'Vista': 'VISTA',
+            'Android': 'ANDROID',
+        };
+
+        var expectation_tokens = {
+            'Crash': 'CRASH',
+            'Failure': 'FAIL',
+            'ImageOnlyFailure': 'IMAGE',
+            'Missing': 'MISSING',
+            'Pass': 'PASS',
+            'Rebaseline': 'REBASELINE',
+            'Skip': 'SKIP',
+            'Slow': 'SLOW',
+            'Timeout': 'TIMEOUT',
+            'WontFix': 'WONTFIX',
+        };
+
+            
+        tokens.forEach(function(token) {
+          if (token.indexOf('Bug') != -1 ||
+              token.indexOf('webkit.org') != -1 ||
+              token.indexOf('crbug.com') != -1 ||
+              token.indexOf('code.google.com') != -1) {
+              parsed_bugs.push(token);
+          } else if (token == '[') {
+              if (state == 'start') {
+                  state = 'configuration';
+              } else if (state == 'name_found') {
+                  state = 'expectations';
+              }
+          } else if (token == ']') {
+              if (state == 'configuration') {
+                  state = 'name';
+              } else if (state == 'expectations') {
+                  state = 'done';
+              }
+          } else if (state == 'configuration') {
+              parsed_modifiers.push(configuration_tokens[token]);
+          } else if (state == 'expectations') {
+              if (token == 'Rebaseline' || token == 'Skip' || token == 'Slow' || token == 'WontFix') {
+                  parsed_modifiers.push(token.toUpperCase());
+              } else {
+                  parsed_expectations.push(expectation_tokens[token]);
+              }
+          } else if (token == '#') {
+              state = 'done';
+          } else if (state == 'name' || state == 'start') {
+              parsed_path = token;
+              state = 'name_found';
+          }
+        });
+
+        if (!parsed_expectations.length) {
+            if (parsed_modifiers.indexOf('Slow') == -1) {
+                parsed_modifiers.push('Skip');
+                parsed_expectations = ['Pass'];
+            }
+        }
+
+        // FIXME: Should we include line number and comment lines here?
+        expectations.push({
+            modifiers: parsed_bugs.concat(parsed_modifiers).join(' '),
+            path: parsed_path,
+            expectations: parsed_expectations.join(' '),
+        });
+    });
+    return expectations;
+}
+
+
+function addTestToAllExpectationsForPlatform(test, platformName, expectations, modifiers)
+{
+    if (!g_allExpectations[test])
+        g_allExpectations[test] = {};
+
+    if (!g_allExpectations[test][platformName])
+        g_allExpectations[test][platformName] = {};
+
+    var allBuildTypes = [];
+    modifiers.split(' ').forEach(function(modifier) {
+        if (modifier in BUILD_TYPES) {
+            allBuildTypes.push(modifier);
+            return;
+        }
+    });
+    if (!allBuildTypes.length)
+        allBuildTypes = Object.keys(BUILD_TYPES);
+
+    allBuildTypes.forEach(function(buildType) {
+        g_allExpectations[test][platformName][buildType] = {modifiers: modifiers, expectations: expectations};
+    });
+}
+
+function processExpectationsForPlatform(platformObject, platformName, expectationsArray)
+{
+    if (!g_allExpectations)
+        g_allExpectations = {};
+
+    if (!expectationsArray)
+        return;
+
+    // Sort the array to hit more specific paths last. More specific
+    // paths (e.g. foo/bar/baz.html) override entries for less-specific ones (e.g. foo/bar).
+    expectationsArray.sort(alphanumericCompare('path'));
+
+    for (var i = 0; i < expectationsArray.length; i++) {
+        var path = expectationsArray[i].path;
+        var modifiers = expectationsArray[i].modifiers;
+        var expectations = expectationsArray[i].expectations;
+
+        var shouldProcessExpectation = false;
+        var hasPlatformModifierUnions = false;
+        if (platformObject.fallbackPlatforms) {
+            platformObject.fallbackPlatforms.forEach(function(fallbackPlatform) {
+                if (shouldProcessExpectation)
+                    return;
+
+                var fallbackPlatformObject = platformObjectForName(fallbackPlatform);
+                if (!fallbackPlatformObject.platformModifierUnions)
+                    return;
+
+                modifiers.split(' ').forEach(function(modifier) {
+                    if (modifier in fallbackPlatformObject.platformModifierUnions) {
+                        hasPlatformModifierUnions = true;
+                        if (fallbackPlatformObject.platformModifierUnions[modifier].indexOf(platformName) != -1)
+                            shouldProcessExpectation = true;
+                    }
+                });
+            });
+        }
+
+        if (!hasPlatformModifierUnions)
+            shouldProcessExpectation = true;
+
+        if (!shouldProcessExpectation)
+            continue;
+
+        getAllTestsTrie().forEach(function(triePath) {
+            addTestToAllExpectationsForPlatform(triePath, platformName, expectations, modifiers);
+        }, path);
+    }
+}
+
+function processExpectations()
+{
+    // FIXME: An expectations-by-platform object should be passed into this function rather than checking
+    // for a global object. That way this function can be better tested and meaningful errors can
+    // be reported when expectations for a given platform are not found in that object.
+    if (!g_expectationsByPlatform)
+        return;
+
+    traversePlatformsTree(function(platform, platformName) {
+        if (platform.fallbackPlatforms) {
+            platform.fallbackPlatforms.forEach(function(fallbackPlatform) {
+                if (fallbackPlatform in g_expectationsByPlatform)
+                    processExpectationsForPlatform(platform, platformName, g_expectationsByPlatform[fallbackPlatform]);
+            });
+        }
+
+        if (platformName in g_expectationsByPlatform)
+            processExpectationsForPlatform(platform, platformName, g_expectationsByPlatform[platformName]);
+    });
+
+    g_expectationsByPlatform = undefined;
+}
+
+function processMissingTestsWithExpectations(builder, platform, buildType)
+{
+    var noFailures = [];
+    var skipped = [];
+
+    var allTestsForPlatformAndBuildType = allTestsWithSamePlatformAndBuildType(platform, buildType);
+    for (var test in g_allExpectations) {
+        var expectations = getExpectations(test, platform, buildType);
+
+        if (!expectations)
+            continue;
+
+        // Test has expectations, but no result in the builders results.
+        // This means it's either SKIP or passes on all builds.
+        if (!allTestsForPlatformAndBuildType[test] && !stringContains(expectations.modifiers, 'WONTFIX')) {
+            if (stringContains(expectations.modifiers, 'SKIP'))
+                skipped.push(test);
+            else if (!expectations.expectations.match(/^\s*PASS\s*$/)) {
+                // Don't show tests expected to always pass. This is used in ways like
+                // the following:
+                // foo/bar = FAIL
+                // foo/bar/baz.html = PASS
+                noFailures.push({test: test, expectations: expectations.expectations, modifiers: expectations.modifiers});
+            }
+        }
+    }
+
+    g_perBuilderSkippedPaths[builder] = skipped.sort();
+    g_perBuilderWithExpectationsButNoFailures[builder] = noFailures.sort();
+}
+
+function processTestResultsForBuilderAsync(builder)
+{
+    setTimeout(function() { processTestRunsForBuilder(builder); }, 0);
+}
+
+function processTestRunsForAllBuilders()
+{
+    for (var builder in g_builders)
+        processTestRunsForBuilder(builder);
+}
+
+function processTestRunsForBuilder(builderName)
+{
+    if (g_perBuilderFailures[builderName])
+      return;
+
+    if (!g_resultsByBuilder[builderName]) {
+        console.error('No tests found for ' + builderName);
+        g_perBuilderFailures[builderName] = [];
+        return;
+    }
+
+    processExpectations();
+    var start = Date.now();
+
+    var buildInfo = platformAndBuildType(builderName);
+    var platform = buildInfo.platform;
+    var buildType = buildInfo.buildType;
+    processMissingTestsWithExpectations(builderName, platform, buildType);
+
+    var failures = [];
+    var allTestsForThisBuilder = g_resultsByBuilder[builderName].tests;
+
+    for (var test in allTestsForThisBuilder) {
+        var resultsForTest = createResultsObjectForTest(test, builderName);
+        populateExpectationsData(resultsForTest);
+
+        var rawTest = g_resultsByBuilder[builderName].tests[test];
+        resultsForTest.rawTimes = rawTest.times;
+        var rawResults = rawTest.results;
+        resultsForTest.rawResults = rawResults;
+
+        // FIXME: Switch to resultsByBuild
+        var times = resultsForTest.rawTimes;
+        var numTimesSeen = 0;
+        var numResultsSeen = 0;
+        var resultsIndex = 0;
+        var currentResult;
+        for (var i = 0; i < times.length; i++) {
+            numTimesSeen += times[i][RLE.LENGTH];
+
+            while (rawResults[resultsIndex] && numTimesSeen > (numResultsSeen + rawResults[resultsIndex][RLE.LENGTH])) {
+                numResultsSeen += rawResults[resultsIndex][RLE.LENGTH];
+                resultsIndex++;
+            }
+
+            if (rawResults && rawResults[resultsIndex])
+                currentResult = rawResults[resultsIndex][RLE.VALUE];
+
+            var time = times[i][RLE.VALUE]
+
+            // Ignore times for crashing/timeout runs for the sake of seeing if
+            // a test should be marked slow.
+            if (currentResult != 'C' && currentResult != 'T')
+                resultsForTest.slowestNonTimeoutCrashTime = Math.max(resultsForTest.slowestNonTimeoutCrashTime, time);
+            resultsForTest.slowestTime = Math.max(resultsForTest.slowestTime, time);
+        }
+
+        processMissingAndExtraExpectations(resultsForTest);
+        failures.push(resultsForTest);
+
+        if (!g_testToResultsMap[test])
+            g_testToResultsMap[test] = [];
+        g_testToResultsMap[test].push(resultsForTest);
+    }
+
+    g_perBuilderFailures[builderName] = failures;
+    logTime('processTestRunsForBuilder: ' + builderName, start);
+}
+
+function processMissingAndExtraExpectations(resultsForTest)
+{
+    // Heuristic for determining whether expectations apply to a given test:
+    // -If a test result happens < MIN_RUNS_FOR_FLAKE, then consider it a flaky
+    // result and include it in the list of expected results.
+    // -Otherwise, grab the first contiguous set of runs with the same result
+    // for >= MIN_RUNS_FOR_FLAKE and ignore all following runs >=
+    // MIN_RUNS_FOR_FLAKE.
+    // This lets us rule out common cases of a test changing expectations for
+    // a few runs, then being fixed or otherwise modified in a non-flaky way.
+    var rawResults = resultsForTest.rawResults;
+
+    // If the first result is no-data that means the test is skipped or is
+    // being run on a different builder (e.g. moved from one shard to another).
+    // Ignore these results since we have no real data about what's going on.
+    if (rawResults[0][RLE.VALUE] == 'N')
+        return;
+
+    // Only consider flake if it doesn't happen twice in a row.
+    var MIN_RUNS_FOR_FLAKE = 2;
+    var resultsMap = {}
+    var numResultsSeen = 0;
+    var haveSeenNonFlakeResult = false;
+    var numRealResults = 0;
+
+    var seenResults = {};
+    for (var i = 0; i < rawResults.length; i++) {
+        var numResults = rawResults[i][RLE.LENGTH];
+        numResultsSeen += numResults;
+
+        var result = rawResults[i][RLE.VALUE];
+
+        var hasMinRuns = numResults >= MIN_RUNS_FOR_FLAKE;
+        if (haveSeenNonFlakeResult && hasMinRuns)
+            continue;
+        else if (hasMinRuns)
+            haveSeenNonFlakeResult = true;
+        else if (!seenResults[result]) {
+            // Only consider a short-lived result if we've seen it more than once.
+            // Otherwise, we include lots of false-positives due to tests that fail
+            // for a couple runs and then start passing.
+            seenResults[result] = true;
+            continue;
+        }
+
+        var expectation = expectationsFileStringForResult(result);
+        resultsMap[expectation] = true;
+        numRealResults++;
+    }
+
+    resultsForTest.flips = i - 1;
+    resultsForTest.isFlaky = numRealResults > 1;
+
+    var missingExpectations = [];
+    var extraExpectations = [];
+
+    if (isLayoutTestResults()) {
+        var expectationsArray = resultsForTest.expectations ? resultsForTest.expectations.split(' ') : [];
+        extraExpectations = expectationsArray.filter(
+            function(element) {
+                // FIXME: Once all the FAIL lines are removed from
+                // TestExpectations, delete all the legacyExpectationsSemantics
+                // code.
+                if (g_currentState.legacyExpectationsSemantics) {
+                    if (element == 'FAIL') {
+                        for (var i = 0; i < FAIL_RESULTS.length; i++) {
+                            if (resultsMap[FAIL_RESULTS[i]])
+                                return false;
+                        }
+                        return true;
+                    }
+                }
+
+                return element && !resultsMap[element] && !stringContains(element, 'BUG');
+            });
+
+        for (var result in resultsMap) {
+            resultsForTest.actualResults.push(result);
+            var hasExpectation = false;
+            for (var i = 0; i < expectationsArray.length; i++) {
+                var expectation = expectationsArray[i];
+                // FIXME: Once all the FAIL lines are removed from
+                // TestExpectations, delete all the legacyExpectationsSemantics
+                // code.
+                if (g_currentState.legacyExpectationsSemantics) {
+                    if (expectation == 'FAIL') {
+                        for (var j = 0; j < FAIL_RESULTS.length; j++) {
+                            if (result == FAIL_RESULTS[j]) {
+                                hasExpectation = true;
+                                break;
+                            }
+                        }
+                    }
+                }
+
+                if (result == expectation)
+                    hasExpectation = true;
+
+                if (hasExpectation)
+                    break;
+            }
+            // If we have no expectations for a test and it only passes, then don't
+            // list PASS as a missing expectation. We only want to list PASS if it
+            // flaky passes, so there would be other expectations.
+            if (!hasExpectation && !(!expectationsArray.length && result == 'PASS' && numRealResults == 1))
+                missingExpectations.push(result);
+        }
+
+        // Only highlight tests that take > 2 seconds as needing to be marked as
+        // slow. There are too many tests that take ~2 seconds every couple
+        // hundred runs. It's not worth the manual maintenance effort.
+        // Also, if a test times out, then it should not be marked as slow.
+        var minTimeForNeedsSlow = isDebug(resultsForTest.builder) ? 2 : 1;
+        if (isSlowTest(resultsForTest) && !resultsMap['TIMEOUT'] && (!resultsForTest.modifiers || !stringContains(resultsForTest.modifiers, 'SLOW')))
+            missingExpectations.push('SLOW');
+        else if (isFastTest(resultsForTest) && resultsForTest.modifiers && stringContains(resultsForTest.modifiers, 'SLOW'))
+            extraExpectations.push('SLOW');
+
+        // If there are no missing results or modifiers besides build
+        // type, platform, or bug and the expectations are all extra
+        // that is, extraExpectations - expectations = PASS,
+        // include PASS as extra, since that means this line in
+        // test_expectations can be deleted..
+        if (!missingExpectations.length && !(resultsForTest.modifiers && realModifiers(resultsForTest.modifiers))) {
+            var extraPlusPass = extraExpectations.concat(['PASS']);
+            if (extraPlusPass.sort().toString() == expectationsArray.slice(0).sort().toString())
+                extraExpectations.push('PASS');
+        }
+
+    }
+
+    resultsForTest.meetsExpectations = !missingExpectations.length && !extraExpectations.length;
+    resultsForTest.missing = missingExpectations.sort().join(' ');
+    resultsForTest.extra = extraExpectations.sort().join(' ');
+}
+
+
+var BUG_URL_PREFIX = '<a href="http://';
+var BUG_URL_POSTFIX = '/$1">crbug.com/$1</a> ';
+var WEBKIT_BUG_URL_POSTFIX = '/$1">webkit.org/b/$1</a> ';
+var INTERNAL_BUG_REPLACE_VALUE = BUG_URL_PREFIX + 'b' + BUG_URL_POSTFIX;
+var EXTERNAL_BUG_REPLACE_VALUE = BUG_URL_PREFIX + 'crbug.com' + BUG_URL_POSTFIX;
+var WEBKIT_BUG_REPLACE_VALUE = BUG_URL_PREFIX + 'webkit.org/b' + WEBKIT_BUG_URL_POSTFIX;
+
+function htmlForBugs(bugs)
+{
+    bugs = bugs.replace(/crbug.com\/(\d+)(\ |$)/g, EXTERNAL_BUG_REPLACE_VALUE);
+    bugs = bugs.replace(/webkit.org\/b\/(\d+)(\ |$)/g, WEBKIT_BUG_REPLACE_VALUE);
+    return bugs;
+}
+
+function linkHTMLToOpenWindow(url, text)
+{
+    return '<a href="' + url + '" target="_blank">' + text + '</a>';
+}
+
+// FIXME: replaced with chromiumRevisionLink/webKitRevisionLink
+function createBlameListHTML(revisions, index, urlBase, separator, repo)
+{
+    var thisRevision = revisions[index];
+    if (!thisRevision)
+        return '';
+
+    var previousRevision = revisions[index + 1];
+    if (previousRevision && previousRevision != thisRevision) {
+        previousRevision++;
+        return linkHTMLToOpenWindow(urlBase + thisRevision + separator + previousRevision,
+            repo + ' blamelist r' + previousRevision + ':r' + thisRevision);
+    } else
+        return 'At ' + repo + ' revision: ' + thisRevision;
+}
+
+// Returns whether the result for index'th result for testName on builder was
+// a failure.
+function isFailure(builder, testName, index)
+{
+    var currentIndex = 0;
+    var rawResults = g_resultsByBuilder[builder].tests[testName].results;
+    for (var i = 0; i < rawResults.length; i++) {
+        currentIndex += rawResults[i][RLE.LENGTH];
+        if (currentIndex > index)
+            return isFailingResult(rawResults[i][RLE.VALUE]);
+    }
+    console.error('Index exceeds number of results: ' + index);
+}
+
+// Returns an array of indexes for all builds where this test failed.
+function indexesForFailures(builder, testName)
+{
+    var rawResults = g_resultsByBuilder[builder].tests[testName].results;
+    var buildNumbers = g_resultsByBuilder[builder].buildNumbers;
+    var index = 0;
+    var failures = [];
+    for (var i = 0; i < rawResults.length; i++) {
+        var numResults = rawResults[i][RLE.LENGTH];
+        if (isFailingResult(rawResults[i][RLE.VALUE])) {
+            for (var j = 0; j < numResults; j++)
+                failures.push(index + j);
+        }
+        index += numResults;
+    }
+    return failures;
+}
+
+// Returns the path to the failure log for this non-webkit test.
+function pathToFailureLog(testName)
+{
+    return '/steps/' + g_crossDashboardState.testType + '/logs/' + testName.split('.')[1]
+}
+
+function showPopupForBuild(e, builder, index, opt_testName)
+{
+    var html = '';
+
+    var time = g_resultsByBuilder[builder].secondsSinceEpoch[index];
+    if (time) {
+        var date = new Date(time * 1000);
+        html += date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
+    }
+
+    var buildNumber = g_resultsByBuilder[builder].buildNumbers[index];
+    var master = builderMaster(builder);
+    var buildBasePath = master.logPath(builder, buildNumber);
+
+    html += '<ul><li>' + linkHTMLToOpenWindow(buildBasePath, 'Build log') +
+        '</li><li>' +
+        createBlameListHTML(g_resultsByBuilder[builder].webkitRevision, index,
+            'http://trac.webkit.org/log/?verbose=on&rev=', '&stop_rev=',
+            'WebKit') +
+        '</li>';
+
+    if (master == WEBKIT_BUILDER_MASTER) {
+        var revision = g_resultsByBuilder[builder].webkitRevision[index];
+        html += '<li><span class=link onclick="setQueryParameter(\'revision\',' +
+            revision + ')">Show results for WebKit r' + revision +
+            '</span></li>';
+    } else {
+        html += '<li>' +
+            createBlameListHTML(g_resultsByBuilder[builder].chromeRevision, index,
+                'http://build.chromium.org/f/chromium/perf/dashboard/ui/changelog.html?url=/trunk/src&mode=html&range=', ':', 'Chrome') +
+            '</li>';
+
+        var chromeRevision = g_resultsByBuilder[builder].chromeRevision[index];
+        if (chromeRevision && isLayoutTestResults()) {
+            html += '<li><a href="' + TEST_RESULTS_BASE_PATH + g_builders[builder] +
+                '/' + chromeRevision + '/layout-test-results.zip">layout-test-results.zip</a></li>';
+        }
+    }
+
+    if (!isLayoutTestResults() && opt_testName && isFailure(builder, opt_testName, index))
+        html += '<li>' + linkHTMLToOpenWindow(buildBasePath + pathToFailureLog(opt_testName), 'Failure log') + '</li>';
+
+    html += '</ul>';
+    showPopup(e.target, html);
+}
+
+function htmlForTestResults(test)
+{
+    var html = '';
+    var results = test.rawResults.concat();
+    var times = test.rawTimes.concat();
+    var builder = test.builder;
+    var master = builderMaster(builder);
+    var buildNumbers = g_resultsByBuilder[builder].buildNumbers;
+
+    var indexToReplaceCurrentResult = -1;
+    var indexToReplaceCurrentTime = -1;
+    var currentResultArray, currentTimeArray, currentResult, innerHTML, resultString;
+    for (var i = 0; i < buildNumbers.length; i++) {
+        if (i > indexToReplaceCurrentResult) {
+            currentResultArray = results.shift();
+            if (currentResultArray) {
+                currentResult = currentResultArray[RLE.VALUE];
+                // Treat simplified diff failures as just text failures.
+                if (currentResult == 'S')
+                    currentResult = 'F';
+                indexToReplaceCurrentResult += currentResultArray[RLE.LENGTH];
+            } else {
+                currentResult = 'N';
+                indexToReplaceCurrentResult += buildNumbers.length;
+            }
+            resultString = expectationsFileStringForResult(currentResult);
+        }
+
+        if (i > indexToReplaceCurrentTime) {
+            currentTimeArray = times.shift();
+            var currentTime = 0;
+            if (currentResultArray) {
+              currentTime = currentTimeArray[RLE.VALUE];
+              indexToReplaceCurrentTime += currentTimeArray[RLE.LENGTH];
+            } else
+              indexToReplaceCurrentTime += buildNumbers.length;
+
+            innerHTML = currentTime || '&nbsp;';
+        }
+
+        var extraClassNames = '';
+        var webkitRevision = g_resultsByBuilder[builder].webkitRevision;
+        var isWebkitMerge = webkitRevision[i + 1] && webkitRevision[i] != webkitRevision[i + 1];
+        if (isWebkitMerge && master != WEBKIT_BUILDER_MASTER)
+            extraClassNames += ' merge';
+
+        html += '<td title="' + (resultString || 'NO DATA') + '. Click for more info." class="results ' + currentResult +
+          extraClassNames + '" onclick=\'showPopupForBuild(event, "' + builder + '",' + i + ',"' + test.test + '")\'>' + innerHTML;
+    }
+    return html;
+}
+
+function htmlForTestsWithExpectationsButNoFailures(builder)
+{
+    var tests = g_perBuilderWithExpectationsButNoFailures[builder];
+    var skippedPaths = g_perBuilderSkippedPaths[builder];
+    var showUnexpectedPassesLink =  linkHTMLToToggleState('showUnexpectedPasses', 'tests that have not failed in last ' + g_resultsByBuilder[builder].buildNumbers.length + ' runs');
+    var showSkippedLink = linkHTMLToToggleState('showSkipped', 'skipped tests in TestExpectations');
+    
+
+    var html = '';
+    if (tests.length || skippedPaths.length) {
+        var buildInfo = platformAndBuildType(builder);
+        html += '<h2 style="display:inline-block">Expectations for ' + buildInfo.platform + '-' + buildInfo.buildType + '</h2> ';
+        if (!g_currentState.showUnexpectedPasses && tests.length)
+            html += showUnexpectedPassesLink;
+        html += ' ';
+        if (!g_currentState.showSkipped && skippedPaths.length)
+            html += showSkippedLink;
+    }
+
+    var open = '<div onclick="selectContents(this)">';
+
+    if (g_currentState.showUnexpectedPasses && tests.length) {
+        html += '<div id="passing-tests">' + showUnexpectedPassesLink;
+        for (var i = 0; i < tests.length; i++)
+            html += open + tests[i].test + '</div>';
+        html += '</div>';
+    }
+
+    if (g_currentState.showSkipped && skippedPaths.length)
+        html += '<div id="skipped-tests">' + showSkippedLink + open + skippedPaths.join('</div>' + open) + '</div></div>';
+    return html + '<br>';
+}
+
+// Returns whether we should exclude test results from the test table.
+function shouldHideTest(testResult)
+{
+    if (testResult.isWontFixSkip)
+        return !g_currentState.showWontFixSkip;
+
+    if (testResult.isFlaky)
+        return !g_currentState.showFlaky;
+
+    if (isSlowTest(testResult))
+        return !g_currentState.showSlow;
+
+    if (testResult.meetsExpectations)
+        return !g_currentState.showCorrectExpectations;
+
+    return !g_currentState.showWrongExpectations;
+}
+
+// Sets the browser's selection to the element's contents.
+function selectContents(element)
+{
+    window.getSelection().selectAllChildren(element);
+}
+
+function createBugHTML(test)
+{
+    var symptom = test.isFlaky ? 'flaky' : 'failing';
+    var title = encodeURIComponent('Layout Test ' + test.test + ' is ' + symptom);
+    var description = encodeURIComponent('The following layout test is ' + symptom + ' on ' +
+        '[insert platform]\n\n' + test.test + '\n\nProbable cause:\n\n' +
+        '[insert probable cause]');
+    
+    var component = encodeURIComponent('Tools / Tests');
+    url = 'https://bugs.webkit.org/enter_bug.cgi?assigned_to=webkit-unassigned%40lists.webkit.org&product=WebKit&form_name=enter_bug&component=' + component + '&short_desc=' + title + '&comment=' + description;
+    return '<a href="' + url + '" class="file-bug">FILE BUG</a>';
+}
+
+function isCrossBuilderView()
+{
+    return g_currentState.tests || g_currentState.result || g_currentState.expectationsUpdate;
+}
+
+function tableHeaders(opt_getAll)
+{
+    var headers = [];
+    if (isCrossBuilderView() || opt_getAll)
+        headers.push('builder');
+
+    if (!isCrossBuilderView() || opt_getAll)
+        headers.push('test');
+
+    if (isLayoutTestResults() || opt_getAll)
+        headers.push('bugs', 'modifiers', 'expectations');
+
+    headers.push('slowest run', 'flakiness (numbers are runtimes in seconds)');
+    return headers;
+}
+
+function htmlForSingleTestRow(test)
+{
+    if (!isCrossBuilderView() && shouldHideTest(test)) {
+        // The innerHTML call is considerably faster if we exclude the rows for
+        // items we're not showing than if we hide them using display:none.
+        // For the crossBuilderView, we want to show all rows the user is
+        // explicitly listing tests to view.
+        return '';
+    }
+
+    var headers = tableHeaders();
+    var html = '';
+    for (var i = 0; i < headers.length; i++) {
+        var header = headers[i];
+        if (startsWith(header, 'test') || startsWith(header, 'builder')) {
+            // If isCrossBuilderView() is true, we're just viewing a single test
+            // with results for many builders, so the first column is builder names
+            // instead of test paths.
+            var testCellClassName = 'test-link' + (isCrossBuilderView() ? ' builder-name' : '');
+            var testCellHTML = isCrossBuilderView() ? test.builder : '<span class="link" onclick="setQueryParameter(\'tests\',\'' + test.test +'\');">' + test.test + '</span>';
+
+            html += '<tr><td class="' + testCellClassName + '">' + testCellHTML;
+        } else if (startsWith(header, 'bugs'))
+            html += '<td class=options-container>' + (test.bugs ? htmlForBugs(test.bugs) : createBugHTML(test));
+        else if (startsWith(header, 'modifiers'))
+            html += '<td class=options-container>' + test.modifiers;
+        else if (startsWith(header, 'expectations'))
+            html += '<td class=options-container>' + test.expectations;
+        else if (startsWith(header, 'slowest'))
+            html += '<td>' + (test.slowestTime ? test.slowestTime + 's' : '');
+        else if (startsWith(header, 'flakiness'))
+            html += htmlForTestResults(test);
+    }
+    return html;
+}
+
+function sortColumnFromTableHeader(headerText)
+{
+    return headerText.split(' ', 1)[0];
+}
+
+function htmlForTableColumnHeader(headerName, opt_fillColSpan)
+{
+    // Use the first word of the header title as the sortkey
+    var thisSortValue = sortColumnFromTableHeader(headerName);
+    var arrowHTML = thisSortValue == g_currentState.sortColumn ?
+        '<span class=' + g_currentState.sortOrder + '>' + (g_currentState.sortOrder == FORWARD ? '&uarr;' : '&darr;' ) + '</span>' : '';
+    return '<th sortValue=' + thisSortValue +
+        // Extend last th through all the rest of the columns.
+        (opt_fillColSpan ? ' colspan=10000' : '') +
+        // Extra span here is so flex boxing actually centers.
+        // There's probably a better way to do this with CSS only though.
+        '><div class=table-header-content><span></span>' + arrowHTML +
+        '<span class=header-text>' + headerName + '</span>' + arrowHTML + '</div></th>';
+}
+
+function htmlForTestTable(rowsHTML, opt_excludeHeaders)
+{
+    var html = '<table class=test-table>';
+    if (!opt_excludeHeaders) {
+        html += '<thead><tr>';
+        var headers = tableHeaders();
+        for (var i = 0; i < headers.length; i++)
+            html += htmlForTableColumnHeader(headers[i], i == headers.length - 1);
+        html += '</tr></thead>';
+    }
+    return html + '<tbody>' + rowsHTML + '</tbody></table>';
+}
+
+function appendHTML(html)
+{
+    var startTime = Date.now();
+    // InnerHTML to a div that's not in the document. This is
+    // ~300ms faster in Safari 4 and Chrome 4 on mac.
+    var div = document.createElement('div');
+    div.innerHTML = html;
+    document.body.appendChild(div);
+    logTime('Time to innerHTML', startTime);
+    postHeightChangedMessage();
+}
+
+function alphanumericCompare(column, reverse)
+{
+    return reversibleCompareFunction(function(a, b) {
+        // Put null entries at the bottom
+        var a = a[column] ? String(a[column]) : 'z';
+        var b = b[column] ? String(b[column]) : 'z';
+
+        if (a < b)
+            return -1;
+        else if (a == b)
+            return 0;
+        else
+            return 1;
+    }, reverse);
+}
+
+function numericSort(column, reverse)
+{
+    return reversibleCompareFunction(function(a, b) {
+        a = parseFloat(a[column]);
+        b = parseFloat(b[column]);
+        return a - b;
+    }, reverse);
+}
+
+function reversibleCompareFunction(compare, reverse)
+{
+    return function(a, b) {
+        return compare(reverse ? b : a, reverse ? a : b);
+    };
+}
+
+function changeSort(e)
+{
+    var target = e.currentTarget;
+    e.preventDefault();
+
+    var sortValue = target.getAttribute('sortValue');
+    while (target && target.tagName != 'TABLE')
+        target = target.parentNode;
+
+    var sort = 'sortColumn';
+    var orderKey = 'sortOrder';
+    if (sortValue == g_currentState[sort] && g_currentState[orderKey] == FORWARD)
+        order = BACKWARD;
+    else
+        order = FORWARD;
+
+    setQueryParameter(sort, sortValue, orderKey, order);
+}
+
+function sortTests(tests, column, order)
+{
+    var resultsProperty, sortFunctionGetter;
+    if (column == 'flakiness') {
+        sortFunctionGetter = numericSort;
+        resultsProperty = 'flips';
+    } else if (column == 'slowest') {
+        sortFunctionGetter = numericSort;
+        resultsProperty = 'slowestTime';
+    } else {
+        sortFunctionGetter = alphanumericCompare;
+        resultsProperty = column;
+    }
+
+    tests.sort(sortFunctionGetter(resultsProperty, order == BACKWARD));
+}
+
+// Sorts a space separated expectations string in alphanumeric order.
+// @param {string} str The expectations string.
+// @return {string} The sorted string.
+function sortExpectationsString(str)
+{
+    return str.split(' ').sort().join(' ');
+}
+
+function addUpdate(testsNeedingUpdate, test, builderName, missing, extra)
+{
+    if (!testsNeedingUpdate[test])
+        testsNeedingUpdate[test] = {};
+
+    var buildInfo = platformAndBuildType(builderName);
+    var builder = buildInfo.platform + ' ' + buildInfo.buildType;
+    if (!testsNeedingUpdate[test][builder])
+        testsNeedingUpdate[test][builder] = {};
+
+    if (missing)
+        testsNeedingUpdate[test][builder].missing = sortExpectationsString(missing);
+
+    if (extra)
+        testsNeedingUpdate[test][builder].extra = sortExpectationsString(extra);
+}
+
+
+// From a string of modifiers, returns a string of modifiers that
+// are for real result changes, like SLOW, and excludes modifiers
+// that specificy things like platform, build_type, bug.
+// @param {string} modifierString String containing all modifiers.
+// @return {string} String containing only modifiers that effect the results.
+function realModifiers(modifierString)
+{
+    var modifiers = modifierString.split(' ');;
+    return modifiers.filter(function(modifier) {
+        if (modifier in BUILD_TYPES || startsWith(modifier, 'BUG'))
+            return false;
+
+        var matchesPlatformOrUnion = false;
+        traversePlatformsTree(function(platform, platformName) {
+            if (matchesPlatformOrUnion)
+                return;
+
+            if (platform.fallbackPlatforms) {
+                platform.fallbackPlatforms.forEach(function(fallbackPlatform) {
+                    if (matchesPlatformOrUnion)
+                        return;
+
+                    var fallbackPlatformObject = platformObjectForName(fallbackPlatform);
+                    if (!fallbackPlatformObject.platformModifierUnions)
+                        return;
+
+                    matchesPlatformOrUnion = modifier in fallbackPlatformObject.subPlatforms || modifier in fallbackPlatformObject.platformModifierUnions;
+                });
+            }
+        });
+
+        return !matchesPlatformOrUnion;
+    }).join(' ');
+}
+
+function generatePageForExpectationsUpdate()
+{
+    // Always show all runs when auto-updating expectations.
+    if (!g_crossDashboardState.showAllRuns)
+        setQueryParameter('showAllRuns', true);
+
+    processTestRunsForAllBuilders();
+    var testsNeedingUpdate = {};
+    for (var test in g_testToResultsMap) {
+        var results = g_testToResultsMap[test];
+        for (var i = 0; i < results.length; i++) {
+            var thisResult = results[i];
+            
+            if (!thisResult.missing && !thisResult.extra)
+                continue;
+
+            var allPassesOrNoDatas = thisResult.rawResults.filter(function (x) { return x[1] != "P" && x[1] != "N"; }).length == 0;
+
+            if (allPassesOrNoDatas)
+                continue;
+
+            addUpdate(testsNeedingUpdate, test, thisResult.builder, thisResult.missing, thisResult.extra);
+        }
+    }
+
+    for (var builder in g_builders) {
+        var tests = g_perBuilderWithExpectationsButNoFailures[builder]
+        for (var i = 0; i < tests.length; i++) {
+            // Anything extra in this case is what is listed in expectations
+            // plus modifiers other than bug, platform, build type.
+            var modifiers = realModifiers(tests[i].modifiers);
+            var extras = tests[i].expectations;
+            extras += modifiers ? ' ' + modifiers : '';
+            addUpdate(testsNeedingUpdate, tests[i].test, builder, null, extras);
+        }
+    }
+
+    // Get the keys in alphabetical order, so it is easy to process groups
+    // of tests.
+    var keys = Object.keys(testsNeedingUpdate).sort();
+    showUpdateInfoForTest(testsNeedingUpdate, keys);
+}
+
+// Show the test results and the json for differing expectations, and
+// allow the user to include or exclude this update.
+//
+// @param {Object} testsNeedingUpdate Tests that need updating.
+// @param {Array.<string>} keys Keys into the testNeedingUpdate object.
+function showUpdateInfoForTest(testsNeedingUpdate, keys)
+{
+    var test = keys[g_currentState.updateIndex];
+    document.body.innerHTML = '';
+
+    // FIXME: Make this DOM creation less verbose.
+    var index = document.createElement('div');
+    index.style.cssFloat = 'right';
+    index.textContent = (g_currentState.updateIndex + 1) + ' of ' + keys.length + ' tests';
+    document.body.appendChild(index);
+
+    var buttonRegion = document.createElement('div');
+    var includeBtn = document.createElement('input');
+    includeBtn.type = 'button';
+    includeBtn.value = 'include selected';
+    includeBtn.addEventListener('click', partial(handleUpdate, testsNeedingUpdate, keys), false);
+    buttonRegion.appendChild(includeBtn);
+
+    var previousBtn = document.createElement('input');
+    previousBtn.type = 'button';
+    previousBtn.value = 'previous';
+    previousBtn.addEventListener('click',
+        function() {
+          setUpdateIndex(g_currentState.updateIndex - 1, testsNeedingUpdate, keys);
+        },
+        false);
+    buttonRegion.appendChild(previousBtn);
+
+    var nextBtn = document.createElement('input');
+    nextBtn.type = 'button';
+    nextBtn.value = 'next';
+    nextBtn.addEventListener('click', partial(nextUpdate, testsNeedingUpdate, keys), false);
+    buttonRegion.appendChild(nextBtn);
+
+    var doneBtn = document.createElement('input');
+    doneBtn.type = 'button';
+    doneBtn.value = 'done';
+    doneBtn.addEventListener('click', finishUpdate, false);
+    buttonRegion.appendChild(doneBtn);
+
+    document.body.appendChild(buttonRegion);
+
+    var updates = testsNeedingUpdate[test];
+    var checkboxes = document.createElement('div');
+    for (var builder in updates) {
+        // Create a checkbox for each builder.
+        var checkboxRegion = document.createElement('div');
+        var checkbox = document.createElement('input');
+        checkbox.type = 'checkbox';
+        checkbox.id = builder;
+        checkbox.checked = true;
+        checkboxRegion.appendChild(checkbox);
+        checkboxRegion.appendChild(document.createTextNode(builder + ' : ' + JSON.stringify(updates[builder])));
+        checkboxes.appendChild(checkboxRegion);
+    }
+    document.body.appendChild(checkboxes);
+
+    var div = document.createElement('div');
+    div.innerHTML = htmlForIndividualTestOnAllBuildersWithResultsLinks(test);
+    document.body.appendChild(div);
+    appendExpectations();
+}
+
+
+// When the user has finished selecting expectations to update, provide them
+// with json to copy over.
+function finishUpdate()
+{
+    document.body.innerHTML = 'The next step is to copy the output below ' +
+        'into a local file and save it.  Then, run<br><code>python ' +
+        'src/webkit/tools/layout_tests/webkitpy/layout_tests/update_expectat' +
+        'ions_from_dashboard.py path/to/local/file</code><br>in order to ' +
+        'update the expectations file.<br><textarea id="results" '+
+        'style="width:600px;height:600px;"> ' +
+        JSON.stringify(g_confirmedTests) + '</textarea>';
+    results.focus();
+    document.execCommand('SelectAll');
+}
+
+// Handle user click on "include selected" button.
+// Includes the tests that are selected and exclude the rest.
+// @param {Object} testsNeedingUpdate Tests that need updating.
+// @param {Array.<string>} keys Keys into the testNeedingUpdate object.
+function handleUpdate(testsNeedingUpdate, keys)
+{
+    var test = keys[g_currentState.updateIndex];
+    var updates = testsNeedingUpdate[test];
+    for (var builder in updates) {
+        // Add included tests, and delete excluded tests if
+        // they were previously included.
+        if ($(builder).checked) {
+            if (!g_confirmedTests[test])
+                g_confirmedTests[test] = {};
+            g_confirmedTests[test][builder] = testsNeedingUpdate[test][builder];
+        } else if (g_confirmedTests[test] && g_confirmedTests[test][builder]) {
+            delete g_confirmedTests[test][builder];
+            if (!Object.keys(g_confirmedTests[test]).length)
+                delete g_confirmedTests[test];
+        }
+    }
+    nextUpdate(testsNeedingUpdate, keys);
+}
+
+
+// Move to the next item to update.
+// @param {Object} testsNeedingUpdate Tests that need updating.
+// @param {Array.<string>} keys Keys into the testNeedingUpdate object.
+function nextUpdate(testsNeedingUpdate, keys)
+{
+    setUpdateIndex(g_currentState.updateIndex + 1, testsNeedingUpdate, keys);
+}
+
+
+// Advance the index we are updating at.  If we walk over the end
+// or beginning, just loop.
+// @param {string} newIndex The index into the keys to move to.
+// @param {Object} testsNeedingUpdate Tests that need updating.
+// @param {Array.<string>} keys Keys into the testNeedingUpdate object.
+function setUpdateIndex(newIndex, testsNeedingUpdate, keys)
+{
+    if (newIndex == -1)
+        newIndex = keys.length - 1;
+    else if (newIndex == keys.length)
+        newIndex = 0;
+    setQueryParameter("updateIndex", newIndex);
+    showUpdateInfoForTest(testsNeedingUpdate, keys);
+}
+
+function htmlForIndividualTestOnAllBuilders(test)
+{
+    processTestRunsForAllBuilders();
+
+    var testResults = g_testToResultsMap[test];
+    if (!testResults)
+        return '<div class="not-found">Test not found. Either it does not exist, is skipped or passes on all platforms.</div>';
+        
+    var html = '';
+    var shownBuilders = [];
+    for (var j = 0; j < testResults.length; j++) {
+        shownBuilders.push(testResults[j].builder);
+        html += htmlForSingleTestRow(testResults[j]);
+    }
+
+    var skippedBuilders = []
+    for (builder in currentBuilderGroup().builders) {
+        if (shownBuilders.indexOf(builder) == -1)
+            skippedBuilders.push(builder);
+    }
+
+    var skippedBuildersHtml = '';
+    if (skippedBuilders.length) {
+        skippedBuildersHtml = '<div>The following builders either don\'t run this test (e.g. it\'s skipped) or all runs passed:</div>' +
+            '<div class=skipped-builder-list><div class=skipped-builder>' + skippedBuilders.join('</div><div class=skipped-builder>') + '</div></div>';
+    }
+
+    return htmlForTestTable(html) + skippedBuildersHtml;
+}
+
+function htmlForIndividualTestOnAllBuildersWithResultsLinks(test)
+{
+    processTestRunsForAllBuilders();
+
+    var testResults = g_testToResultsMap[test];
+    var html = '';
+    html += htmlForIndividualTestOnAllBuilders(test);
+
+    html += '<div class=expectations test=' + test + '><div>' +
+        linkHTMLToToggleState('showExpectations', 'results')
+
+    if (isLayoutTestResults() || isGPUTestResults()) {
+        if (isLayoutTestResults())
+            html += ' | ' + linkHTMLToToggleState('showLargeExpectations', 'large thumbnails');
+        if (testResults && builderMaster(testResults[0].builder) == WEBKIT_BUILDER_MASTER) {
+            var revision = g_currentState.revision || '';
+            html += '<form onsubmit="setQueryParameter(\'revision\', revision.value);' +
+                'return false;">Show results for WebKit revision: ' +
+                '<input name=revision placeholder="e.g. 65540" value="' + revision +
+                '" id=revision-input></form>';
+        } else
+            html += ' | <b>Only shows actual results/diffs from the most recent *failure* on each bot.</b>';
+    } else {
+      html += ' | <span>Results height:<input ' +
+          'onchange="setQueryParameter(\'resultsHeight\',this.value)" value="' +
+          g_currentState.resultsHeight + '" style="width:2.5em">px</span>';
+    }
+    html += '</div></div>';
+    return html;
+}
+
+function getExpectationsContainer(expectationsContainers, parentContainer, expectationsType)
+{
+    if (!expectationsContainers[expectationsType]) {
+        var container = document.createElement('div');
+        container.className = 'expectations-container';
+        parentContainer.appendChild(container);
+        expectationsContainers[expectationsType] = container;
+    }
+    return expectationsContainers[expectationsType];
+}
+
+function ensureTrailingSlash(path)
+{
+    if (path.match(/\/$/))
+        return path;
+    return path + '/';
+}
+
+function maybeAddPngChecksum(expectationDiv, pngUrl)
+{
+    // pngUrl gets served from the browser cache since we just loaded it in an
+    // <img> tag.
+    loader.request(pngUrl,
+        function(xhr) {
+            // Convert the first 2k of the response to a byte string.
+            var bytes = xhr.responseText.substring(0, 2048);
+            for (var position = 0; position < bytes.length; ++position)
+                bytes[position] = bytes[position] & 0xff;
+
+            // Look for the comment.
+            var commentKey = 'tEXtchecksum\x00';
+            var checksumPosition = bytes.indexOf(commentKey);
+            if (checksumPosition == -1)
+                return;
+
+            var checksum = bytes.substring(checksumPosition + commentKey.length, checksumPosition + commentKey.length + 32);
+            var checksumContainer = document.createElement('span');
+            checksumContainer.innerText = 'Embedded checksum: ' + checksum;
+            checksumContainer.setAttribute('class', 'pngchecksum');
+            expectationDiv.parentNode.appendChild(checksumContainer);
+        },
+        function(xhr) {},
+        true);
+}
+
+// Adds a specific expectation. If it's an image, it's only added on the
+// image's onload handler. If it's a text file, then a script tag is appended
+// as a hack to see if the file 404s (necessary since it's cross-domain).
+// Once all the expectations for a specific type have loaded or errored
+// (e.g. all the text results), then we go through and identify which platform
+// uses which expectation.
+//
+// @param {Object} expectationsContainers Map from expectations type to
+//     container DIV.
+// @param {Element} parentContainer Container element for
+//     expectationsContainer divs.
+// @param {string} platform Platform string. Empty string for non-platform
+//     specific expectations.
+// @param {string} path Relative path to the expectation.
+// @param {string} base Base path for the expectation URL.
+// @param {string} opt_builder Builder whose actual results this expectation
+//     points to.
+// @param {string} opt_suite "virtual suite" that the test belongs to, if any.
+function addExpectationItem(expectationsContainers, parentContainer, platform, path, base, opt_builder, opt_suite)
+{
+    var parts = path.split('.')
+    var fileExtension = parts[parts.length - 1];
+    if (fileExtension == 'html')
+        fileExtension = 'txt';
+    
+    var container = getExpectationsContainer(expectationsContainers, parentContainer, fileExtension);
+    var isImage = path.match(/\.png$/);
+
+    // FIXME: Stop using script tags once all the places we pull from support CORS.
+    var platformPart = platform ? ensureTrailingSlash(platform) : '';
+    var suitePart = opt_suite ? ensureTrailingSlash(opt_suite) : '';
+
+    var childContainer = document.createElement('span');
+    childContainer.className = 'unloaded';
+
+    var appendExpectationsItem = function(item) {
+        childContainer.appendChild(expectationsTitle(platformPart + suitePart, path, opt_builder));
+        childContainer.className = 'expectations-item';
+        item.className = 'expectation ' + fileExtension;
+        if (g_currentState.showLargeExpectations)
+            item.className += ' large';
+        childContainer.appendChild(item);
+        handleFinishedLoadingExpectations(container);
+    };
+
+    var url = base + platformPart + path;
+    if (isImage || !startsWith(base, 'http://svn.webkit.org')) {
+        var dummyNode = document.createElement(isImage ? 'img' : 'script');
+        dummyNode.src = url;
+        dummyNode.onload = function() {
+            var item;
+            if (isImage) {
+                item = dummyNode;
+                if (startsWith(base, 'http://svn.webkit.org'))
+                    maybeAddPngChecksum(item, url);
+            } else {
+                item = document.createElement('iframe');
+                item.src = url;
+            }
+            appendExpectationsItem(item);
+        }
+        dummyNode.onerror = function() {
+            childContainer.parentNode.removeChild(childContainer);
+            handleFinishedLoadingExpectations(container);
+        }
+
+        // Append script elements now so that they load. Images load without being
+        // appended to the DOM.
+        if (!isImage)
+            childContainer.appendChild(dummyNode);
+    } else {
+        loader.request(url,
+            function(xhr) {
+                var item = document.createElement('pre');
+                item.innerText = xhr.responseText;
+                appendExpectationsItem(item);
+            },
+            function(xhr) {/* Do nothing on errors since they're expected */});
+    }
+
+    container.appendChild(childContainer);
+}
+
+
+// Identifies which expectations are used on which platform once all the
+// expectations of a given type have loaded (e.g. the container for png
+// expectations for this test had no child elements with the class
+// "unloaded").
+//
+// @param {string} container Element containing the expectations for a given
+//     test and a given type (e.g. png).
+function handleFinishedLoadingExpectations(container)
+{
+    if (container.getElementsByClassName('unloaded').length)
+        return;
+
+    var titles = container.getElementsByClassName('expectations-title');
+    for (var platform in g_fallbacksMap) {
+        var fallbacks = g_fallbacksMap[platform];
+        var winner = null;
+        var winningIndex = -1;
+        for (var i = 0; i < titles.length; i++) {
+            var title = titles[i];
+
+            if (!winner && title.platform == "") {
+                winner = title;
+                continue;
+            }
+
+            var rawPlatform = title.platform && title.platform.replace('platform/', '');
+            for (var j = 0; j < fallbacks.length; j++) {
+                if ((winningIndex == -1 || winningIndex > j) && rawPlatform == fallbacks[j]) {
+                    winningIndex = j;
+                    winner = title;
+                    break;
+                }
+            }
+        }
+        if (winner)
+            winner.getElementsByClassName('platforms')[0].innerHTML += '<div class=used-platform>' + platform + '</div>';
+        else {
+            console.log('No expectations identified for this test. This means ' +
+                'there is a logic bug in the dashboard for which expectations a ' +
+                'platform uses or trac.webkit.org/src.chromium.org is giving 5XXs.');
+        }
+    }
+
+    consolidateUsedPlatforms(container);
+}
+
+// Consolidate platforms when all sub-platforms for a given platform are represented.
+// e.g., if all of the WIN- platforms are there, replace them with just WIN.
+function consolidateUsedPlatforms(container)
+{
+    var allPlatforms = Object.keys(g_fallbacksMap);
+
+    var platformElements = container.getElementsByClassName('platforms');
+    for (var i = 0, platformsLength = platformElements.length; i < platformsLength; i++) {
+        var usedPlatforms = platformElements[i].getElementsByClassName('used-platform');
+        if (!usedPlatforms.length)
+            continue;
+
+        var platforms = {};
+        platforms['MAC'] = {};
+        platforms['WIN'] = {};
+        platforms['LINUX'] = {};
+        allPlatforms.forEach(function(platform) {
+            if (startsWith(platform, 'MAC'))
+                platforms['MAC'][platform] = 1;
+            else if (startsWith(platform, 'WIN'))
+                platforms['WIN'][platform] = 1;
+            else if (startsWith(platform, 'LINUX'))
+                platforms['LINUX'][platform] = 1;
+        });
+
+        for (var j = 0, usedPlatformsLength = usedPlatforms.length; j < usedPlatformsLength; j++) {
+            for (var platform in platforms)
+                delete platforms[platform][usedPlatforms[j].textContent];
+        }
+
+        for (var platform in platforms) {
+            if (!Object.keys(platforms[platform]).length) {
+                var nodesToRemove = [];
+                for (var j = 0, usedPlatformsLength = usedPlatforms.length; j < usedPlatformsLength; j++) {
+                    var usedPlatform = usedPlatforms[j];
+                    if (startsWith(usedPlatform.textContent, platform))
+                        nodesToRemove.push(usedPlatform);
+                }
+
+                nodesToRemove.forEach(function(element) { element.parentNode.removeChild(element); });
+                platformElements[i].insertAdjacentHTML('afterBegin', '<div class=used-platform>' + platform + '</div>');
+            }
+        }
+    }
+}
+
+function addExpectations(expectationsContainers, container, base,
+    platform, text, png, reftest_html_file, reftest_mismatch_html_file, suite)
+{
+    var builder = '';
+    addExpectationItem(expectationsContainers, container, platform, text, base, builder, suite);
+    addExpectationItem(expectationsContainers, container, platform, png, base, builder, suite);
+    addExpectationItem(expectationsContainers, container, platform, reftest_html_file, base, builder, suite);
+    addExpectationItem(expectationsContainers, container, platform, reftest_mismatch_html_file, base, builder, suite);
+}
+
+function expectationsTitle(platform, path, builder)
+{
+    var header = document.createElement('h3');
+    header.className = 'expectations-title';
+
+    var innerHTML;
+    if (builder) {
+        var resultsType;
+        if (endsWith(path, '-crash-log.txt'))
+            resultsType = 'STACKTRACE';
+        else if (endsWith(path, '-actual.txt') || endsWith(path, '-actual.png'))
+            resultsType = 'ACTUAL RESULTS';
+        else if (endsWith(path, '-wdiff.html'))
+            resultsType = 'WDIFF';
+        else
+            resultsType = 'DIFF';
+
+        innerHTML = resultsType + ': ' + builder;
+    } else if (platform === "") {
+        var parts = path.split('/');
+        innerHTML = parts[parts.length - 1];
+    } else
+        innerHTML = platform || path;
+
+    header.innerHTML = '<div class=title>' + innerHTML +
+        '</div><div style="float:left">&nbsp;</div>' +
+        '<div class=platforms style="float:right"></div>';
+    header.platform = platform;
+    return header;
+}
+
+function loadExpectations(expectationsContainer)
+{
+    var test = expectationsContainer.getAttribute('test');
+    if (isLayoutTestResults())
+        loadExpectationsLayoutTests(test, expectationsContainer);
+    else {
+        var results = g_testToResultsMap[test];
+        for (var i = 0; i < results.length; i++)
+            if (isGPUTestResults())
+                loadGPUResultsForBuilder(results[i].builder, test, expectationsContainer);
+            else
+                loadNonWebKitResultsForBuilder(results[i].builder, test, expectationsContainer);
+    }
+}
+
+function gpuResultsPath(chromeRevision, builder)
+{
+  return chromeRevision + '_' + builder.replace(/[^A-Za-z0-9]+/g, '_');
+}
+
+function loadGPUResultsForBuilder(builder, test, expectationsContainer)
+{
+    var container = document.createElement('div');
+    container.className = 'expectations-container';
+    container.innerHTML = '<div><b>' + builder + '</b></div>';
+    expectationsContainer.appendChild(container);
+
+    var failureIndex = indexesForFailures(builder, test)[0];
+
+    var buildNumber = g_resultsByBuilder[builder].buildNumbers[failureIndex];
+    var pathToLog = builderMaster(builder).logPath(builder, buildNumber) + pathToFailureLog(test);
+
+    var chromeRevision = g_resultsByBuilder[builder].chromeRevision[failureIndex];
+    var resultsUrl = GPU_RESULTS_BASE_PATH + gpuResultsPath(chromeRevision, builder);
+    var filename = test.split(/\./)[1] + '.png';
+
+    appendNonWebKitResults(container, pathToLog, 'non-webkit-results');
+    appendNonWebKitResults(container, resultsUrl + '/gen/' + filename, 'gpu-test-results', 'Generated');
+    appendNonWebKitResults(container, resultsUrl + '/ref/' + filename, 'gpu-test-results', 'Reference');
+    appendNonWebKitResults(container, resultsUrl + '/diff/' + filename, 'gpu-test-results', 'Diff');
+}
+
+function loadNonWebKitResultsForBuilder(builder, test, expectationsContainer)
+{
+    var failureIndexes = indexesForFailures(builder, test);
+    var container = document.createElement('div');
+    container.innerHTML = '<div><b>' + builder + '</b></div>';
+    expectationsContainer.appendChild(container);
+    for (var i = 0; i < failureIndexes.length; i++) {
+        // FIXME: This doesn't seem to work anymore. Did the paths change?
+        // Once that's resolved, see if we need to try each GTEST_MODIFIERS prefix as well.
+        var buildNumber = g_resultsByBuilder[builder].buildNumbers[failureIndexes[i]];
+        var pathToLog = builderMaster(builder).logPath(builder, buildNumber) + pathToFailureLog(test);
+        appendNonWebKitResults(container, pathToLog, 'non-webkit-results');
+    }
+}
+
+function appendNonWebKitResults(container, url, itemClassName, opt_title)
+{
+    // Use a script tag to detect whether the URL 404s.
+    // Need to use a script tag since the URL is cross-domain.
+    var dummyNode = document.createElement('script');
+    dummyNode.src = url;
+
+    dummyNode.onload = function() {
+        var item = document.createElement('iframe');
+        item.src = dummyNode.src;
+        item.className = itemClassName;
+        item.style.height = g_currentState.resultsHeight + 'px';
+
+        if (opt_title) {
+            var childContainer = document.createElement('div');
+            childContainer.style.display = 'inline-block';
+            var title = document.createElement('div');
+            title.textContent = opt_title;
+            childContainer.appendChild(title);
+            childContainer.appendChild(item);
+            container.replaceChild(childContainer, dummyNode);
+        } else
+            container.replaceChild(item, dummyNode);
+    }
+    dummyNode.onerror = function() {
+        container.removeChild(dummyNode);
+    }
+
+    container.appendChild(dummyNode);
+}
+
+function buildInfoForRevision(builder, revision)
+{
+    var revisions = g_resultsByBuilder[builder].webkitRevision;
+    var revisionStart = 0, revisionEnd = 0, buildNumber = 0;
+    for (var i = 0; i < revisions.length; i++) {
+        if (revision > revisions[i]) {
+            revisionStart = revisions[i - 1];
+            revisionEnd = revisions[i];
+            buildNumber = g_resultsByBuilder[builder].buildNumbers[i - 1];
+            break;
+        }
+    }
+
+    if (revisionEnd)
+      revisionEnd++;
+    else
+      revisionEnd = '';
+
+    return {revisionStart: revisionStart, revisionEnd: revisionEnd, buildNumber: buildNumber};
+}
+
+function lookupVirtualTestSuite(test) {
+    for (var suite in VIRTUAL_SUITES) {
+        if (test.indexOf(suite) != -1)
+            return suite;
+    }
+    return '';
+}
+
+function baseTest(test, suite) {
+    base = VIRTUAL_SUITES[suite];
+    return base ? test.replace(suite, base) : test;
+}
+
+function loadBaselinesForTest(expectationsContainers, expectationsContainer, test) {
+    var testWithoutSuffix = test.substring(0, test.lastIndexOf('.'));
+    var text = testWithoutSuffix + "-expected.txt";
+    var png = testWithoutSuffix + "-expected.png";
+    var reftest_html_file = testWithoutSuffix + "-expected.html";
+    var reftest_mismatch_html_file = testWithoutSuffix + "-expected-mismatch.html";
+    var suite = lookupVirtualTestSuite(test);
+
+    if (!suite)
+        addExpectationItem(expectationsContainers, expectationsContainer, null, test, TEST_URL_BASE_PATH);
+
+    addExpectations(expectationsContainers, expectationsContainer,
+        TEST_URL_BASE_PATH, '', text, png, reftest_html_file, reftest_mismatch_html_file, suite);
+
+    var fallbacks = allFallbacks();
+    for (var i = 0; i < fallbacks.length; i++) {
+      var fallback = 'platform/' + fallbacks[i];
+      addExpectations(expectationsContainers, expectationsContainer, TEST_URL_BASE_PATH, fallback, text, png,
+          reftest_html_file, reftest_mismatch_html_file, suite);
+    }
+
+    if (suite)
+        loadBaselinesForTest(expectationsContainers, expectationsContainer, baseTest(test, suite));
+}
+
+function loadExpectationsLayoutTests(test, expectationsContainer)
+{
+    // Map from file extension to container div for expectations of that type.
+    var expectationsContainers = {};
+
+    var revisionContainer = document.createElement('div');
+    revisionContainer.textContent = "Showing results for: "
+    expectationsContainer.appendChild(revisionContainer);
+    for (var builder in g_builders) {
+        if (builderMaster(builder) == WEBKIT_BUILDER_MASTER) {
+            var latestRevision = g_currentState.revision || g_resultsByBuilder[builder].webkitRevision[0];
+            var buildInfo = buildInfoForRevision(builder, latestRevision);
+            var revisionInfo = document.createElement('div');
+            revisionInfo.style.cssText = 'background:lightgray;margin:0 3px;padding:0 2px;display:inline-block;';
+            revisionInfo.innerHTML = builder + ' r' + buildInfo.revisionEnd +
+                ':r' + buildInfo.revisionStart + ', build ' + buildInfo.buildNumber;
+            revisionContainer.appendChild(revisionInfo);
+        }
+    }
+
+    loadBaselinesForTest(expectationsContainers, expectationsContainer, test);
+        
+    var testWithoutSuffix = test.substring(0, test.lastIndexOf('.'));
+    var actualResultSuffixes = ['-actual.txt', '-actual.png', '-crash-log.txt', '-diff.txt', '-wdiff.html', '-diff.png'];
+
+    for (var builder in g_builders) {
+        var actualResultsBase;
+        if (builderMaster(builder) == WEBKIT_BUILDER_MASTER) {
+            var latestRevision = g_currentState.revision || g_resultsByBuilder[builder].webkitRevision[0];
+            var buildInfo = buildInfoForRevision(builder, latestRevision);
+            actualResultsBase = 'http://build.webkit.org/results/' + builder +
+                '/r' + buildInfo.revisionStart + ' (' + buildInfo.buildNumber + ')/';
+        } else
+            actualResultsBase = TEST_RESULTS_BASE_PATH + g_builders[builder] + '/results/layout-test-results/';
+
+        for (var i = 0; i < actualResultSuffixes.length; i++) {
+            addExpectationItem(expectationsContainers, expectationsContainer, null,
+                testWithoutSuffix + actualResultSuffixes[i], actualResultsBase, builder);
+        }
+    }
+
+    // Add a clearing element so floated elements don't bleed out of their
+    // containing block.
+    var br = document.createElement('br');
+    br.style.clear = 'both';
+    expectationsContainer.appendChild(br);
+}
+
+var g_allFallbacks;
+
+// Returns the reverse sorted, deduped list of all platform fallback
+// directories.
+function allFallbacks()
+{
+    if (!g_allFallbacks) {
+        var holder = {};
+        for (var platform in g_fallbacksMap) {
+            var fallbacks = g_fallbacksMap[platform];
+            for (var i = 0; i < fallbacks.length; i++)
+                holder[fallbacks[i]] = 1;
+        }
+
+        g_allFallbacks = [];
+        for (var fallback in holder)
+            g_allFallbacks.push(fallback);
+
+        g_allFallbacks.sort(function(a, b) {
+            if (a == b)
+                return 0;
+            return a < b;
+        });
+    }
+    return g_allFallbacks;
+}
+
+function appendExpectations()
+{
+    var expectations = g_currentState.showExpectations ? document.getElementsByClassName('expectations') : [];
+    // Loading expectations is *very* slow. Use a large timeout to avoid
+    // totally hanging the renderer.
+    performChunkedAction(expectations, function(chunk) {
+        for (var i = 0, len = chunk.length; i < len; i++)
+            loadExpectations(chunk[i]);
+        postHeightChangedMessage();
+
+    }, hideLoadingUI, 10000);
+}
+
+function hideLoadingUI()
+{
+    var loadingDiv = $('loading-ui');
+    if (loadingDiv)
+        loadingDiv.style.display = 'none';
+    postHeightChangedMessage();
+}
+
+function generatePageForIndividualTests(tests)
+{
+    console.log('Number of tests: ' + tests.length);
+    if (g_currentState.showChrome)
+        appendHTML(htmlForNavBar());
+    performChunkedAction(tests, function(chunk) {
+        appendHTML(htmlForIndividualTests(chunk));
+    }, appendExpectations, 500);
+    if (g_currentState.showChrome)
+        $('tests-input').value = g_currentState.tests;
+}
+
+function performChunkedAction(tests, handleChunk, onComplete, timeout, opt_index) {
+    var index = opt_index || 0;
+    setTimeout(function() {
+        var chunk = Array.prototype.slice.call(tests, index * CHUNK_SIZE, (index + 1) * CHUNK_SIZE);
+        if (chunk.length) {
+            handleChunk(chunk);
+            performChunkedAction(tests, handleChunk, onComplete, timeout, ++index);
+        } else
+            onComplete();
+    // No need for a timeout on the first chunked action.
+    }, index ? timeout : 0);
+}
+
+function htmlForIndividualTests(tests)
+{
+    var testsHTML = [];
+    for (var i = 0; i < tests.length; i++) {
+        var test = tests[i];
+        var testNameHtml = '';
+        if (g_currentState.showChrome || tests.length > 1) {
+            if (isLayoutTestResults()) {
+                var suite = lookupVirtualTestSuite(test);
+                var base = suite ? baseTest(test, suite) : test;
+                var tracURL = TEST_URL_BASE_PATH_TRAC + base;
+                testNameHtml += '<h2>' + linkHTMLToOpenWindow(tracURL, test) + '</h2>';
+            } else
+                testNameHtml += '<h2>' + test + '</h2>';
+        }
+
+        testsHTML.push(testNameHtml + htmlForIndividualTestOnAllBuildersWithResultsLinks(test));
+    }
+    return testsHTML.join('<hr>');
+}
+
+function htmlForNavBar()
+{
+    var extraHTML = '';
+    var html = htmlForTestTypeSwitcher(false, extraHTML, isCrossBuilderView());
+    html += '<div class=forms><form id=result-form ' +
+        'onsubmit="setQueryParameter(\'result\', result.value);' +
+        'return false;">Show all tests with result: ' +
+        '<input name=result placeholder="e.g. CRASH" id=result-input>' +
+        '</form><form id=tests-form ' +
+        'onsubmit="setQueryParameter(\'tests\', tests.value);' +
+        'return false;"><span>Show tests on all platforms: </span>' +
+        '<input name=tests ' +
+        'placeholder="Comma or space-separated list of tests or partial ' +
+        'paths to show test results across all builders, e.g., ' +
+        'foo/bar.html,foo/baz,domstorage" id=tests-input></form>' +
+        '<span class=link onclick="showLegend()">Show legend [type ?]</span></div>';
+    return html;
+}
+
+function checkBoxToToggleState(key, text)
+{
+    var stateEnabled = g_currentState[key];
+    return '<label><input type=checkbox ' + (stateEnabled ? 'checked ' : '') + 'onclick="setQueryParameter(\'' + key + '\', ' + !stateEnabled + ')">' + text + '</label> ';
+}
+
+function linkHTMLToToggleState(key, linkText)
+{
+    var stateEnabled = g_currentState[key];
+    return '<span class=link onclick="setQueryParameter(\'' + key + '\', ' + !stateEnabled + ')">' + (stateEnabled ? 'Hide' : 'Show') + ' ' + linkText + '</span>';
+}
+
+function headerForTestTableHtml()
+{
+    return '<h2 style="display:inline-block">Failing tests</h2>' +
+        checkBoxToToggleState('showWontFixSkip', 'WONTFIX/SKIP') +
+        checkBoxToToggleState('showCorrectExpectations', 'tests with correct expectations') +
+        checkBoxToToggleState('showWrongExpectations', 'tests with wrong expectations') +
+        checkBoxToToggleState('showFlaky', 'flaky') +
+        checkBoxToToggleState('showSlow', 'slow');
+}
+
+function generatePageForBuilder(builderName)
+{
+    processTestRunsForBuilder(builderName);
+
+    var results = g_perBuilderFailures[builderName];
+    sortTests(results, g_currentState.sortColumn, g_currentState.sortOrder);
+
+    var testsHTML = '';
+    if (results.length) {
+        var tableRowsHTML = '';
+        for (var i = 0; i < results.length; i++)
+            tableRowsHTML += htmlForSingleTestRow(results[i])
+        testsHTML = htmlForTestTable(tableRowsHTML);
+    } else {
+        testsHTML = '<div>No tests found. ';
+        if (isLayoutTestResults())
+            testsHTML += 'Try showing tests with correct expectations.</div>';
+        else
+            testsHTML += 'This means no tests have failed!</div>';
+    }
+
+    var html = htmlForNavBar();
+
+    if (isLayoutTestResults())
+        html += htmlForTestsWithExpectationsButNoFailures(builderName) + headerForTestTableHtml();
+
+    html += '<br>' + testsHTML;
+    appendHTML(html);
+
+    var ths = document.getElementsByTagName('th');
+    for (var i = 0; i < ths.length; i++) {
+        ths[i].addEventListener('click', changeSort, false);
+        ths[i].className = "sortable";
+    }
+
+    hideLoadingUI();
+}
+
+var VALID_KEYS_FOR_CROSS_BUILDER_VIEW = {
+    tests: 1,
+    result: 1,
+    showChrome: 1,
+    showExpectations: 1,
+    showLargeExpectations: 1,
+    legacyExpectationsSemantics: 1,
+    resultsHeight: 1,
+    revision: 1
+};
+
+function isInvalidKeyForCrossBuilderView(key)
+{
+    return !(key in VALID_KEYS_FOR_CROSS_BUILDER_VIEW) && !(key in g_defaultCrossDashboardStateValues);
+}
+
+function updateDefaultBuilderState()
+{
+    if (isCrossBuilderView())
+        delete g_defaultDashboardSpecificStateValues.builder;
+    else
+        g_defaultDashboardSpecificStateValues.builder = g_defaultBuilderName;
+}
+
+// Sets the page state to regenerate the page.
+// @param {Object} params New or modified query parameters as key: value.
+function handleQueryParameterChange(params)
+{
+    for (key in params) {
+        if (key == 'tests') {
+            // Entering cross-builder view, only keep valid keys for that view.
+            for (var currentKey in g_currentState) {
+              if (isInvalidKeyForCrossBuilderView(currentKey)) {
+                delete g_currentState[currentKey];
+              }
+            }
+        } else if (isInvalidKeyForCrossBuilderView(key)) {
+            delete g_currentState.tests;
+            delete g_currentState.result;
+        }
+    }
+
+    updateDefaultBuilderState();
+    return true;
+}
+
+function hideLegend()
+{
+    var legend = $('legend');
+    if (legend)
+        legend.parentNode.removeChild(legend);
+}
+
+var g_fallbacksMap = {};
+g_fallbacksMap['WIN-XP'] = ['chromium-win-xp', 'chromium-win', 'chromium'];
+g_fallbacksMap['WIN-7'] = ['chromium-win', 'chromium'];
+g_fallbacksMap['MAC-SNOWLEOPARD'] = ['chromium-mac-snowleopard', 'chromium-mac', 'chromium'];
+g_fallbacksMap['MAC-LION'] = ['chromium-mac', 'chromium'];
+g_fallbacksMap['LINUX-32'] = ['chromium-linux-x86', 'chromium-linux', 'chromium-win', 'chromium'];
+g_fallbacksMap['LINUX-64'] = ['chromium-linux', 'chromium-win', 'chromium'];
+
+function htmlForFallbackHelp(fallbacks)
+{
+    return '<ol class=fallback-list><li>' + fallbacks.join('</li><li>') + '</li></ol>';
+}
+
+function showLegend()
+{
+    var legend = $('legend');
+    if (!legend) {
+        legend = document.createElement('div');
+        legend.id = 'legend';
+        document.body.appendChild(legend);
+    }
+
+    var html = '<div id=legend-toggle onclick="hideLegend()">Hide ' +
+        'legend [type esc]</div><div id=legend-contents>';
+    for (var expectation in expectationsMap())
+        html += '<div class=' + expectation + '>' + expectationsMap()[expectation] + '</div>';
+
+    html += '<div class=merge>WEBKIT MERGE</div>';
+    if (isLayoutTestResults()) {
+      html += '</div><br style="clear:both">' +
+          '</div><h3>Test expectatons fallback order.</h3>';
+
+      for (var platform in g_fallbacksMap)
+          html += '<div class=fallback-header>' + platform + '</div>' + htmlForFallbackHelp(g_fallbacksMap[platform]);
+
+      html += '<div>TIMES:</div>' +
+          htmlForSlowTimes(MIN_SECONDS_FOR_SLOW_TEST) +
+          '<div>DEBUG TIMES:</div>' +
+          htmlForSlowTimes(MIN_SECONDS_FOR_SLOW_TEST_DEBUG);
+    }
+
+    legend.innerHTML = html;
+}
+
+function htmlForSlowTimes(minTime)
+{
+    return '<ul><li>&lt;1 second == !SLOW</li><li>&gt;1 second && &lt;' +
+        minTime + ' seconds == SLOW || !SLOW is fine</li><li>&gt;' +
+        minTime + ' seconds == SLOW</li></ul>';
+}
+
+function postHeightChangedMessage()
+{
+    if (window == parent)
+        return;
+
+    var root = document.documentElement;
+    var height = root.offsetHeight;
+    if (root.offsetWidth < root.scrollWidth) {
+        // We have a horizontal scrollbar. Include it in the height.
+        var dummyNode = document.createElement('div');
+        dummyNode.style.overflow = 'scroll';
+        document.body.appendChild(dummyNode);
+        var scrollbarWidth = dummyNode.offsetHeight - dummyNode.clientHeight;
+        document.body.removeChild(dummyNode);
+        height += scrollbarWidth;
+    }
+    parent.postMessage({command: 'heightChanged', height: height}, '*')
+}
+
+if (window != parent)
+    window.addEventListener('blur', hidePopup);
+
+document.addEventListener('keydown', function(e) {
+    if (e.keyIdentifier == 'U+003F' || e.keyIdentifier == 'U+00BF') {
+        // WebKit MAC retursn 3F. WebKit WIN returns BF. This is a bug!
+        // ? key
+        showLegend();
+    } else if (e.keyIdentifier == 'U+001B') {
+        // escape key
+        hideLegend();
+        hidePopup();
+    }
+}, false);
diff --git a/Tools/TestResultServer/static-dashboards/flakiness_dashboard_embedded.html b/Tools/TestResultServer/static-dashboards/flakiness_dashboard_embedded.html
new file mode 100644
index 0000000..c371e78
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/flakiness_dashboard_embedded.html
@@ -0,0 +1,83 @@
+<!-- Copyright (C) 2011 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.
+-->
+<!DOCTYPE HTML>
+<style>
+iframe {
+    width: 100%;
+    display: block;
+}
+#toolbar {
+    display: -webkit-flexbox;
+    -webkit-flex-align: baseline;
+}
+#tests {
+    -webkit-flex: 1;
+    /* WebKit bug. Don't properly wrap input elements for flexing. */
+    display: block;
+}
+</style>
+
+<div>This is a demo page for working on iframe embedding the flakiness dashboard.</div>
+<div id="toolbar">
+    <input id="chrome" type=checkbox></input><label>Hide chrome</label> | 
+    <label>Tests:</label><input id=tests placeholder="Type test name here to load a different test in the frame" ></input>
+</div>
+<iframe src="flakiness_dashboard.html"></iframe>
+
+<script>
+var timeoutId;
+document.querySelector('#tests').oninput = function(event) {
+    if (timeoutId)
+        clearTimeout(timeoutId);
+    timeoutId = setTimeout(setFrameSrc, 1000);
+};
+document.querySelector('#chrome').onchange = setFrameSrc;
+
+function setFrameSrc() {
+    var tests = document.querySelector('#tests').value;
+    var hideChrome = document.querySelector('#chrome').checked ? '&showChrome=false' : '';
+    var url = 'flakiness_dashboard.html#tests=' + tests + hideChrome;
+    document.querySelector('iframe').src = url;
+};
+
+function sizeIframeToContents() {
+    document.querySelector('iframe').contentWindow.postMessage({command: 'queryContentHeight'}, '*');
+};
+
+window.addEventListener('message', function(event) {
+    switch(event.data.command) {
+    case 'heightChanged':
+        document.querySelector('iframe').style.height = event.data.height + 'px';
+        break;
+
+    default:
+        console.error('Did not understand message: ' + event.data);
+    }
+});
+</script>
diff --git a/Tools/TestResultServer/static-dashboards/flakiness_dashboard_embedded_unittests.js b/Tools/TestResultServer/static-dashboards/flakiness_dashboard_embedded_unittests.js
new file mode 100644
index 0000000..5599b3e
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/flakiness_dashboard_embedded_unittests.js
@@ -0,0 +1,40 @@
+// Copyright (C) 2011 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.
+
+test('hidePopupOnBlur', 2, function() {
+    showPopup(document.body, 'dummy content');
+    ok(document.querySelector('#popup'));
+
+    // Cause the window to be blurred.
+    var frame = document.createElement('iframe');
+    document.body.appendChild(frame);
+    frame.focus();
+    document.body.removeChild(frame);
+
+    ok(!document.querySelector('#popup'));
+});
\ No newline at end of file
diff --git a/Tools/TestResultServer/static-dashboards/flakiness_dashboard_tests.css b/Tools/TestResultServer/static-dashboards/flakiness_dashboard_tests.css
new file mode 100644
index 0000000..6165bcd
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/flakiness_dashboard_tests.css
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#errors {
+    color: red;
+    font-size: 16px;
+    margin: 0;
+}
+#result-input {
+    width: 7em;
+}
+.test-link.builder-name {
+    white-space: nowrap;
+}
+.test-link, .options-container {
+    padding: 0 2px;
+}
+.test-table {
+    white-space: nowrap;
+    border-spacing: 1px;
+}
+/* Let the bugs column wrap. */
+.test-table tr > td:nth-child(2) {
+    white-space: normal;
+}
+.test-table {
+    width: 100%;
+}
+.test-table tr {
+    border: 1px solid red;
+    background-color: #E8E8E8;
+}
+.test-table tbody tr:hover {
+    opacity: .7;
+}
+.test-table th {
+    -webkit-user-select: none;
+    -moz-user-select: none;
+}
+.link, .sortable .header-text {
+    color: blue;
+    text-decoration: underline;
+    cursor: pointer;
+}
+.table-header-content,
+.table-header-content * {
+    display: -webkit-box;
+}
+.table-header-content * {
+    -webkit-box-flex: 1;
+    cursor: pointer;
+}
+.results {
+    cursor: pointer;
+    padding: 0 1px;
+    font-size: 10px;
+    text-align: center;
+}
+#legend {
+    position: fixed;
+    top: 5px;
+    right: 5px;
+    width: 400px;
+    padding: 2px;
+    border: 2px solid grey;
+    background-color: white;
+    z-index: 1;
+}
+#legend ul, #legend ol {
+    margin-top: 0;
+    margin-bottom: 5px;
+}
+#legend-contents * {
+    margin: 3px 0;
+    padding: 0 2px;
+    float: left;
+    border: 1px solid grey;
+}
+.P {
+    background-color: #3f3;
+}
+.N {
+    background-color: #fff;
+}
+.X {
+    background-color: lightgray;
+}
+.C {
+    background-color: #c90;
+}
+.T {
+    background-color: #fffc6c;
+}
+.I {
+    background-color: #69f;
+}
+.S {
+    background-color: #c6c;
+}
+.F {
+    background-color: #e98080;
+}
+.O {
+    background-color: #8a7700;
+}
+.Z {
+    background-color: #96f;
+}
+#legend .merge {
+    background-color: black;
+    color: white;
+}
+.test-table .merge {
+    border-right: solid 2px #000;
+    padding-right: 0;
+}
+.separator {
+    border: 1px solid lightgray;
+    height: 0px;
+}
+#passing-tests,
+#skipped-tests {
+    -webkit-column-count: 3;
+    -webkit-column-gap: 25px;
+    -webkit-column-rule: 1px dashed black;
+    -moz-column-count: 3;
+    -moz-column-gap: 25px;
+    -moz-column-rule: 1px dashed black;
+    border-top: 1px dashed black;
+    border-bottom: 1px dashed black;
+}
+.not-found {
+    color: red;
+    font-size: large;
+}
+#loading-ui {
+    position: fixed;
+    top: 0;
+    left: 0;
+    background-color: yellow;
+    padding: 5px;
+    text-align: center;
+    font-weight: bold;
+}
+#popup {
+    background-color: white;
+    z-index: 1;
+    position: absolute;
+    border: 3px solid grey;
+    padding: 3px;
+    -webkit-box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.5);
+    -moz-box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.5);
+    -webkit-border-radius: 5px;
+    -moz-border-radius: 5px;
+}
+#popup > ul {
+    margin: 0;
+    padding-left: 20px;
+}
+.expectations-container {
+    clear: both;
+}
+.expectations-item {
+    float: left;
+    border: 1px solid grey;
+    display: -webkit-box;
+    display: -moz-box;
+    position: relative;
+    -webkit-box-orient: vertical;
+    -moz-box-orient: vertical;
+}
+.expectations-item .expectation {
+    width: 400px;
+    height: 300px;
+    border: 0;
+    border-top: 1px solid grey;
+    overflow: auto;
+    display: -webkit-box;
+    display: -moz-box;
+    -webkit-box-flex: 1;
+    -moz-box-flex: 1;
+}
+pre.expectation {
+    padding: 8px;
+    margin: 0;
+    box-sizing: border-box;
+}
+.expectations-item .large {
+    width: 800px;
+    height: 600px;
+}
+.non-webkit-results {
+    width: 99%;
+}
+.gpu-test-results {
+    width: 400px;
+}
+.used-platform {
+    float: right;
+    color: darkblue;
+    margin: 0 5px;
+}
+.expectations-title {
+    /* Hack to make a containing block for absolute positioned elements. */
+    position: relative;
+    clear: both;
+}
+.title {
+    /* Position absolutely so the container does not grow to contain this. */
+    position: absolute;
+}
+.platforms {
+    position: absolute;
+    right: 0;
+    z-index: 1;
+}
+.file-bug {
+    font-weight: bold;
+    font-size: 11px;
+}
+.pngchecksum {
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    background-color: #ffffc8;
+    visibility: hidden;
+}
+.expectations-item:hover .pngchecksum {
+    visibility: visible;
+}
+.skipped-builder-list {
+    margin-left: 20px;
+    background-color: #E8E8E8;
+}
+.skipped-builder {
+    display: inline-block;
+    white-space: nowrap;
+}
+.skipped-builder:after {
+    content: '|';
+    margin: 5px;
+}
+.skipped-builder:last-child:after {
+    content: '';
+}
+
diff --git a/Tools/TestResultServer/static-dashboards/flakiness_dashboard_unittests.js b/Tools/TestResultServer/static-dashboards/flakiness_dashboard_unittests.js
new file mode 100644
index 0000000..597c62e
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/flakiness_dashboard_unittests.js
@@ -0,0 +1,799 @@
+// Copyright (C) 2011 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.
+
+function resetGlobals()
+{
+    allExpectations = null;
+    allTests = null;
+    g_expectationsByPlatform = {};
+    g_resultsByBuilder = {};
+    g_builders = {};
+    g_allExpectations = null;
+    g_allTestsTrie = null;
+    g_currentState = {};
+    g_crossDashboardState = {};
+    for (var key in g_defaultCrossDashboardStateValues)
+        g_crossDashboardState[key] = g_defaultCrossDashboardStateValues[key];
+}
+
+function runExpectationsTest(builder, test, expectations, modifiers)
+{
+    g_builders[builder] = true;
+
+    // Put in some dummy results. processExpectations expects the test to be
+    // there.
+    var tests = {};
+    tests[test] = {'results': [[100, 'F']], 'times': [[100, 0]]};
+    g_resultsByBuilder[builder] = {'tests': tests};
+
+    processExpectations();
+    var resultsForTest = createResultsObjectForTest(test, builder);
+    populateExpectationsData(resultsForTest);
+
+    var message = 'Builder: ' + resultsForTest.builder + ' test: ' + resultsForTest.test;
+    equal(resultsForTest.expectations, expectations, message);
+    equal(resultsForTest.modifiers, modifiers, message);
+}
+
+test('flattenTrie', 1, function() {
+    resetGlobals();
+    var tests = {
+        'bar.html': {'results': [[100, 'F']], 'times': [[100, 0]]},
+        'foo': {
+            'bar': {
+                'baz.html': {'results': [[100, 'F']], 'times': [[100, 0]]},
+            }
+        }
+    };
+    var expectedFlattenedTests = {
+        'bar.html': {'results': [[100, 'F']], 'times': [[100, 0]]},
+        'foo/bar/baz.html': {'results': [[100, 'F']], 'times': [[100, 0]]},
+    };
+    equal(JSON.stringify(flattenTrie(tests)), JSON.stringify(expectedFlattenedTests))
+});
+
+test('releaseFail', 2, function() {
+    resetGlobals();
+    var builder = 'WebKit Win';
+    var test = 'foo/1.html';
+    var expectationsArray = [
+        {'modifiers': 'RELEASE', 'expectations': 'FAIL'}
+    ];
+    g_expectationsByPlatform['CHROMIUM'] = getParsedExpectations('[ Release ] ' + test + ' [ Failure ]');
+    runExpectationsTest(builder, test, 'FAIL', 'RELEASE');
+});
+
+test('releaseFailDebugCrashReleaseBuilder', 2, function() {
+    resetGlobals();
+    var builder = 'WebKit Win';
+    var test = 'foo/1.html';
+    var expectationsArray = [
+        {'modifiers': 'RELEASE', 'expectations': 'FAIL'},
+        {'modifiers': 'DEBUG', 'expectations': 'CRASH'}
+    ];
+    g_expectationsByPlatform['CHROMIUM'] = getParsedExpectations('[ Release ] ' + test + ' [ Failure ]\n' +
+        '[ Debug ] ' + test + ' [ Crash ]');
+    runExpectationsTest(builder, test, 'FAIL', 'RELEASE');
+});
+
+test('releaseFailDebugCrashDebugBuilder', 2, function() {
+    resetGlobals();
+    var builder = 'WebKit Win (dbg)';
+    var test = 'foo/1.html';
+    var expectationsArray = [
+        {'modifiers': 'RELEASE', 'expectations': 'FAIL'},
+        {'modifiers': 'DEBUG', 'expectations': 'CRASH'}
+    ];
+    g_expectationsByPlatform['CHROMIUM'] = getParsedExpectations('[ Release ] ' + test + ' [ Failure ]\n' +
+        '[ Debug ] ' + test + ' [ Crash ]');
+    runExpectationsTest(builder, test, 'CRASH', 'DEBUG');
+});
+
+test('overrideJustBuildType', 12, function() {
+    resetGlobals();
+    var test = 'bar/1.html';
+    g_expectationsByPlatform['CHROMIUM'] = getParsedExpectations('bar [ WontFix Failure Pass Timeout ]\n' +
+        '[ Mac ] ' + test + ' [ WontFix Failure ]\n' +
+        '[ Linux Debug ] ' + test + ' [ Crash ]');
+    
+    runExpectationsTest('WebKit Win', test, 'FAIL PASS TIMEOUT', 'WONTFIX');
+    runExpectationsTest('WebKit Win (dbg)(3)', test, 'FAIL PASS TIMEOUT', 'WONTFIX');
+    runExpectationsTest('WebKit Linux', test, 'FAIL PASS TIMEOUT', 'WONTFIX');
+    runExpectationsTest('WebKit Linux (dbg)(3)', test, 'CRASH', 'LINUX DEBUG');
+    runExpectationsTest('WebKit Mac10.7', test, 'FAIL', 'MAC WONTFIX');
+    runExpectationsTest('WebKit Mac10.7 (dbg)(3)', test, 'FAIL', 'MAC WONTFIX');
+});
+
+test('platformAndBuildType', 78, function() {
+    var runPlatformAndBuildTypeTest = function(builder, expectedPlatform, expectedBuildType) {
+        g_perBuilderPlatformAndBuildType = {};
+        buildInfo = platformAndBuildType(builder);
+        var message = 'Builder: ' + builder;
+        equal(buildInfo.platform, expectedPlatform, message);
+        equal(buildInfo.buildType, expectedBuildType, message);
+    }
+    runPlatformAndBuildTypeTest('WebKit Win (deps)', 'CHROMIUM_XP', 'RELEASE');
+    runPlatformAndBuildTypeTest('WebKit Win (deps)(dbg)(1)', 'CHROMIUM_XP', 'DEBUG');
+    runPlatformAndBuildTypeTest('WebKit Win (deps)(dbg)(2)', 'CHROMIUM_XP', 'DEBUG');
+    runPlatformAndBuildTypeTest('WebKit Linux (deps)', 'CHROMIUM_LUCID', 'RELEASE');
+    runPlatformAndBuildTypeTest('WebKit Linux (deps)(dbg)(1)', 'CHROMIUM_LUCID', 'DEBUG');
+    runPlatformAndBuildTypeTest('WebKit Linux (deps)(dbg)(2)', 'CHROMIUM_LUCID', 'DEBUG');
+    runPlatformAndBuildTypeTest('WebKit Mac10.6 (deps)', 'CHROMIUM_SNOWLEOPARD', 'RELEASE');
+    runPlatformAndBuildTypeTest('WebKit Mac10.6 (deps)(dbg)(1)', 'CHROMIUM_SNOWLEOPARD', 'DEBUG');
+    runPlatformAndBuildTypeTest('WebKit Mac10.6 (deps)(dbg)(2)', 'CHROMIUM_SNOWLEOPARD', 'DEBUG');
+    runPlatformAndBuildTypeTest('WebKit Win', 'CHROMIUM_XP', 'RELEASE');
+    runPlatformAndBuildTypeTest('WebKit Win7', 'CHROMIUM_WIN7', 'RELEASE');
+    runPlatformAndBuildTypeTest('WebKit Win (dbg)(1)', 'CHROMIUM_XP', 'DEBUG');
+    runPlatformAndBuildTypeTest('WebKit Win (dbg)(2)', 'CHROMIUM_XP', 'DEBUG');
+    runPlatformAndBuildTypeTest('WebKit Linux', 'CHROMIUM_LUCID', 'RELEASE');
+    runPlatformAndBuildTypeTest('WebKit Linux 32', 'CHROMIUM_LUCID', 'RELEASE');
+    runPlatformAndBuildTypeTest('WebKit Linux (dbg)(1)', 'CHROMIUM_LUCID', 'DEBUG');
+    runPlatformAndBuildTypeTest('WebKit Linux (dbg)(2)', 'CHROMIUM_LUCID', 'DEBUG');
+    runPlatformAndBuildTypeTest('WebKit Mac10.6', 'CHROMIUM_SNOWLEOPARD', 'RELEASE');
+    runPlatformAndBuildTypeTest('WebKit Mac10.6 (dbg)', 'CHROMIUM_SNOWLEOPARD', 'DEBUG');
+    runPlatformAndBuildTypeTest('XP Tests', 'CHROMIUM_XP', 'RELEASE');
+    runPlatformAndBuildTypeTest('Interactive Tests (dbg)', 'CHROMIUM_XP', 'DEBUG');
+    
+    g_crossDashboardState.group = '@ToT - webkit.org';
+    g_crossDashboardState.testType = 'layout-tests';
+    runPlatformAndBuildTypeTest('Chromium Win Release (Tests)', 'CHROMIUM_XP', 'RELEASE');
+    runPlatformAndBuildTypeTest('Chromium Linux Release (Tests)', 'CHROMIUM_LUCID', 'RELEASE');
+    runPlatformAndBuildTypeTest('Chromium Mac Release (Tests)', 'CHROMIUM_SNOWLEOPARD', 'RELEASE');
+    
+    // FIXME: These platforms should match whatever we use in the TestExpectations format.
+    runPlatformAndBuildTypeTest('Lion Release (Tests)', 'APPLE_MAC_LION_WK1', 'RELEASE');
+    runPlatformAndBuildTypeTest('Lion Debug (Tests)', 'APPLE_MAC_LION_WK1', 'DEBUG');
+    runPlatformAndBuildTypeTest('SnowLeopard Intel Release (Tests)', 'APPLE_MAC_SNOWLEOPARD_WK1', 'RELEASE');
+    runPlatformAndBuildTypeTest('SnowLeopard Intel Leaks', 'APPLE_MAC_SNOWLEOPARD_WK1', 'RELEASE');
+    runPlatformAndBuildTypeTest('SnowLeopard Intel Debug (Tests)', 'APPLE_MAC_SNOWLEOPARD_WK1', 'DEBUG');
+    runPlatformAndBuildTypeTest('GTK Linux 32-bit Release', 'GTK_LINUX_WK1', 'RELEASE');
+    runPlatformAndBuildTypeTest('GTK Linux 32-bit Debug', 'GTK_LINUX_WK1', 'DEBUG');
+    runPlatformAndBuildTypeTest('GTK Linux 64-bit Debug', 'GTK_LINUX_WK1', 'DEBUG');
+    runPlatformAndBuildTypeTest('GTK Linux 64-bit Debug WK2', 'GTK_LINUX_WK2', 'DEBUG');
+    runPlatformAndBuildTypeTest('Qt Linux Release', 'QT_LINUX', 'RELEASE');
+    runPlatformAndBuildTypeTest('Windows 7 Release (Tests)', 'APPLE_WIN_WIN7', 'RELEASE');
+    runPlatformAndBuildTypeTest('Windows XP Debug (Tests)', 'APPLE_WIN_XP', 'DEBUG');
+    
+    // FIXME: Should WebKit2 be it's own platform?
+    runPlatformAndBuildTypeTest('SnowLeopard Intel Release (WebKit2 Tests)', 'APPLE_MAC_SNOWLEOPARD_WK2', 'RELEASE');
+    runPlatformAndBuildTypeTest('SnowLeopard Intel Debug (WebKit2 Tests)', 'APPLE_MAC_SNOWLEOPARD_WK2', 'DEBUG');
+    runPlatformAndBuildTypeTest('Windows 7 Release (WebKit2 Tests)', 'APPLE_WIN_WIN7', 'RELEASE');    
+});
+
+test('realModifiers', 3, function() {
+    equal(realModifiers('BUG(Foo) LINUX LION WIN DEBUG SLOW'), 'SLOW');
+    equal(realModifiers('BUG(Foo) LUCID MAC XP RELEASE SKIP'), 'SKIP');
+    equal(realModifiers('BUG(Foo)'), '');
+});
+
+test('allTestsWithSamePlatformAndBuildType', 1, function() {
+    // FIXME: test that allTestsWithSamePlatformAndBuildType actually returns the right set of tests.
+    var expectedPlatformsList = ['CHROMIUM_LION', 'CHROMIUM_SNOWLEOPARD', 'CHROMIUM_XP', 'CHROMIUM_VISTA', 'CHROMIUM_WIN7', 'CHROMIUM_LUCID',
+                                 'CHROMIUM_ANDROID', 'APPLE_MAC_LION_WK1', 'APPLE_MAC_LION_WK2', 'APPLE_MAC_SNOWLEOPARD_WK1', 'APPLE_MAC_SNOWLEOPARD_WK2',
+                                 'APPLE_WIN_XP', 'APPLE_WIN_WIN7',  'GTK_LINUX_WK1', 'GTK_LINUX_WK2', 'QT_LINUX', 'EFL_LINUX_WK1', 'EFL_LINUX_WK2'];
+    var actualPlatformsList = Object.keys(g_allTestsByPlatformAndBuildType);
+    deepEqual(expectedPlatformsList, actualPlatformsList);
+});
+
+test('filterBugs',4, function() {
+    var filtered = filterBugs('Skip crbug.com/123 webkit.org/b/123 Slow Bug(Tony) Debug')
+    equal(filtered.modifiers, 'Skip Slow Debug');
+    equal(filtered.bugs, 'crbug.com/123 webkit.org/b/123 Bug(Tony)');
+
+    filtered = filterBugs('Skip Slow Debug')
+    equal(filtered.modifiers, 'Skip Slow Debug');
+    equal(filtered.bugs, '');
+});
+
+test('getExpectations', 16, function() {
+    resetGlobals();
+    g_builders['WebKit Win'] = true;
+    g_resultsByBuilder = {
+        'WebKit Win': {
+            'tests': {
+                'foo/test1.html': {'results': [[100, 'F']], 'times': [[100, 0]]},
+                'foo/test2.html': {'results': [[100, 'F']], 'times': [[100, 0]]},
+                'foo/test3.html': {'results': [[100, 'F']], 'times': [[100, 0]]},
+                'test1.html': {'results': [[100, 'F']], 'times': [[100, 0]]}
+            }
+        }
+    }
+
+    g_expectationsByPlatform['CHROMIUM'] = getParsedExpectations('Bug(123) foo [ Failure Pass Crash ]\n' +
+        'Bug(Foo) [ Release ] foo/test1.html [ Failure ]\n' +
+        '[ Debug ] foo/test1.html [ Crash ]\n' +
+        'Bug(456) foo/test2.html [ Failure ]\n' +
+        '[ Linux Debug ] foo/test2.html [ Crash ]\n' +
+        '[ Release ] test1.html [ Failure ]\n' +
+        '[ Debug ] test1.html [ Crash ]\n');
+    g_expectationsByPlatform['CHROMIUM_ANDROID'] = getParsedExpectations('Bug(654) foo/test2.html [ Crash ]\n');
+
+    g_expectationsByPlatform['GTK'] = getParsedExpectations('Bug(42) foo/test2.html [ ImageOnlyFailure ]\n' +
+        '[ Debug ] test1.html [ Crash ]\n');
+    g_expectationsByPlatform['GTK_LINUX_WK1'] = getParsedExpectations('[ Release ] foo/test1.html [ ImageOnlyFailure ]\n' +
+        'Bug(789) foo/test2.html [ Crash ]\n');
+    g_expectationsByPlatform['GTK_LINUX_WK2'] = getParsedExpectations('Bug(987) foo/test2.html [ Failure ]\n');
+
+    processExpectations();
+    
+    var expectations = getExpectations('foo/test1.html', 'CHROMIUM_XP', 'DEBUG');
+    equal(JSON.stringify(expectations), '{"modifiers":"DEBUG","expectations":"CRASH"}');
+
+    var expectations = getExpectations('foo/test1.html', 'CHROMIUM_LUCID', 'RELEASE');
+    equal(JSON.stringify(expectations), '{"modifiers":"Bug(Foo) RELEASE","expectations":"FAIL"}');
+
+    var expectations = getExpectations('foo/test2.html', 'CHROMIUM_LUCID', 'RELEASE');
+    equal(JSON.stringify(expectations), '{"modifiers":"Bug(456)","expectations":"FAIL"}');
+
+    var expectations = getExpectations('foo/test2.html', 'CHROMIUM_LION', 'DEBUG');
+    equal(JSON.stringify(expectations), '{"modifiers":"Bug(456)","expectations":"FAIL"}');
+
+    var expectations = getExpectations('foo/test2.html', 'CHROMIUM_LUCID', 'DEBUG');
+    equal(JSON.stringify(expectations), '{"modifiers":"LINUX DEBUG","expectations":"CRASH"}');
+
+    var expectations = getExpectations('foo/test2.html', 'CHROMIUM_ANDROID', 'RELEASE');
+    equal(JSON.stringify(expectations), '{"modifiers":"Bug(654)","expectations":"CRASH"}');
+
+    var expectations = getExpectations('test1.html', 'CHROMIUM_ANDROID', 'RELEASE');
+    equal(JSON.stringify(expectations), '{"modifiers":"RELEASE","expectations":"FAIL"}');
+
+    var expectations = getExpectations('foo/test3.html', 'CHROMIUM_LUCID', 'DEBUG');
+    equal(JSON.stringify(expectations), '{"modifiers":"Bug(123)","expectations":"FAIL PASS CRASH"}');
+
+    var expectations = getExpectations('test1.html', 'CHROMIUM_XP', 'DEBUG');
+    equal(JSON.stringify(expectations), '{"modifiers":"DEBUG","expectations":"CRASH"}');
+
+    var expectations = getExpectations('test1.html', 'CHROMIUM_LUCID', 'RELEASE');
+    equal(JSON.stringify(expectations), '{"modifiers":"RELEASE","expectations":"FAIL"}');
+
+    var expectations = getExpectations('foo/test1.html', 'GTK_LINUX_WK1', 'RELEASE');
+    equal(JSON.stringify(expectations), '{"modifiers":"RELEASE","expectations":"IMAGE"}');
+
+    var expectations = getExpectations('foo/test2.html', 'GTK_LINUX_WK1', 'DEBUG');
+    equal(JSON.stringify(expectations), '{"modifiers":"Bug(789)","expectations":"CRASH"}');
+
+    var expectations = getExpectations('test1.html', 'GTK_LINUX_WK1', 'DEBUG');
+    equal(JSON.stringify(expectations), '{"modifiers":"DEBUG","expectations":"CRASH"}');
+
+    var expectations = getExpectations('foo/test2.html', 'GTK_LINUX_WK2', 'DEBUG');
+    equal(JSON.stringify(expectations), '{"modifiers":"Bug(987)","expectations":"FAIL"}');
+
+    var expectations = getExpectations('foo/test2.html', 'GTK_LINUX_WK2', 'RELEASE');
+    equal(JSON.stringify(expectations), '{"modifiers":"Bug(987)","expectations":"FAIL"}');
+
+    var expectations = getExpectations('test1.html', 'GTK_LINUX_WK2', 'DEBUG');
+    equal(JSON.stringify(expectations), '{"modifiers":"DEBUG","expectations":"CRASH"}');
+});
+
+test('substringList', 2, function() {
+    g_crossDashboardState.testType = 'gtest';
+    g_currentState.tests = 'test.FLAKY_foo test.FAILS_foo1 test.DISABLED_foo2 test.MAYBE_foo3 test.foo4';
+    equal(substringList().toString(), 'test.foo,test.foo1,test.foo2,test.foo3,test.foo4');
+
+    g_crossDashboardState.testType = 'layout-tests';
+    g_currentState.tests = 'foo/bar.FLAKY_foo.html';
+    equal(substringList().toString(), 'foo/bar.FLAKY_foo.html');
+});
+
+test('htmlForTestsWithExpectationsButNoFailures', 4, function() {
+    var builder = 'WebKit Win';
+    g_perBuilderWithExpectationsButNoFailures[builder] = ['passing-test1.html', 'passing-test2.html'];
+    g_perBuilderSkippedPaths[builder] = ['skipped-test1.html'];
+    g_resultsByBuilder[builder] = { buildNumbers: [5, 4, 3, 1] };
+
+    g_currentState.showUnexpectedPasses = true;
+    g_currentState.showSkipped = true;
+
+    g_crossDashboardState.group = '@ToT - chromium.org';
+    g_crossDashboardState.testType = 'layout-tests';
+    
+    var container = document.createElement('div');
+    container.innerHTML = htmlForTestsWithExpectationsButNoFailures(builder);
+    equal(container.querySelectorAll('#passing-tests > div').length, 2);
+    equal(container.querySelectorAll('#skipped-tests > div').length, 1);
+    
+    g_currentState.showUnexpectedPasses = false;
+    g_currentState.showSkipped = false;
+    
+    var container = document.createElement('div');
+    container.innerHTML = htmlForTestsWithExpectationsButNoFailures(builder);
+    equal(container.querySelectorAll('#passing-tests > div').length, 0);
+    equal(container.querySelectorAll('#skipped-tests > div').length, 0);
+});
+
+test('headerForTestTableHtml', 1, function() {
+    var container = document.createElement('div');
+    container.innerHTML = headerForTestTableHtml();
+    equal(container.querySelectorAll('input').length, 5);
+});
+
+test('htmlForTestTypeSwitcherGroup', 6, function() {
+    var container = document.createElement('div');
+    g_crossDashboardState.testType = 'ui_tests';
+    container.innerHTML = htmlForTestTypeSwitcher(true);
+    var selects = container.querySelectorAll('select');
+    equal(selects.length, 2);
+    var group = selects[1];
+    equal(group.parentNode.textContent.indexOf('Group:'), 0);
+    equal(group.children.length, 3);
+
+    g_crossDashboardState.testType = 'layout-tests';
+    container.innerHTML = htmlForTestTypeSwitcher(true);
+    var selects = container.querySelectorAll('select');
+    equal(selects.length, 2);
+    var group = selects[1];
+    equal(group.parentNode.textContent.indexOf('Group:'), 0);
+    equal(group.children.length, 4);
+});
+
+test('htmlForIndividualTestOnAllBuilders', 1, function() {
+    resetGlobals();
+    equal(htmlForIndividualTestOnAllBuilders('foo/nonexistant.html'), '<div class="not-found">Test not found. Either it does not exist, is skipped or passes on all platforms.</div>');
+});
+
+test('htmlForIndividualTestOnAllBuildersWithResultsLinksNonexistant', 1, function() {
+    resetGlobals();
+    equal(htmlForIndividualTestOnAllBuildersWithResultsLinks('foo/nonexistant.html'),
+        '<div class="not-found">Test not found. Either it does not exist, is skipped or passes on all platforms.</div>' +
+        '<div class=expectations test=foo/nonexistant.html>' +
+            '<div>' +
+                '<span class=link onclick="setQueryParameter(\'showExpectations\', true)">Show results</span> | ' +
+                '<span class=link onclick="setQueryParameter(\'showLargeExpectations\', true)">Show large thumbnails</span> | ' +
+                '<b>Only shows actual results/diffs from the most recent *failure* on each bot.</b>' +
+            '</div>' +
+        '</div>');
+});
+
+test('htmlForIndividualTestOnAllBuildersWithResultsLinks', 1, function() {
+    resetGlobals();
+    var test = 'dummytest.html';
+    var builderName = 'dummyBuilder';
+    BUILDER_TO_MASTER[builderName] = CHROMIUM_BUILDER_MASTER;
+    g_testToResultsMap[test] = [createResultsObjectForTest(test, builderName)];
+
+    equal(htmlForIndividualTestOnAllBuildersWithResultsLinks(test),
+        '<table class=test-table><thead><tr>' +
+                '<th sortValue=test><div class=table-header-content><span></span><span class=header-text>test</span></div></th>' +
+                '<th sortValue=bugs><div class=table-header-content><span></span><span class=header-text>bugs</span></div></th>' +
+                '<th sortValue=modifiers><div class=table-header-content><span></span><span class=header-text>modifiers</span></div></th>' +
+                '<th sortValue=expectations><div class=table-header-content><span></span><span class=header-text>expectations</span></div></th>' +
+                '<th sortValue=slowest><div class=table-header-content><span></span><span class=header-text>slowest run</span></div></th>' +
+                '<th sortValue=flakiness colspan=10000><div class=table-header-content><span></span><span class=header-text>flakiness (numbers are runtimes in seconds)</span></div></th>' +
+            '</tr></thead>' +
+            '<tbody></tbody>' +
+        '</table>' +
+        '<div>The following builders either don\'t run this test (e.g. it\'s skipped) or all runs passed:</div>' +
+        '<div class=skipped-builder-list>' +
+            '<div class=skipped-builder>WebKit Linux</div><div class=skipped-builder>WebKit Linux (dbg)</div>' +
+            '<div class=skipped-builder>WebKit Mac10.7</div><div class=skipped-builder>WebKit Win</div>' +
+        '</div>' +
+        '<div class=expectations test=dummytest.html>' +
+            '<div><span class=link onclick="setQueryParameter(\'showExpectations\', true)">Show results</span> | ' +
+            '<span class=link onclick="setQueryParameter(\'showLargeExpectations\', true)">Show large thumbnails</span> | ' +
+            '<b>Only shows actual results/diffs from the most recent *failure* on each bot.</b></div>' +
+        '</div>');
+});
+
+test('htmlForIndividualTestOnAllBuildersWithResultsLinksWebkitMaster', 1, function() {
+    resetGlobals();
+    var test = 'dummytest.html';
+    var builderName = 'dummyBuilder';
+    BUILDER_TO_MASTER[builderName] = WEBKIT_BUILDER_MASTER;
+    g_testToResultsMap[test] = [createResultsObjectForTest(test, builderName)];
+
+    equal(htmlForIndividualTestOnAllBuildersWithResultsLinks(test),
+        '<table class=test-table><thead><tr>' +
+                '<th sortValue=test><div class=table-header-content><span></span><span class=header-text>test</span></div></th>' +
+                '<th sortValue=bugs><div class=table-header-content><span></span><span class=header-text>bugs</span></div></th>' +
+                '<th sortValue=modifiers><div class=table-header-content><span></span><span class=header-text>modifiers</span></div></th>' +
+                '<th sortValue=expectations><div class=table-header-content><span></span><span class=header-text>expectations</span></div></th>' +
+                '<th sortValue=slowest><div class=table-header-content><span></span><span class=header-text>slowest run</span></div></th>' +
+                '<th sortValue=flakiness colspan=10000><div class=table-header-content><span></span><span class=header-text>flakiness (numbers are runtimes in seconds)</span></div></th>' +
+            '</tr></thead>' +
+            '<tbody></tbody>' +
+        '</table>' +
+        '<div>The following builders either don\'t run this test (e.g. it\'s skipped) or all runs passed:</div>' +
+        '<div class=skipped-builder-list>' +
+            '<div class=skipped-builder>WebKit Linux</div><div class=skipped-builder>WebKit Linux (dbg)</div>' +
+            '<div class=skipped-builder>WebKit Mac10.7</div><div class=skipped-builder>WebKit Win</div>' +
+        '</div>' +
+        '<div class=expectations test=dummytest.html>' +
+            '<div><span class=link onclick="setQueryParameter(\'showExpectations\', true)">Show results</span> | ' +
+            '<span class=link onclick="setQueryParameter(\'showLargeExpectations\', true)">Show large thumbnails</span>' +
+            '<form onsubmit="setQueryParameter(\'revision\', revision.value);return false;">' +
+                'Show results for WebKit revision: <input name=revision placeholder="e.g. 65540" value="" id=revision-input>' +
+            '</form></div>' +
+        '</div>');
+});
+
+test('htmlForIndividualTests', 4, function() {
+    resetGlobals();
+    var test1 = 'foo/nonexistant.html';
+    var test2 = 'bar/nonexistant.html';
+
+    g_currentState.showChrome = false;
+
+    var tests = [test1, test2];
+    equal(htmlForIndividualTests(tests),
+        '<h2><a href="http://trac.webkit.org/browser/trunk/LayoutTests/foo/nonexistant.html" target="_blank">foo/nonexistant.html</a></h2>' +
+        htmlForIndividualTestOnAllBuilders(test1) + 
+        '<div class=expectations test=foo/nonexistant.html>' +
+            '<div><span class=link onclick=\"setQueryParameter(\'showExpectations\', true)\">Show results</span> | ' +
+            '<span class=link onclick=\"setQueryParameter(\'showLargeExpectations\', true)\">Show large thumbnails</span> | ' +
+            '<b>Only shows actual results/diffs from the most recent *failure* on each bot.</b></div>' +
+        '</div>' +
+        '<hr>' +
+        '<h2><a href="http://trac.webkit.org/browser/trunk/LayoutTests/bar/nonexistant.html" target="_blank">bar/nonexistant.html</a></h2>' +
+        htmlForIndividualTestOnAllBuilders(test2) +
+        '<div class=expectations test=bar/nonexistant.html>' +
+            '<div><span class=link onclick=\"setQueryParameter(\'showExpectations\', true)\">Show results</span> | ' +
+            '<span class=link onclick=\"setQueryParameter(\'showLargeExpectations\', true)\">Show large thumbnails</span> | ' +
+            '<b>Only shows actual results/diffs from the most recent *failure* on each bot.</b></div>' +
+        '</div>');
+
+    tests = [test1];
+    equal(htmlForIndividualTests(tests), htmlForIndividualTestOnAllBuilders(test1) +
+        '<div class=expectations test=foo/nonexistant.html>' +
+            '<div><span class=link onclick=\"setQueryParameter(\'showExpectations\', true)\">Show results</span> | ' +
+            '<span class=link onclick=\"setQueryParameter(\'showLargeExpectations\', true)\">Show large thumbnails</span> | ' +
+            '<b>Only shows actual results/diffs from the most recent *failure* on each bot.</b></div>' +
+        '</div>');
+
+    g_currentState.showChrome = true;
+
+    equal(htmlForIndividualTests(tests),
+        '<h2><a href="http://trac.webkit.org/browser/trunk/LayoutTests/foo/nonexistant.html" target="_blank">foo/nonexistant.html</a></h2>' +
+        htmlForIndividualTestOnAllBuildersWithResultsLinks(test1));
+
+    tests = [test1, test2];
+    equal(htmlForIndividualTests(tests),
+        '<h2><a href="http://trac.webkit.org/browser/trunk/LayoutTests/foo/nonexistant.html" target="_blank">foo/nonexistant.html</a></h2>' +
+        htmlForIndividualTestOnAllBuildersWithResultsLinks(test1) + '<hr>' +
+        '<h2><a href="http://trac.webkit.org/browser/trunk/LayoutTests/bar/nonexistant.html" target="_blank">bar/nonexistant.html</a></h2>' +
+        htmlForIndividualTestOnAllBuildersWithResultsLinks(test2));
+});
+
+test('htmlForSingleTestRow', 1, function() {
+    resetGlobals();
+    var builder = 'dummyBuilder';
+    BUILDER_TO_MASTER[builder] = CHROMIUM_WEBKIT_BUILDER_MASTER;
+    var test = createResultsObjectForTest('foo/exists.html', builder);
+    g_currentState.showCorrectExpectations = true;
+    g_resultsByBuilder[builder] = {buildNumbers: [2, 1], webkitRevision: [1234, 1233]};
+    test.rawResults = [[1, 'F'], [2, 'I']];
+    test.rawTimes = [[1, 0], [2, 5]];
+    var expected = '<tr>' +
+        '<td class="test-link"><span class="link" onclick="setQueryParameter(\'tests\',\'foo/exists.html\');">foo/exists.html</span>' +
+        '<td class=options-container><a href="https://bugs.webkit.org/enter_bug.cgi?assigned_to=webkit-unassigned%40lists.webkit.org&product=WebKit&form_name=enter_bug&component=Tools%20%2F%20Tests&short_desc=Layout%20Test%20foo%2Fexists.html%20is%20failing&comment=The%20following%20layout%20test%20is%20failing%20on%20%5Binsert%20platform%5D%0A%0Afoo%2Fexists.html%0A%0AProbable%20cause%3A%0A%0A%5Binsert%20probable%20cause%5D" class="file-bug">FILE BUG</a>' +
+        '<td class=options-container>' +
+            '<td class=options-container>' +
+                '<td><td title="TEXT. Click for more info." class="results F merge" onclick=\'showPopupForBuild(event, "dummyBuilder",0,"foo/exists.html")\'>&nbsp;' +
+                '<td title="IMAGE. Click for more info." class="results I" onclick=\'showPopupForBuild(event, "dummyBuilder",1,"foo/exists.html")\'>5';
+
+    equal(htmlForSingleTestRow(test), expected);
+});
+
+test('lookupVirtualTestSuite', 2, function() {
+    equal(lookupVirtualTestSuite('fast/canvas/foo.html'), '');
+    equal(lookupVirtualTestSuite('platform/chromium/virtual/gpu/fast/canvas/foo.html'), 'platform/chromium/virtual/gpu/fast/canvas');
+});
+
+test('baseTest', 2, function() {
+    equal(baseTest('fast/canvas/foo.html', ''), 'fast/canvas/foo.html');
+    equal(baseTest('platform/chromium/virtual/gpu/fast/canvas/foo.html', 'platform/chromium/virtual/gpu/fast/canvas'), 'fast/canvas/foo.html');
+});
+
+// FIXME: Create builders_tests.js and move this there.
+test('generateChromiumDepsFyiGpuBuildersFromBuilderList', 1, function() {
+    var builderList = ["Linux Audio", "Linux Release (ATI)", "Linux Release (Intel)", "Mac Release (ATI)", "Win7 Audio", "Win7 Release (ATI)", "Win7 Release (Intel)", "WinXP Debug (NVIDIA)", "WinXP Release (NVIDIA)"];
+    var expectedBuilders = [["Linux Release (ATI)", 2], ["Linux Release (Intel)"], ["Mac Release (ATI)"], ["Win7 Release (ATI)"], ["Win7 Release (Intel)"], ["WinXP Debug (NVIDIA)"], ["WinXP Release (NVIDIA)"] ];
+    deepEqual(generateBuildersFromBuilderList(builderList, isChromiumDepsFyiGpuTestRunner), expectedBuilders);
+});
+
+test('generateChromiumTipOfTreeGpuBuildersFromBuilderList', 1, function() {
+    var builderList = ["Chrome Frame Tests", "GPU Linux (NVIDIA)", "GPU Linux (dbg) (NVIDIA)", "GPU Mac", "GPU Mac (dbg)", "GPU Win7 (NVIDIA)", "GPU Win7 (dbg) (NVIDIA)", "Linux Perf",
+        "Linux Tests", "Linux Valgrind", "Mac Builder (dbg)", "Mac10.6 Perf", "Mac10.6 Tests", "Vista Perf", "Vista Tests", "WebKit Linux", "WebKit Linux ASAN", "WebKit Linux (dbg)", "WebKit Linux (deps)",
+        "WebKit Linux 32", "WebKit Mac Builder", "WebKit Mac Builder (dbg)", "WebKit Mac Builder (deps)",
+        "WebKit Mac10.6", "WebKit Mac10.6 (dbg)", "WebKit Mac10.6 (deps)", "WebKit Mac10.7", "WebKit Win", "WebKit Win (dbg)(1)", "WebKit Win (dbg)(2)",
+        "WebKit Win (deps)", "WebKit Win Builder", "WebKit Win Builder (dbg)", "WebKit Win Builder (deps)", "WebKit Win7", "Win (dbg)", "Win Builder"];
+    var expectedBuilders = [["GPU Linux (NVIDIA)", 2], ["GPU Linux (dbg) (NVIDIA)"], ["GPU Mac"], ["GPU Mac (dbg)"], ["GPU Win7 (NVIDIA)"], ["GPU Win7 (dbg) (NVIDIA)"]];
+    deepEqual(generateBuildersFromBuilderList(builderList, isChromiumTipOfTreeGpuTestRunner), expectedBuilders);
+});
+
+test('generateWebkitBuildersFromBuilderList', 1, function() {
+    var builderList = ["Chromium Android Release", "Chromium Linux Release", "Chromium Linux Release (Grid Layout)", "Chromium Linux Release (Perf)", "Chromium Linux Release (Tests)",
+        "Chromium Mac Release", "Chromium Mac Release (Perf)", "Chromium Mac Release (Tests)", "Chromium Win Release", "Chromium Win Release (Perf)", "Chromium Win Release (Tests)",
+        "EFL Linux Release", "GTK Linux 32-bit Release", "GTK Linux 64-bit Debug", "GTK Linux 64-bit Release", "Lion Debug (Build)", "Lion Debug (Tests)", "Lion Debug (WebKit2 Tests)",
+        "Lion Leaks", "Lion Release (Build)", "Lion Release (Perf)", "Lion Release (Tests)", "Lion Release (WebKit2 Tests)", "Qt Linux 64-bit Release (Perf)",
+        "Qt Linux 64-bit Release (WebKit2 Perf)", "Qt Linux ARMv7 Release", "Qt Linux MIPS Release", "Qt Linux Release", "Qt Linux Release minimal", "Qt Linux SH4 Release",
+        "Qt SnowLeopard Release", "Qt Windows 32-bit Debug", "Qt Windows 32-bit Release", "SnowLeopard Intel Debug (Build)", "SnowLeopard Intel Debug (Tests)",
+        "SnowLeopard Intel Debug (WebKit2 Tests)", "SnowLeopard Intel Release (Build)", "SnowLeopard Intel Release (Tests)", "SnowLeopard Intel Release (WebKit2 Tests)",
+        "WinCE Release (Build)", "WinCairo Release", "Windows 7 Release (Tests)", "Windows 7 Release (WebKit2 Tests)", "Windows Debug (Build)", "Windows Release (Build)", "Windows XP Debug (Tests)",
+        "EFL Linux 32-bit Release (Build)", "EFL Linux 64-bit Debug"];
+    var expectedBuilders = [["Chromium Linux Release (Tests)", 2], ["Chromium Mac Release (Tests)"], ["EFL Linux Release"], ["GTK Linux 32-bit Release"], ["GTK Linux 64-bit Debug"],
+        ["GTK Linux 64-bit Release"], ["Lion Debug (Tests)"], ["Lion Debug (WebKit2 Tests)"], ["Lion Release (Tests)"], ["Lion Release (WebKit2 Tests)"], ["Qt Linux Release"],
+        ["SnowLeopard Intel Debug (Tests)"], ["SnowLeopard Intel Debug (WebKit2 Tests)"], ["SnowLeopard Intel Release (Tests)"], ["SnowLeopard Intel Release (WebKit2 Tests)"], ["EFL Linux 64-bit Debug"]];
+    deepEqual(generateBuildersFromBuilderList(builderList, isWebkitTestRunner), expectedBuilders);
+});
+
+test('generateChromiumWebkitTipOfTreeBuildersFromBuilderList', 1, function() {
+    var builderList = ["Chrome Frame Tests", "GPU Linux (NVIDIA)", "GPU Linux (dbg) (NVIDIA)", "GPU Mac", "GPU Mac (dbg)", "GPU Win7 (NVIDIA)", "GPU Win7 (dbg) (NVIDIA)", "Linux Perf", "Linux Tests",
+        "Linux Valgrind", "Mac Builder (dbg)", "Mac10.6 Perf", "Mac10.6 Tests", "Vista Perf", "Vista Tests", "WebKit Linux", "WebKit Linux ASAN",  "WebKit Linux (dbg)", "WebKit Linux (deps)", "WebKit Linux 32",
+        "WebKit Mac Builder", "WebKit Mac Builder (dbg)", "WebKit Mac Builder (deps)", "WebKit Mac10.6", "WebKit Mac10.6 (dbg)",
+        "WebKit Mac10.6 (deps)", "WebKit Mac10.7", "WebKit Win", "WebKit Win (dbg)(1)", "WebKit Win (dbg)(2)", "WebKit Win (deps)", "WebKit Win Builder", "WebKit Win Builder (dbg)",
+        "WebKit Win Builder (deps)", "WebKit Win7", "Win (dbg)", "Win Builder",
+        "Linux (Content Shell)"];
+    var expectedBuilders = [["WebKit Linux", 2], ["WebKit Linux (dbg)"], ["WebKit Linux 32"], ["WebKit Mac10.6"],
+        ["WebKit Mac10.6 (dbg)"], ["WebKit Mac10.7"], ["WebKit Win"], ["WebKit Win (dbg)(1)"], ["WebKit Win (dbg)(2)"], ["WebKit Win7"]];
+    deepEqual(generateBuildersFromBuilderList(builderList, isChromiumWebkitTipOfTreeTestRunner), expectedBuilders);
+});
+
+test('generateChromiumWebkitDepsBuildersFromBuilderList', 1, function() {
+    var builderList = ["Chrome Frame Tests", "GPU Linux (NVIDIA)", "GPU Linux (dbg) (NVIDIA)", "GPU Mac", "GPU Mac (dbg)", "GPU Win7 (NVIDIA)", "GPU Win7 (dbg) (NVIDIA)", "Linux Perf", "Linux Tests",
+        "Linux Valgrind", "Mac Builder (dbg)", "Mac10.6 Perf", "Mac10.6 Tests", "Vista Perf", "Vista Tests", "WebKit Linux", "WebKit Linux ASAN",  "WebKit Linux (dbg)", "WebKit Linux (deps)", "WebKit Linux 32",
+        "WebKit Mac Builder", "WebKit Mac Builder (dbg)", "WebKit Mac Builder (deps)", "WebKit Mac10.6", "WebKit Mac10.6 (dbg)",
+        "WebKit Mac10.6 (deps)", "WebKit Mac10.7", "WebKit Win", "WebKit Win (dbg)(1)", "WebKit Win (dbg)(2)", "WebKit Win (deps)", "WebKit Win Builder", "WebKit Win Builder (dbg)",
+        "WebKit Win Builder (deps)", "WebKit Win7", "Win (dbg)", "Win Builder"];
+    var expectedBuilders = [["WebKit Linux (deps)", 2], ["WebKit Mac10.6 (deps)"], ["WebKit Win (deps)"]];
+    deepEqual(generateBuildersFromBuilderList(builderList, isChromiumWebkitDepsTestRunner), expectedBuilders);
+});
+
+test('generateChromiumDepsGTestBuildersFromBuilderList', 1, function() {
+    var builderList = ["Android Builder", "Chrome Frame Tests (ie6)", "Chrome Frame Tests (ie7)", "Chrome Frame Tests (ie8)", "Interactive Tests (dbg)", "Linux", "Linux Builder (dbg)",
+        "Linux Builder (dbg)(shared)", "Linux Builder x64", "Linux Clang (dbg)", "Linux Sync", "Linux Tests (dbg)(1)", "Linux Tests (dbg)(2)", "Linux Tests (dbg)(shared)", "Linux Tests x64",
+        "Linux x64", "Mac", "Mac 10.6 Tests (dbg)(1)", "Mac 10.6 Tests (dbg)(2)",
+        "Mac 10.6 Tests (dbg)(3)", "Mac 10.6 Tests (dbg)(4)", "Mac Builder", "Mac Builder (dbg)", "Mac10.6 Sync",
+        "Mac10.6 Tests (1)", "Mac10.6 Tests (2)", "Mac10.6 Tests (3)", "NACL Tests", "NACL Tests (x64)", "Vista Tests (1)", "Vista Tests (2)", "Vista Tests (3)", "Win", "Win Aura",
+        "Win Builder", "Win Builder (dbg)", "Win Builder 2010 (dbg)", "Win7 Sync", "Win7 Tests (1)", "Win7 Tests (2)", "Win7 Tests (3)", "Win7 Tests (dbg)(1)", "Win7 Tests (dbg)(2)",
+        "Win7 Tests (dbg)(3)", "Win7 Tests (dbg)(4)", "Win7 Tests (dbg)(5)", "Win7 Tests (dbg)(6)", "XP Tests (1)", "XP Tests (2)", "XP Tests (3)", "XP Tests (dbg)(1)", "XP Tests (dbg)(2)",
+        "XP Tests (dbg)(3)", "XP Tests (dbg)(4)", "XP Tests (dbg)(5)", "XP Tests (dbg)(6)"];
+    var expectedBuilders = [["Interactive Tests (dbg)", 2], ["Linux Sync"], ["Linux Tests (dbg)(1)"], ["Linux Tests (dbg)(2)"], ["Linux Tests (dbg)(shared)"], ["Linux Tests x64"],
+        ["Mac 10.6 Tests (dbg)(1)"], ["Mac 10.6 Tests (dbg)(2)"], ["Mac 10.6 Tests (dbg)(3)"],
+        ["Mac 10.6 Tests (dbg)(4)"], ["Mac10.6 Sync"], ["Mac10.6 Tests (1)"], ["Mac10.6 Tests (2)"], ["Mac10.6 Tests (3)"], ["NACL Tests"],
+        ["NACL Tests (x64)"], ["Vista Tests (1)"], ["Vista Tests (2)"], ["Vista Tests (3)"], ["Win7 Sync"], ["Win7 Tests (1)"], ["Win7 Tests (2)"], ["Win7 Tests (3)"], ["Win7 Tests (dbg)(1)"],
+        ["Win7 Tests (dbg)(2)"], ["Win7 Tests (dbg)(3)"], ["Win7 Tests (dbg)(4)"], ["Win7 Tests (dbg)(5)"], ["Win7 Tests (dbg)(6)"], ["XP Tests (1)"], ["XP Tests (2)"], ["XP Tests (3)"],
+        ["XP Tests (dbg)(1)"], ["XP Tests (dbg)(2)"], ["XP Tests (dbg)(3)"], ["XP Tests (dbg)(4)"], ["XP Tests (dbg)(5)"], ["XP Tests (dbg)(6)"]];
+    deepEqual(generateBuildersFromBuilderList(builderList, isChromiumDepsGTestRunner), expectedBuilders);
+});
+
+test('generateChromiumDepsCrosGTestBuildersFromBuilderList', 1, function() {
+    var builderList = ["ChromiumOS (amd64)", "ChromiumOS (arm)", "ChromiumOS (tegra2)", "ChromiumOS (x86)", "Linux ChromiumOS (Clang dbg)", "Linux ChromiumOS Builder", "Linux ChromiumOS Builder (dbg)",
+        "Linux ChromiumOS Tests (1)", "Linux ChromiumOS Tests (2)", "Linux ChromiumOS Tests (dbg)(1)", "Linux ChromiumOS Tests (dbg)(2)", "Linux ChromiumOS Tests (dbg)(3)"];
+    var expectedBuilders = [["Linux ChromiumOS Tests (1)", 2], ["Linux ChromiumOS Tests (2)"], ["Linux ChromiumOS Tests (dbg)(1)"], ["Linux ChromiumOS Tests (dbg)(2)"], ["Linux ChromiumOS Tests (dbg)(3)"]];
+    deepEqual(generateBuildersFromBuilderList(builderList, isChromiumDepsCrosGTestRunner), expectedBuilders);
+});
+
+test('generateChromiumTipOfTreeGTestBuildersFromBuilderList', 1, function() {
+    var builderList = ["Chrome Frame Tests", "GPU Linux (NVIDIA)", "GPU Linux (dbg) (NVIDIA)", "GPU Mac", "GPU Mac (dbg)", "GPU Win7 (NVIDIA)", "GPU Win7 (dbg) (NVIDIA)", "Linux Perf",
+        "Linux Tests", "Linux Valgrind", "Mac Builder (dbg)", "Mac10.6 Perf", "Mac10.6 Tests", "Vista Perf", "Vista Tests", "WebKit Linux", "WebKit Linux (dbg)", "WebKit Linux (deps)",
+        "WebKit Linux 32", "WebKit Mac Builder", "WebKit Mac Builder (dbg)", "WebKit Mac Builder (deps)",
+        "WebKit Mac10.6", "WebKit Mac10.6 (dbg)", "WebKit Mac10.6 (deps)", "WebKit Mac10.7", "WebKit Win", "WebKit Win (dbg)(1)", "WebKit Win (dbg)(2)",
+        "WebKit Win (deps)", "WebKit Win Builder", "WebKit Win Builder (dbg)", "WebKit Win Builder (deps)", "WebKit Win7", "Win (dbg)", "Win Builder"];
+    var expectedBuilders = [['Linux Tests', BuilderGroup.DEFAULT_BUILDER], ['Mac10.6 Tests'], ['Vista Tests'], ['Win (dbg)']];
+    deepEqual(generateBuildersFromBuilderList(builderList, isChromiumTipOfTreeGTestRunner), expectedBuilders);
+});
+
+test('queryHashAsMap', 2, function() {
+    equal(window.location.hash, '#useTestData=true');
+    deepEqual(queryHashAsMap(), {useTestData: 'true'});
+});
+
+test('parseCrossDashboardParameters', 2, function() {
+    equal(window.location.hash, '#useTestData=true');
+    parseCrossDashboardParameters();
+
+    var expectedParameters = {};
+    for (var key in g_defaultCrossDashboardStateValues)
+        expectedParameters[key] = g_defaultCrossDashboardStateValues[key];
+    expectedParameters.useTestData = true;
+
+    deepEqual(g_crossDashboardState, expectedParameters);
+});
+
+test('diffStates', 5, function() {
+    var newState = {a: 1, b: 2};
+    deepEqual(diffStates(null, newState), newState);
+
+    var oldState = {a: 1};
+    deepEqual(diffStates(oldState, newState), {b: 2});
+
+    // FIXME: This is kind of weird. I think the existing users of this code work correctly, but it's a confusing result.
+    var oldState = {c: 1};
+    deepEqual(diffStates(oldState, newState), {a:1, b: 2});
+
+    var oldState = {a: 1, b: 2};
+    deepEqual(diffStates(oldState, newState), {});
+
+    var oldState = {a: 2, b: 3};
+    deepEqual(diffStates(oldState, newState), {a: 1, b: 2});
+});
+
+test('addBuilderLoadErrors', 1, function() {
+    clearErrors();
+    g_hasDoneInitialPageGeneration = false;
+    g_buildersThatFailedToLoad = ['builder1', 'builder2'];
+    g_staleBuilders = ['staleBuilder1'];
+    addBuilderLoadErrors();
+    equal(g_errorMessages, 'ERROR: Failed to get data from builder1,builder2.<br>ERROR: Data from staleBuilder1 is more than 1 day stale.<br>');
+});
+
+test('builderGroupIsToTWebKitAttribute', 2, function() {
+    var dummyMaster = new BuilderMaster('dummy.org', 'http://build.dummy.org');
+    var testBuilderGroups = {
+        '@ToT - dummy.org': new BuilderGroup(BuilderGroup.TOT_WEBKIT),
+        '@DEPS - dummy.org': new BuilderGroup(BuilderGroup.DEPS_WEBKIT),
+    }
+    testBuilderGroups['@ToT - dummy.org'].expectedGroups = 1;
+    testBuilderGroups['@DEPS - dummy.org'].expectedGroups = 1;
+
+    var testJSONData = "{ \"Dummy Builder 1\": null, \"Dummy Builder 2\": null }";
+    onBuilderListLoad(testBuilderGroups, function() { return true; }, dummyMaster, '@ToT - dummy.org', {responseText: testJSONData});
+    equal(testBuilderGroups['@ToT - dummy.org'].isToTWebKit, true);
+    onBuilderListLoad(testBuilderGroups, function() { return true; }, dummyMaster, '@DEPS - dummy.org', {responseText: testJSONData});
+    equal(testBuilderGroups['@DEPS - dummy.org'].isToTWebKit, false);
+});
+
+test('builderGroupExpectedGroups', 4, function() {
+    var dummyMaster = new BuilderMaster('dummy.org', 'http://build.dummy.org');
+    var testBuilderGroups = {
+        '@ToT - dummy.org': new BuilderGroup(BuilderGroup.TOT_WEBKIT),
+    }
+    testBuilderGroups['@ToT - dummy.org'].expectedGroups = 3;
+
+    var testJSONData = "{ \"Dummy Builder 1\": null }";
+    equal(testBuilderGroups['@ToT - dummy.org'].expectedGroups, 3);
+    onBuilderListLoad(testBuilderGroups,  function() { return true; }, dummyMaster, '@ToT - dummy.org', {responseText: testJSONData});
+    equal(testBuilderGroups['@ToT - dummy.org'].groups, 1);
+    var testJSONData = "{ \"Dummy Builder 2\": null }";
+    onBuilderListLoad(testBuilderGroups,  function() { return true; }, dummyMaster, '@ToT - dummy.org', {responseText: testJSONData});
+    equal(testBuilderGroups['@ToT - dummy.org'].groups, 2);
+    onErrorLoadingBuilderList('http://build.dummy.org', testBuilderGroups,  '@ToT - dummy.org');
+    equal(testBuilderGroups['@ToT - dummy.org'].groups, 3);
+});
+
+test('requestBuilderListAddsBuilderGroupEntry', 2, function() {
+    var testBuilderGroups = { '@ToT - dummy.org': null };
+
+    var requestFunction = loader.request;
+    loader.request = function() {};
+
+    try {
+        var builderFilter = null;
+        var master = { builderJsonPath: function() {} };
+        var groupName = '@ToT - dummy.org';
+        var builderGroup = { expectedGroups: 0 };
+        requestBuilderList(testBuilderGroups, builderFilter, master, groupName, builderGroup);
+
+        equal(testBuilderGroups['@ToT - dummy.org'], builderGroup);
+        equal(testBuilderGroups['@ToT - dummy.org'].expectedGroups, 1);
+    } finally {
+        loader.request = requestFunction;
+    }
+})
+
+test('sortTests', 4, function() {
+    var test1 = createResultsObjectForTest('foo/test1.html', 'dummyBuilder');
+    var test2 = createResultsObjectForTest('foo/test2.html', 'dummyBuilder');
+    var test3 = createResultsObjectForTest('foo/test3.html', 'dummyBuilder');
+    test1.modifiers = 'b';
+    test2.modifiers = 'a';
+    test3.modifiers = '';
+
+    var tests = [test1, test2, test3];
+    sortTests(tests, 'modifiers', FORWARD);
+    deepEqual(tests, [test2, test1, test3]);
+    sortTests(tests, 'modifiers', BACKWARD);
+    deepEqual(tests, [test3, test1, test2]);
+
+    test1.bugs = 'b';
+    test2.bugs = 'a';
+    test3.bugs = '';
+
+    var tests = [test1, test2, test3];
+    sortTests(tests, 'bugs', FORWARD);
+    deepEqual(tests, [test2, test1, test3]);
+    sortTests(tests, 'bugs', BACKWARD);
+    deepEqual(tests, [test3, test1, test2]);
+});
+
+test('popup', 2, function() {
+    showPopup(document.body, 'dummy content');
+    ok(document.querySelector('#popup'));
+    hidePopup();
+    ok(!document.querySelector('#popup'));
+});
+
+test('gpuResultsPath', 3, function() {
+  equal(gpuResultsPath('777777', 'Win7 Release (ATI)'), '777777_Win7_Release_ATI_');
+  equal(gpuResultsPath('123', 'GPU Linux (dbg)(NVIDIA)'), '123_GPU_Linux_dbg_NVIDIA_');
+  equal(gpuResultsPath('12345', 'GPU Mac'), '12345_GPU_Mac');
+});
+
+test('TestTrie', 3, function() {
+    var builders = {
+        "Dummy Chromium Windows Builder": true,
+        "Dummy GTK Linux Builder": true,
+        "Dummy Apple Mac Lion Builder": true
+    };
+
+    var resultsByBuilder = {
+        "Dummy Chromium Windows Builder": {
+            tests: {
+                "foo": true,
+                "foo/bar/1.html": true,
+                "foo/bar/baz": true
+            }
+        },
+        "Dummy GTK Linux Builder": {
+            tests: {
+                "bar": true,
+                "foo/1.html": true,
+                "foo/bar/2.html": true,
+                "foo/bar/baz/1.html": true,
+            }
+        },
+        "Dummy Apple Mac Lion Builder": {
+            tests: {
+                "foo/bar/3.html": true,
+                "foo/bar/baz/foo": true,
+            }
+        }
+    };
+    var expectedTrie = {
+        "foo": {
+            "bar": {
+                "1.html": true,
+                "2.html": true,
+                "3.html": true,
+                "baz": {
+                    "1.html": true,
+                    "foo": true
+                }
+            },
+            "1.html": true
+        },
+        "bar": true
+    }
+
+    var trie = new TestTrie(builders, resultsByBuilder);
+    deepEqual(trie._trie, expectedTrie);
+
+    var leafsOfCompleteTrieTraversal = [];
+    var expectedLeafs = ["foo/bar/1.html", "foo/bar/baz/1.html", "foo/bar/baz/foo", "foo/bar/2.html", "foo/bar/3.html", "foo/1.html", "bar"];
+    trie.forEach(function(triePath) {
+        leafsOfCompleteTrieTraversal.push(triePath);
+    });
+    deepEqual(leafsOfCompleteTrieTraversal, expectedLeafs);
+
+    var leafsOfPartialTrieTraversal = [];
+    expectedLeafs = ["foo/bar/1.html", "foo/bar/baz/1.html", "foo/bar/baz/foo", "foo/bar/2.html", "foo/bar/3.html"];
+    trie.forEach(function(triePath) {
+        leafsOfPartialTrieTraversal.push(triePath);
+    }, "foo/bar");
+    deepEqual(leafsOfPartialTrieTraversal, expectedLeafs);
+});
diff --git a/Tools/TestResultServer/static-dashboards/loader.js b/Tools/TestResultServer/static-dashboards/loader.js
new file mode 100644
index 0000000..be9e708
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/loader.js
@@ -0,0 +1,247 @@
+// Copyright (C) 2012 Google Inc. All rights reserved.
+// Copyright (C) 2012 Zan Dobersek <zandobersek@gmail.com>
+//
+// 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.
+
+var loader = loader || {};
+
+(function() {
+
+var TEST_RESULTS_SERVER = 'http://test-results.appspot.com/';
+var CHROMIUM_EXPECTATIONS_URL = 'http://svn.webkit.org/repository/webkit/trunk/LayoutTests/platform/chromium/TestExpectations';
+
+function pathToBuilderResultsFile(builderName) {
+    return TEST_RESULTS_SERVER + 'testfile?builder=' + builderName +
+           '&master=' + builderMaster(builderName).name +
+           '&testtype=' + g_crossDashboardState.testType + '&name=';
+}
+
+loader.request = function(url, success, error, opt_isBinaryData)
+{
+    var xhr = new XMLHttpRequest();
+    xhr.open('GET', url, true);
+    if (opt_isBinaryData)
+        xhr.overrideMimeType('text/plain; charset=x-user-defined');
+    xhr.onreadystatechange = function(e) {
+        if (xhr.readyState == 4) {
+            if (xhr.status == 200)
+                success(xhr);
+            else
+                error(xhr);
+        }
+    }
+    xhr.send();
+}
+
+loader.Loader = function()
+{
+    this._loadingSteps = [
+        this._loadBuildersList,
+        this._loadResultsFiles,
+        this._loadExpectationsFiles,
+    ];
+}
+
+loader.Loader.prototype = {
+    load: function()
+    {
+        this._loadNext();
+    },
+    buildersListLoaded: function()
+    {
+        initBuilders();
+        this._loadNext();
+    },
+    _loadNext: function()
+    {
+        var loadingStep = this._loadingSteps.shift();
+        if (!loadingStep) {
+            resourceLoadingComplete();
+            return;
+        }
+        loadingStep.apply(this);
+    },
+    _loadBuildersList: function()
+    {
+        loadBuildersList(g_crossDashboardState.group, g_crossDashboardState.testType);
+    },
+    _loadResultsFiles: function()
+    {
+        parseParameters();
+
+        for (var builderName in g_builders)
+            this._loadResultsFileForBuilder(builderName);
+    },
+    _loadResultsFileForBuilder: function(builderName)
+    {
+        var resultsFilename;
+        if (isTreeMap())
+            resultsFilename = 'times_ms.json';
+        else if (g_crossDashboardState.showAllRuns)
+            resultsFilename = 'results.json';
+        else
+            resultsFilename = 'results-small.json';
+
+        var resultsFileLocation = pathToBuilderResultsFile(builderName) + resultsFilename;
+        loader.request(resultsFileLocation,
+                partial(function(loader, builderName, xhr) {
+                    loader._handleResultsFileLoaded(builderName, xhr.responseText);
+                }, this, builderName),
+                partial(function(loader, builderName, xhr) {
+                    loader._handleResultsFileLoadError(builderName);
+                }, this, builderName));
+    },
+    _handleResultsFileLoaded: function(builderName, fileData)
+    {
+        if (isTreeMap())
+            this._processTimesJSONData(builderName, fileData);
+        else
+            this._processResultsJSONData(builderName, fileData);
+
+        // We need this work-around for webkit.org/b/50589.
+        if (!g_resultsByBuilder[builderName]) {
+            this._handleResultsFileLoadError(builderName);
+            return;
+        }
+
+        this._handleResourceLoad();
+    },
+    _processTimesJSONData: function(builderName, fileData)
+    {
+        // FIXME: We should probably include the builderName in the JSON
+        // rather than relying on only loading one JSON file per page.
+        g_resultsByBuilder[builderName] = JSON.parse(fileData);
+    },
+    _processResultsJSONData: function(builderName, fileData)
+    {
+        var builds = JSON.parse(fileData);
+
+        var json_version = builds['version'];
+        for (var builderName in builds) {
+            if (builderName == 'version')
+                continue;
+
+            // If a test suite stops being run on a given builder, we don't want to show it.
+            // Assume any builder without a run in two weeks for a given test suite isn't
+            // running that suite anymore.
+            // FIXME: Grab which bots run which tests directly from the buildbot JSON instead.
+            var lastRunSeconds = builds[builderName].secondsSinceEpoch[0];
+            if ((Date.now() / 1000) - lastRunSeconds > ONE_WEEK_SECONDS)
+                continue;
+
+            if ((Date.now() / 1000) - lastRunSeconds > ONE_DAY_SECONDS)
+                g_staleBuilders.push(builderName);
+
+            if (json_version >= 4)
+                builds[builderName][TESTS_KEY] = flattenTrie(builds[builderName][TESTS_KEY]);
+            g_resultsByBuilder[builderName] = builds[builderName];
+        }
+    },
+    _handleResultsFileLoadError: function(builderName)
+    {
+        var error = 'Failed to load results file for ' + builderName + '.';
+
+        if (isLayoutTestResults()) {
+            console.error(error);
+            g_buildersThatFailedToLoad.push(builderName);
+        } else {
+            // Avoid to show error/warning messages for non-layout tests. We may be
+            // checking the builders that are not running the tests.
+            console.info('info:' + error);
+        }
+
+        // Remove this builder from builders, so we don't try to use the
+        // data that isn't there.
+        delete g_builders[builderName];
+
+        // Change the default builder name if it has been deleted.
+        if (g_defaultBuilderName == builderName) {
+            g_defaultBuilderName = null;
+            for (var availableBuilderName in g_builders) {
+                g_defaultBuilderName = availableBuilderName;
+                g_defaultDashboardSpecificStateValues.builder = availableBuilderName;
+                break;
+            }
+            if (!g_defaultBuilderName) {
+                var error = 'No tests results found for ' + g_crossDashboardState.testType + '. Reload the page to try fetching it again.';
+                console.error(error);
+                addError(error);
+            }
+        }
+
+        // Proceed as if the resource had loaded.
+        this._handleResourceLoad();
+    },
+    _handleResourceLoad: function()
+    {
+        if (this._haveResultsFilesLoaded())
+            this._loadNext();
+    },
+    _haveResultsFilesLoaded: function()
+    {
+        for (var builder in g_builders) {
+            if (!g_resultsByBuilder[builder])
+                return false;
+        }
+        return true;
+    },
+    _loadExpectationsFiles: function()
+    {
+        if (!isFlakinessDashboard() && !g_crossDashboardState.useTestData) {
+            this._loadNext();
+            return;
+        }
+
+        var expectationsFilesToRequest = {};
+        traversePlatformsTree(function(platform, platformName) {
+            if (platform.fallbackPlatforms)
+                platform.fallbackPlatforms.forEach(function(fallbackPlatform) {
+                    var fallbackPlatformObject = platformObjectForName(fallbackPlatform);
+                    if (fallbackPlatformObject.expectationsDirectory && !(fallbackPlatform in expectationsFilesToRequest))
+                        expectationsFilesToRequest[fallbackPlatform] = EXPECTATIONS_URL_BASE_PATH + fallbackPlatformObject.expectationsDirectory + '/TestExpectations';
+                });
+
+            if (platform.expectationsDirectory)
+                expectationsFilesToRequest[platformName] = EXPECTATIONS_URL_BASE_PATH + platform.expectationsDirectory + '/TestExpectations';
+        });
+
+        for (platformWithExpectations in expectationsFilesToRequest)
+            loader.request(expectationsFilesToRequest[platformWithExpectations],
+                    partial(function(loader, platformName, xhr) {
+                        g_expectationsByPlatform[platformName] = getParsedExpectations(xhr.responseText);
+
+                        delete expectationsFilesToRequest[platformName];
+                        if (!Object.keys(expectationsFilesToRequest).length)
+                            loader._loadNext();
+                    }, this, platformWithExpectations),
+                    partial(function(platformName, xhr) {
+                        console.error('Could not load expectations file for ' + platformName);
+                    }, platformWithExpectations));
+    }
+}
+
+})();
diff --git a/Tools/TestResultServer/static-dashboards/loader_unittests.js b/Tools/TestResultServer/static-dashboards/loader_unittests.js
new file mode 100644
index 0000000..e2f546c
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/loader_unittests.js
@@ -0,0 +1,106 @@
+// Copyright (C) Zan Dobersek <zandobersek@gmail.com>
+//
+// 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.
+
+module('loader');
+
+test('loading steps', 1, function() {
+    var loadedSteps = [];
+    var resourceLoader = new loader.Loader();
+    function loadingStep1() {
+        loadedSteps.push('step 1');
+        resourceLoader.load();
+    }
+    function loadingStep2() {
+        loadedSteps.push('step 2');
+        resourceLoader.load();
+    }
+
+    var loadingCompleteCallback = resourceLoadingComplete;
+    resourceLoadingComplete = function() {
+        deepEqual(loadedSteps, ['step 1', 'step 2']);
+    }
+
+    try {
+        resourceLoader._loadingSteps = [loadingStep1, loadingStep2];
+        resourceLoader.load();
+    } finally {
+        resourceLoadingComplete = loadingCompleteCallback;
+    }
+});
+
+test('results files loading', 5, function() {
+    var expectedLoadedBuilders = ["WebKit Linux", "WebKit Win"];
+    var loadedBuilders = [];
+    var resourceLoader = new loader.Loader();
+    resourceLoader._loadNext = function() {
+        deepEqual(loadedBuilders.sort(), expectedLoadedBuilders);
+        loadedBuilders.forEach(function(builderName) {
+            ok('secondsSinceEpoch' in g_resultsByBuilder[builderName]);
+            deepEqual(g_resultsByBuilder[builderName].tests, {});
+        });
+    }
+
+    var requestFunction = loader.request;
+    loader.request = function(url, successCallback, errorCallback) {
+        var builderName = /builder=([\w ]+)&/.exec(url)[1];
+        loadedBuilders.push(builderName);
+        successCallback({responseText: '{"version": 4, "' + builderName + '": {"secondsSinceEpoch": [' + Date.now() + '], "tests": {}}}'});
+    }
+
+    g_builders = {"WebKit Linux": true, "WebKit Win": true};
+
+    try {
+        resourceLoader._loadResultsFiles();
+    } finally {
+        g_builders = undefined;
+        g_resultsByBuilder = undefined;
+        loader.request = requestFunction;
+    }
+});
+
+test('expectations files loading', 1, function() {
+    var expectedLoadedPlatforms = ["chromium", "chromium-android", "efl", "efl-wk1", "efl-wk2", "gtk",
+                                   "gtk-wk2", "mac", "mac-lion", "mac-snowleopard", "qt", "win", "wk2"];
+    var loadedPlatforms = [];
+    var resourceLoader = new loader.Loader();
+    resourceLoader._loadNext = function() {
+        deepEqual(loadedPlatforms.sort(), expectedLoadedPlatforms);
+    }
+
+    var requestFunction = loader.request;
+    loader.request = function(url, successCallback, errorCallback) {
+        loadedPlatforms.push(/LayoutTests\/platform\/(.+)\/TestExpectations/.exec(url)[1]);
+        successCallback({responseText: ''});
+    }
+
+    try {
+        resourceLoader._loadExpectationsFiles();
+    } finally {
+        loader.request = requestFunction;
+    }
+});
diff --git a/Tools/TestResultServer/static-dashboards/run-embedded-unittests.html b/Tools/TestResultServer/static-dashboards/run-embedded-unittests.html
new file mode 100644
index 0000000..835be21
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/run-embedded-unittests.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2012 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:
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. 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.
+
+THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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.
+-->
+<html>
+<head>
+<link rel="stylesheet" href="../../../Source/ThirdParty/qunit/qunit/qunit.css">
+<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script> 
+<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.15/jquery-ui.min.js"></script>
+<script src="../../../Source/ThirdParty/qunit/qunit/qunit.js"></script>
+</head>
+<body>
+<h1 id="qunit-header">Test Results Server JavaScript Unit Tests</h1>
+<h2 id="qunit-banner"></h2>
+<div id="qunit-testrunner-toolbar"></div>
+<h2 id="qunit-userAgent"></h2>
+<ol id="qunit-tests"></ol>
+
+<link rel="stylesheet" href="flakiness_dashboard.css"></link>
+<link rel="stylesheet" href="flakiness_dashboard_tests.css"></link>
+<script src="builders.js"></script>
+
+<script>
+// Don't request the actual builders off the bots when running unittests.
+function loadBuildersList() {};
+function g_handleBuildersListLoaded() {};
+
+// Mimic being embedded. All our embedded checks compare window and parent.
+window.parent = null;
+</script>
+
+<script src="dashboard_base.js"></script>
+<script src="flakiness_dashboard.js"></script>
+
+<script>
+window.location.href = '#useTestData=true';
+var builderGroup = '@ToT - chromium.org';
+var builders = {'Webkit Linux': '', 'Webkit Linux (dbg)': '', 'Webkit Mac10.7': '', 'Webkit Win': ''};
+onBuilderListLoad(LAYOUT_TESTS_BUILDER_GROUPS, isChromiumWebkitTipOfTreeTestRunner, CHROMIUM_WEBKIT_BUILDER_MASTER, builderGroup, BuilderGroup.TOT_WEBKIT, builders);
+initBuilders();
+</script>
+
+<!-- FIXME: Split this up into multiple unittest.js, e.g. one for builders.js and one for dashboard_base.js. -->
+<script src="flakiness_dashboard_embedded_unittests.js"></script>
+</body>
+</html>
diff --git a/Tools/TestResultServer/static-dashboards/run-unittests.html b/Tools/TestResultServer/static-dashboards/run-unittests.html
new file mode 100644
index 0000000..9999c71
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/run-unittests.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2012 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:
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. 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.
+
+THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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.
+-->
+<html>
+<head>
+<link rel="stylesheet" href="../../../Source/ThirdParty/qunit/qunit/qunit.css">
+<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script> 
+<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.15/jquery-ui.min.js"></script>
+<script src="../../../Source/ThirdParty/qunit/qunit/qunit.js"></script>
+</head>
+<body>
+<h1 id="qunit-header">Test Results Server JavaScript Unit Tests</h1>
+<h2 id="qunit-banner"></h2>
+<div id="qunit-testrunner-toolbar"></div>
+<h2 id="qunit-userAgent"></h2>
+<ol id="qunit-tests"></ol>
+
+<link rel="stylesheet" href="flakiness_dashboard.css"></link>
+<link rel="stylesheet" href="flakiness_dashboard_tests.css"></link>
+<script src="builders.js"></script>
+
+<script>
+// Don't request the actual builders off the bots when running unittests.
+function loadBuildersList() {};
+</script>
+
+<script src="dashboard_base.js"></script>
+<script src="loader.js"></script>
+<script src="flakiness_dashboard.js"></script>
+
+<script>
+window.location.href = '#useTestData=true';
+var groupName = '@ToT - chromium.org';
+var builders = '{"WebKit Linux": true, "WebKit Linux (dbg)": true, "WebKit Mac10.7": true, "WebKit Win": true}';
+LAYOUT_TESTS_BUILDER_GROUPS[groupName] = new BuilderGroup(BuilderGroup.TOT_WEBKIT);
+LAYOUT_TESTS_BUILDER_GROUPS[groupName].expectedGroups = 4;
+onBuilderListLoad(LAYOUT_TESTS_BUILDER_GROUPS, isChromiumWebkitTipOfTreeTestRunner, CHROMIUM_WEBKIT_BUILDER_MASTER, groupName, {responseText: builders});
+initBuilders();
+</script>
+
+<!-- FIXME: Split this up into multiple unittest.js, e.g. one for builders.js and one for dashboard_base.js. -->
+<script src="flakiness_dashboard_unittests.js"></script>
+<script src="loader_unittests.js"></script>
+</body>
+</html>
diff --git a/Tools/TestResultServer/static-dashboards/timeline_explorer.html b/Tools/TestResultServer/static-dashboards/timeline_explorer.html
new file mode 100644
index 0000000..b3fa41b
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/timeline_explorer.html
@@ -0,0 +1,460 @@
+<!-- Copyright (C) 2011 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.
+-->
+<!DOCTYPE HTML>
+<html>
+
+<head>
+<title>Test Timeline Explorer</title>
+<style>
+body {
+    font-family: Helvetica, Arial, sans-serif;
+    font-size: 12px;
+}
+
+#timeline-container {
+    position: relative;
+}
+
+#inspector-container {
+    width: 300px;
+    float: right;
+    border-left: 1px dashed;
+    padding-left: 5px;
+    margin-left: 5px;
+}
+
+#inspector-container caption {
+    background: #eee;
+    font-weight: bold;
+    padding: 2px;
+    font-size: 14px;
+    text-align: left;
+}
+
+#inspector-table {
+    width: 100%;
+}
+
+#inspector-table td {
+    white-space: nowrap;
+}
+
+#inspector-table .label {
+    color: #666;
+    text-align: right;
+    width: 10em;
+}
+
+#inspector-table .delta.positive {
+    color: #090;
+}
+
+#inspector-table .delta.negative {
+    color: #900;
+}
+
+#inspector-container .buttons {
+    padding: 1em 0;
+    text-align: center;
+}
+
+#inspector-container #build-buttons {
+    border-top: 1px dashed;
+}
+
+#indicator {
+    top: 0;
+    width: 1px;
+    position: absolute;
+    background: red;
+}
+</style>
+<script src="dygraph-combined.js"></script>
+<script src="builders.js"></script>
+<script src="loader.js"></script>
+<script src="dashboard_base.js"></script>
+<script>
+var FAILING_TESTS_DATASET_NAME = 'Failing tests';
+
+var g_dygraph;
+var g_buildIndicesByTimestamp = {};
+var g_currentBuildIndex = -1;
+var g_currentBuilderTestResults;
+
+//////////////////////////////////////////////////////////////////////////////
+// Methods and objects from dashboard_base.js to override.
+//////////////////////////////////////////////////////////////////////////////
+function generatePage()
+{
+    g_buildIndicesByTimestamp = {};
+    var results = g_resultsByBuilder[g_currentState.builder];
+
+    for (var i = 0; i < results[FIXABLE_COUNTS_KEY].length; i++) {
+        var buildDate = new Date(results[TIMESTAMPS_KEY][i] * 1000);
+        g_buildIndicesByTimestamp[buildDate.getTime()] = i;
+    }
+
+    if (g_currentState.buildTimestamp != -1 && g_currentState.buildTimestamp in g_buildIndicesByTimestamp) {
+        var newBuildIndex = g_buildIndicesByTimestamp[g_currentState.buildTimestamp];
+
+        if (newBuildIndex == g_currentBuildIndex) {
+            // This happens when selectBuild is called, which updates the UI
+            // immediately, in addition to updating the location hash (we don't
+            // just rely on the hash change since we don't want to regenerate the
+            // whole page just because the user clicked on something)
+            return;
+        } else if (newBuildIndex)
+            g_currentBuildIndex = newBuildIndex;
+    }
+
+    initCurrentBuilderTestResults();
+
+    $('test-type-switcher').innerHTML = htmlForTestTypeSwitcher( false,
+        checkboxHTML('ignoreFlakyTests', 'Ignore flaky tests', g_currentState.ignoreFlakyTests, 'g_currentBuildIndex = -1')
+    );
+
+    updateTimelineForBuilder();
+}
+
+function initCurrentBuilderTestResults()
+{
+    var startTime = Date.now();
+    g_currentBuilderTestResults = decompressResults(g_resultsByBuilder[g_currentState.builder]);
+    console.log( 'Time to get test results by build: ' + (Date.now() - startTime));
+}
+
+function handleValidHashParameter(key, value)
+{
+    switch(key) {
+    case 'buildTimestamp':
+        g_currentState.buildTimestamp = parseInt(value, 10);
+        return true;
+    case 'ignoreFlakyTests':
+        g_currentState.ignoreFlakyTests = value == 'true';
+        return true;
+    default:
+        return false;
+    }
+}
+
+g_defaultDashboardSpecificStateValues = {
+    buildTimestamp: -1,
+    ignoreFlakyTests: true
+};
+
+function shouldShowWebKitRevisionsOnly()
+{
+    return isTipOfTreeWebKitBuilder();
+}
+
+function updateTimelineForBuilder()
+{
+    var builder = g_currentState.builder;
+    var results = g_resultsByBuilder[builder];
+    var graphData = [];
+
+    var annotations = [];
+
+    // Dygraph prefers to be handed data in chronological order.
+    for (var i = results[FIXABLE_COUNTS_KEY].length - 1; i >= 0; i--) {
+        var buildDate = new Date(results[TIMESTAMPS_KEY][i] * 1000);
+        // FIXME: Find a better way to exclude outliers. This is just so we
+        // exclude runs where every test failed.
+        var failureCount = Math.min(results[FIXABLE_COUNT_KEY][i], 10000);
+
+        if (g_currentState.ignoreFlakyTests)
+            failureCount -= g_currentBuilderTestResults.flakyDeltasByBuild[i].total || 0;
+
+        graphData.push([buildDate, failureCount]);
+
+        if (!shouldShowWebKitRevisionsOnly() && (results[WEBKIT_REVISIONS_KEY][i] != results[WEBKIT_REVISIONS_KEY][i + 1])) {
+            annotations.push({
+                series: FAILING_TESTS_DATASET_NAME,
+                x: buildDate,
+                shortText: 'R',
+                text: 'WebKit roll: r' + results[WEBKIT_REVISIONS_KEY][i + 1] + ' to ' + results[WEBKIT_REVISIONS_KEY][i]
+            });
+        }
+    }
+
+    var windowWidth = document.documentElement.clientWidth;
+    var windowHeight = document.documentElement.clientHeight;
+    var switcherNode = $('test-type-switcher');
+    var inspectorNode = $('inspector-container');
+    var graphWidth = windowWidth - 20 - inspectorNode.offsetWidth;
+    var graphHeight = windowHeight - switcherNode.offsetTop - switcherNode.offsetHeight - 20;
+
+    var containerNode = $('timeline-container');
+    containerNode.style.height = graphHeight + 'px';
+    containerNode.style.width = graphWidth + 'px';
+    inspectorNode.style.height = graphHeight + 'px';
+
+    g_dygraph = new Dygraph(
+        containerNode,
+        graphData, {
+            labels: ['Date', FAILING_TESTS_DATASET_NAME],
+            width: graphWidth,
+            height: graphHeight,
+            clickCallback: function(event, date) {
+                selectBuild(results, builder, g_dygraph, g_buildIndicesByTimestamp[date]);
+            },
+            drawCallback: function(dygraph, isInitial) {
+                if (isInitial)
+                    return;
+                updateBuildIndicator(results, dygraph);
+            },
+            // xValueParser is necessary for annotations to work, even though we
+            // already have Date instances
+            xValueParser: function(input) { return input.getTime(); }
+        });
+    if (annotations.length)
+        g_dygraph.setAnnotations(annotations);
+
+    inspectorNode.style.visibility = 'visible';
+
+    if (g_currentBuildIndex != -1)
+        selectBuild(results, builder, g_dygraph, g_currentBuildIndex);
+}
+
+function selectBuild(results, builder, dygraph, index)
+{
+    g_currentBuildIndex = index;
+    updateBuildIndicator(results, dygraph);
+    updateBuildInspector(results, builder, dygraph, index);
+    setQueryParameter('buildTimestamp', results[TIMESTAMPS_KEY][index] * 1000);
+}
+
+function updateBuildIndicator(results, dygraph)
+{
+    var indicatorNode = $('indicator');
+
+    if (!indicatorNode) {
+        var containerNode = $('timeline-container');
+        indicatorNode = document.createElement('div');
+        indicatorNode.id = 'indicator';
+        indicatorNode.style.height = containerNode.offsetHeight + 'px';
+        containerNode.appendChild(indicatorNode);
+    }
+
+    if (g_currentBuildIndex == -1)
+        indicatorNode.style.display = 'none';
+    else {
+        indicatorNode.style.display = 'block';
+        var buildDate = new Date(results[TIMESTAMPS_KEY][g_currentBuildIndex] * 1000);
+        var domCoords = dygraph.toDomCoords(buildDate, 0);
+        indicatorNode.style.left = domCoords[0] + 'px';
+    }
+}
+
+function updateBuildInspector(results, builder, dygraph, index)
+{
+    var html = '<table id="inspector-table"><caption>Details</caption>';
+
+    function addRow(label, value)
+    {
+        html += '<tr><td class="label">' + label + '</td><td>' + value + '</td></tr>';
+    }
+
+    // Builder and results links
+    var buildNumber = results[BUILD_NUMBERS_KEY][index];
+    addRow('', '');
+    var master = builderMaster(builder);
+    var buildUrl = master.logPath(builder, results[BUILD_NUMBERS_KEY][index]);
+    if (master == WEBKIT_BUILDER_MASTER) {
+        var resultsUrl = 'http://build.webkit.org/results/' + builder + '/r' + results[WEBKIT_REVISIONS_KEY][index] +
+            ' (' + results[BUILD_NUMBERS_KEY][index] + ')';
+    } else {
+        var resultsUrl = 'http://build.chromium.org/f/chromium/layout_test_results/' +
+            g_builders[builder] + '/' + results[CHROME_REVISIONS_KEY][index];
+    }
+
+    addRow('Build:', '<a href="' + buildUrl + '" target="_blank">' + buildNumber + '</a> (<a href="' + resultsUrl + '" target="_blank">results</a>)');
+
+    // Revision link(s)
+    if (!shouldShowWebKitRevisionsOnly())
+        addRow('Chromium change:', chromiumRevisionLink(results, index));
+    addRow('WebKit change:', webKitRevisionLink(results, index));
+
+    // Test status/counts
+    addRow('', '');
+
+    function addNumberRow(label, currentValue, previousValue)
+    {
+        var delta = currentValue - previousValue;
+        var deltaText = ''
+        if (delta < 0)
+            deltaText = ' <span class="delta negative">' + delta + '</span>';
+        else if (delta > 0)
+            deltaText = ' <span class="delta positive">+' + delta + '</span>';
+
+        addRow(label, currentValue + deltaText);
+    }
+
+    var expectations = expectationsMap();
+    var flakyDeltasByBuild = g_currentBuilderTestResults.flakyDeltasByBuild;
+    for (var expectationKey in expectations) {
+        if (expectationKey in results[FIXABLE_COUNTS_KEY][index]) {
+            var currentCount = results[FIXABLE_COUNTS_KEY][index][expectationKey];
+            var previousCount = results[FIXABLE_COUNTS_KEY][index + 1][expectationKey];
+            if (g_currentState.ignoreFlakyTests) {
+                currentCount -= flakyDeltasByBuild[index][expectationKey] || 0;
+                previousCount -= flakyDeltasByBuild[index + 1][expectationKey] || 0;
+            }
+            addNumberRow(expectations[expectationKey], currentCount, previousCount);
+        }
+    }
+
+    var currentTotal = results[FIXABLE_COUNT_KEY][index];
+    var previousTotal = results[FIXABLE_COUNT_KEY][index + 1];
+    if (g_currentState.ignoreFlakyTests) {
+        currentTotal -= flakyDeltasByBuild[index].total || 0;
+        previousTotal -= flakyDeltasByBuild[index + 1].total || 0;
+    }
+    addNumberRow('Total failing tests:', currentTotal, previousTotal);
+
+    html += '</table>';
+
+    html += '<div id="changes-button" class="buttons">';
+    html += '<button>Show changed test results</button>';
+    html += '</div>';
+
+    html += '<div id="build-buttons" class="buttons">';
+    html += '<button>Previous build</button> <button>Next build</button>';
+    html += '</div>';
+
+    var inspectorNode = $('inspector-container');
+    inspectorNode.innerHTML = html;
+
+    inspectorNode.getElementsByTagName('button')[0].onclick = function() {
+        showResultsDelta(index, buildNumber, buildUrl, resultsUrl);
+    };
+    inspectorNode.getElementsByTagName('button')[1].onclick = function() {
+        selectBuild(results, builder, dygraph, index + 1);
+    };
+    inspectorNode.getElementsByTagName('button')[2].onclick = function() {
+        selectBuild(results, builder, dygraph, index - 1);
+    };
+}
+
+function showResultsDelta(index, buildNumber, buildUrl, resultsUrl)
+{
+    var flakyTests = g_currentBuilderTestResults.flakyTests;
+    var currentResults = g_currentBuilderTestResults.resultsByBuild[index];
+    var testNames = g_currentBuilderTestResults.testNames;
+    var previousResults = g_currentBuilderTestResults.resultsByBuild[index + 1];
+    var expectations = expectationsMap();
+
+    var deltas = {};
+    function addDelta(category, testIndex)
+    {
+        if (g_currentState.ignoreFlakyTests && flakyTests[testIndex])
+            return;
+        if (!(category in deltas))
+            deltas[category] = [];
+        var testName = testNames[testIndex];
+        var flakinessDashboardUrl = 'flakiness_dashboard.html' + (location.hash ? location.hash + '&' : '#') + 'tests=' + testName;
+        var html = '<a href="' + flakinessDashboardUrl + '">' + testName + '</a>';
+        if (flakyTests[testIndex])
+            html += ' <span style="color: #f66">possibly flaky</span>';
+        deltas[category].push(html);
+    }
+
+    for (var testIndex = 0; testIndex < currentResults.length; testIndex++) {
+        if (currentResults[testIndex] === undefined)
+            continue;
+
+        if (previousResults[testIndex] !== undefined) {
+            if (currentResults[testIndex] == previousResults[testIndex])
+                continue;
+            addDelta('Was <b>' + expectations[previousResults[testIndex]] + '</b> now <b>' + expectations[currentResults[testIndex]] + '</b>', testIndex);
+        } else
+            addDelta('Newly <b>' + expectations[currentResults[testIndex]] + '</b>', testIndex);
+    }
+
+    for (var testIndex = 0; testIndex < previousResults.length; testIndex++) {
+        if (previousResults[testIndex] === undefined)
+            continue;
+        if (currentResults[testIndex] === undefined)
+            addDelta('Was <b>' + expectations[previousResults[testIndex]] + '</b>', testIndex);
+    }
+
+    var html = '';
+
+    html += '<head><base target="_blank"></head>';
+    html += '<h1>Changes in test results</h1>';
+
+    html += '<p>For build <a href="' + buildUrl + '" target="_blank">' +
+        buildNumber + '</a> ' + '(<a href="' + resultsUrl +
+        '" target="_blank">results</a>)</p>';
+
+    for (var deltaCategory in deltas) {
+        html += '<p><div>' + deltaCategory + ' (' + deltas[deltaCategory].length + ')</div><ul>';
+        deltas[deltaCategory].forEach(function(deltaHtml) {
+            html += '<li>' + deltaHtml + '</li>';
+        });
+        html += '</ul></p>';
+    }
+
+    var deltaWindow = window.open();
+    deltaWindow.document.write(html);
+}
+
+document.addEventListener('keydown', function(e) {
+    if (g_currentBuildIndex == -1)
+        return;
+
+    switch (e.keyIdentifier) {
+    case 'Left':
+        selectBuild(
+            g_resultsByBuilder[g_currentState.builder],
+            g_currentState.builder,
+            g_dygraph,
+            g_currentBuildIndex + 1);
+        break;
+    case 'Right':
+        selectBuild(
+            g_resultsByBuilder[g_currentState.builder],
+            g_currentState.builder,
+            g_dygraph,
+            g_currentBuildIndex - 1);
+        break;
+    }
+});
+</script>
+</head>
+<body>
+    <div id="test-type-switcher"></div>
+
+    <div id="inspector-container" style="visibility: hidden">
+        <p>Click on a point on the graph to see details about that build.</p>
+        <p>Click and drag on the graph to zoom in to that period.</p>
+    </div>
+    <div id="timeline-container">Loading...</div>
+</body>
+</html>
diff --git a/Tools/TestResultServer/static-dashboards/treemap.html b/Tools/TestResultServer/static-dashboards/treemap.html
new file mode 100644
index 0000000..aa7ae43
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/treemap.html
@@ -0,0 +1,364 @@
+<!-- Copyright (C) 2011 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.
+-->
+<!DOCTYPE html>
+<title>Test Runtimes</title>
+<link rel='stylesheet' href='webtreemap.css'></link>
+<style>
+body {
+    display: -moz-box;
+    display: -webkit-box;
+    display: box;
+    -moz-box-orient: vertical;
+    -webkit-box-orient: vertical;
+    box-orient: vertical;
+
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+}
+
+td:first-child {
+    text-align: left;
+}
+
+td {
+    text-align: right;
+}
+
+#map {
+    display: -moz-box;
+    display: -webkit-box;
+    display: box;
+
+    -moz-box-flex: 1;
+    -webkit-box-flex: 1;
+    box-flex: 1;
+
+    position: relative;
+    cursor: pointer;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+}
+
+.extra-dom {
+    display: none;
+    border: none;
+    border-top: 1px dashed;
+    padding: 4px;
+    margin: 0;
+    overflow: auto;
+    cursor: auto;
+    -webkit-user-select: text;
+    -moz-user-select: text;
+}
+
+#focused-leaf {
+    display: -webkit-box;
+    display: -moz-box;
+    -webkit-box-orient: vertical;
+    -moz-box-orient: vertical;
+}
+
+#focused-leaf > .extra-dom {
+    display: -webkit-box;
+    display: -moz-box;
+    -webkit-box-flex: 1;
+    -moz-box-flex: 1;
+}
+
+#focused-leaf.webtreemap-node:hover {
+    background: white;
+}
+
+#focused-leaf .webtreemap-caption:hover {
+    background: #eee;
+}
+
+.error {
+    color: red;
+    font-style: italic;
+}
+</style>
+<script src="builders.js"></script>
+<script src="loader.js"></script>
+<script src="dashboard_base.js"></script>
+<script src='webtreemap.js'></script>
+
+<div id='header-container'></div>
+<p>Click on a box to zoom in. Click on the outermost box to zoom out. <a href="" onclick="showAverages();return false;">Show averages</a></p>
+<div id='map'></div>
+
+<script>
+var TEST_URL_BASE_PATH = "http://svn.webkit.org/repository/webkit/trunk/";
+
+function humanReadableTime(milliseconds)
+{
+    if (milliseconds < 1000)
+        return Math.floor(milliseconds) + 'ms';
+    else if (milliseconds < 60000)
+        return (milliseconds / 1000).toPrecision(2) + 's';
+
+    var minutes = Math.floor(milliseconds / 60000);
+    var seconds = Math.floor((milliseconds - minutes * 60000) / 1000);
+    return minutes + 'm' + seconds + 's';
+}
+
+// This looks like:
+// { "data": {"$area": (sum of all timings)},
+//   "name": (name of this node),
+//   "children": [ (child nodes, in the same format as this) ] }
+// childCount is added just to be includes in the node's name
+function convertToWebTreemapFormat(treename, tree, path)
+{
+    var total = 0;
+    var childCount = 0;
+    var children = [];
+    for (var name in tree) {
+        var treeNode = tree[name];
+        if (typeof treeNode == "number") {
+            var time = treeNode;
+            var node = {
+                "data": {"$area": time},
+                "name": name + " (" + humanReadableTime(time) + ")"
+            };
+            children.push(node);
+            total += time;
+            childCount++;
+        } else {
+            var newPath = path ? path + '/' + name : name;
+            var subtree = convertToWebTreemapFormat(name, treeNode, newPath);
+            children.push(subtree);
+            total += subtree["data"]["$area"];
+            childCount += subtree["childCount"];
+        }
+    }
+
+    children.sort(function(a, b) {
+        aTime = a.data["$area"]
+        bTime = b.data["$area"]
+        return bTime - aTime;
+    });
+
+    return {
+        "data": {"$area": total},
+        "name": treename + " (" + humanReadableTime(total) + " - " + childCount + " tests)",
+        "children": children,
+        "childCount": childCount,
+        "path": path
+    };
+}
+
+function listOfAllNonLeafNodes(tree, list)
+{
+    if (!tree.children)
+        return;
+
+    if (!list)
+        list = [];
+    list.push(tree);
+
+    tree.children.forEach(function(child) {
+        listOfAllNonLeafNodes(child, list);
+    });
+    return list;
+}
+
+function reverseSortByAverage(list)
+{
+    list.sort(function(a, b) {
+        var avgA = a.data['$area'] / a.childCount;
+        var avgB = b.data['$area'] / b.childCount;
+        return avgB - avgA;
+    });
+}
+
+function showAverages()
+{
+    if (!document.getElementById('map'))
+        return;
+
+    var table = document.createElement('table');
+    table.innerHTML = '<th>directory</th><th># tests</th><th>avg time / test</th>';
+
+    var allNodes = listOfAllNonLeafNodes(g_webTree);
+    reverseSortByAverage(allNodes);
+    allNodes.forEach(function(node) {
+        var average = node.data['$area'] / node.childCount;
+        if (average > 100 && node.childCount != 1) {
+            var tr = document.createElement('tr');
+            tr.innerHTML = '<td></td><td>' + node.childCount + '</td><td>' + humanReadableTime(average) + '</td>';
+            tr.querySelector('td').innerText = node.path;
+            table.appendChild(tr);
+        }
+    });
+
+    var map = document.getElementById('map');
+    map.parentNode.replaceChild(table, map);
+}
+
+var g_isGeneratingPage = false;
+var g_webTree;
+
+function generatePage()
+{
+    $('header-container').innerHTML = htmlForTestTypeSwitcher();
+
+    g_isGeneratingPage = true;
+
+    var rawTree = g_resultsByBuilder[g_currentState.builder];
+    g_webTree = convertToWebTreemapFormat('LayoutTests', rawTree);
+    appendTreemap($('map'), g_webTree);
+
+    if (g_currentState.treemapfocus)
+        focusPath(g_webTree, g_currentState.treemapfocus)
+
+    g_isGeneratingPage = false;
+}
+
+function focusPath(tree, path)
+{
+    var parts = decodeURIComponent(path).split('/');
+    if (extractName(tree) != parts[0]) {
+        console.error('Could not focus tree rooted at ' + parts[0]);
+        return;
+    }
+
+    for (var i = 1; i < parts.length; i++) {
+        var children = tree.children;
+        for (var j = 0; j < children.length; j++) {
+            var child = children[j];
+            if (extractName(child) == parts[i]) {
+                tree = child;
+                focus(tree);
+                break;
+            }
+        }
+        if (j == children.length) {
+            console.error('Could not find tree at ' + parts[i]);
+            break;
+        }
+    }
+
+}
+
+function handleValidHashParameter(key, value)
+{
+    switch(key) {
+    case 'builder':
+        validateParameter(g_currentState, key, value,
+            function() { return value in g_builders; });
+        return true;
+
+    case 'treemapfocus':
+        validateParameter(g_currentState, key, value,
+            function() {
+                // FIXME: There's probably a simpler regexp here. Just trying to match ascii + forward-slash.
+                // e.g. LayoutTests/foo/bar.html
+                return (value.match(/^(\w+\/\w*)*$/));
+            });
+        return true;
+
+    default:
+        return false;
+    }
+}
+
+g_defaultDashboardSpecificStateValues = {
+    treemapfocus: '',
+}
+
+function handleQueryParameterChange(params)
+{
+    for (var param in params) {
+        if (param != 'treemapfocus') {
+            $('map').innerHTML = 'Loading...';
+            return true;
+        }
+    }
+    return false;
+}
+
+// Overrides handleResourceLoadError in dashboard_base.js.
+function handleResourceLoadError(builderName, e)
+{
+    $('map').innerHTML = '<span class=error>Could not load data for ' + builderName + '. ' +
+        'Either there was a server-side error or ' + builderName + ' does not run ' +
+        g_crossDashboardState.testType + '.</span>';
+}
+
+function extractName(node)
+{
+    return node.name.split(' ')[0];
+}
+
+function fullName(node)
+{
+    var buffer = [extractName(node)];
+    while (node.parent) {
+        node = node.parent;
+        buffer.unshift(extractName(node));
+    }
+    return buffer.join('/');
+}
+
+function handleFocus(tree)
+{
+    var currentlyFocusedNode = $('focused-leaf');
+    if (currentlyFocusedNode)
+        currentlyFocusedNode.id = '';
+
+    if (!tree.children)
+        tree.dom.id = 'focused-leaf';
+
+    var name = fullName(tree);
+
+    if (!tree.children && !tree.extraDom && isLayoutTestResults()) {
+        tree.extraDom = document.createElement('pre');
+        tree.extraDom.className = 'extra-dom';
+        tree.dom.appendChild(tree.extraDom);
+
+        loader.request(TEST_URL_BASE_PATH + name,
+            function(xhr) {
+                tree.extraDom.onmousedown = function(e) {
+                    e.stopPropagation();
+                };
+                tree.extraDom.textContent = xhr.responseText;
+            },
+            function (xhr) {
+                tree.extraDom.textContent = "Could not load test."
+        });
+    }
+
+    // We don't want the focus calls during generatePage to try to modify the query state.
+    if (!g_isGeneratingPage)
+        setQueryParameter('treemapfocus', name);
+}
+</script>
diff --git a/Tools/TestResultServer/static-dashboards/webtreemap.css b/Tools/TestResultServer/static-dashboards/webtreemap.css
new file mode 100644
index 0000000..a078650
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/webtreemap.css
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+.webtreemap-node {
+  /* Required attributes. */
+  position: absolute;
+  overflow: hidden;   /* To hide overlong captions. */
+  background: white;  /* Nodes must be opaque for zIndex layering. */
+  border: solid 1px black;  /* Calculations assume 1px border. */
+
+  /* Optional: CSS animation. */
+  -webkit-transition: top    0.3s,
+                      left   0.3s,
+                      width  0.3s,
+                      height 0.3s;
+}
+
+/* Optional: highlight nodes on mouseover. */
+.webtreemap-node:hover {
+  background: #eee;
+}
+
+/* Optional: Different borders depending on level. */
+.webtreemap-level0 {
+  border: solid 1px #444;
+}
+.webtreemap-level1 {
+  border: solid 1px #666;
+}
+.webtreemap-level2 {
+  border: solid 1px #888;
+}
+.webtreemap-level3 {
+  border: solid 1px #aaa;
+}
+.webtreemap-level4 {
+  border: solid 1px #ccc;
+}
+
+/* Optional: styling on node captions. */
+.webtreemap-caption {
+  font-family: sans-serif;
+  font-size: 11px;
+  padding: 2px;
+  text-align: center;
+}
+
+/* Optional: styling on captions on mouse hover. */
+/*.webtreemap-node:hover > .webtreemap-caption {
+  text-decoration: underline;
+}*/
diff --git a/Tools/TestResultServer/static-dashboards/webtreemap.js b/Tools/TestResultServer/static-dashboards/webtreemap.js
new file mode 100644
index 0000000..35c03c6
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/webtreemap.js
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+// Size of border around nodes.
+// We could support arbitrary borders using getComputedStyle(), but I am
+// skeptical the extra complexity (and performance hit) is worth it.
+var kBorderWidth = 1;
+
+// Padding around contents.
+// TODO: do this with a nested div to allow it to be CSS-styleable.
+var kPadding = 4;
+
+var focused = null;
+
+// Callback for embedding page to update after a focus.
+function handleFocus(tree) {}
+
+function focus(tree) {
+  focused = tree;
+
+  // Hide all visible siblings of all our ancestors by lowering them.
+  var level = 0;
+  var root = tree;
+  while (root.parent) {
+    root = root.parent;
+    level += 1;
+    for (var i = 0, sibling; sibling = root.children[i]; ++i) {
+      if (sibling.dom)
+        sibling.dom.style.zIndex = 0;
+    }
+  }
+  var width = root.dom.offsetWidth;
+  var height = root.dom.offsetHeight;
+  // Unhide (raise) and maximize us and our ancestors.
+  for (var t = tree; t.parent; t = t.parent) {
+    // Shift off by border so we don't get nested borders.
+    // TODO: actually make nested borders work (need to adjust width/height).
+    position(t.dom, -kBorderWidth, -kBorderWidth, width, height);
+    t.dom.style.zIndex = 1;
+  }
+  // And layout into the topmost box.
+  layout(tree, level, width, height);
+  handleFocus(tree);
+}
+
+function makeDom(tree, level) {
+  var dom = document.createElement('div');
+  dom.style.zIndex = 1;
+  dom.className = 'webtreemap-node webtreemap-level' + Math.min(level, 4);
+
+  dom.onmousedown = function(e) {
+    if (e.button == 0) {
+      if (focused && tree == focused && focused.parent) {
+        focus(focused.parent);
+      } else {
+        focus(tree);
+      }
+    }
+    e.stopPropagation();
+    return true;
+  };
+
+  var caption = document.createElement('div');
+  caption.className = 'webtreemap-caption';
+  caption.innerHTML = tree.name;
+  dom.appendChild(caption);
+
+  tree.dom = dom;
+  return dom;
+}
+
+function position(dom, x, y, width, height) {
+  // CSS width/height does not include border.
+  width -= kBorderWidth*2;
+  height -= kBorderWidth*2;
+
+  dom.style.left   = x + 'px';
+  dom.style.top    = y + 'px';
+  dom.style.width  = Math.max(width, 0) + 'px';
+  dom.style.height = Math.max(height, 0) + 'px';
+}
+
+// Given a list of rectangles |nodes|, the 1-d space available
+// |space|, and a starting rectangle index |start|, compute an span of
+// rectangles that optimizes a pleasant aspect ratio.
+//
+// Returns [end, sum], where end is one past the last rectangle and sum is the
+// 2-d sum of the rectangles' areas.
+function selectSpan(nodes, space, start) {
+  // Add rectangle one by one, stopping when aspect ratios begin to go
+  // bad.  Result is [start,end) covering the best run for this span.
+  // http://scholar.google.com/scholar?cluster=5972512107845615474
+  var node = nodes[start];
+  var rmin = node.data['$area'];  // Smallest seen child so far.
+  var rmax = rmin;                // Largest child.
+  var rsum = 0;                   // Sum of children in this span.
+  var last_score = 0;             // Best score yet found.
+  for (var end = start; node = nodes[end]; ++end) {
+    var size = node.data['$area'];
+    if (size < rmin)
+      rmin = size;
+    if (size > rmax)
+      rmax = size;
+    rsum += size;
+
+    // This formula is from the paper, but you can easily prove to
+    // yourself it's taking the larger of the x/y aspect ratio or the
+    // y/x aspect ratio.  The additional magic fudge constant of 5
+    // makes us prefer wider rectangles to taller ones.
+    var score = Math.max(5*space*space*rmax / (rsum*rsum),
+                         1*rsum*rsum / (space*space*rmin));
+    if (last_score && score > last_score) {
+      rsum -= size;  // Undo size addition from just above.
+      break;
+    }
+    last_score = score;
+  }
+  return [end, rsum];
+}
+
+function layout(tree, level, width, height) {
+  if (!('children' in tree))
+    return;
+
+  var total = tree.data['$area'];
+
+  // XXX why do I need an extra -1/-2 here for width/height to look right?
+  var x1 = 0, y1 = 0, x2 = width - 1, y2 = height - 2;
+  x1 += kPadding; y1 += kPadding;
+  x2 -= kPadding; y2 -= kPadding;
+  y1 += 14;  // XXX get first child height for caption spacing
+
+  var pixels_to_units = Math.sqrt(total / ((x2 - x1) * (y2 - y1)));
+
+  for (var start = 0, child; child = tree.children[start]; ++start) {
+    if (x2 - x1 < 60 || y2 - y1 < 40) {
+      if (child.dom) {
+        child.dom.style.zIndex = 0;
+        position(child.dom, -2, -2, 0, 0);
+      }
+      continue;
+    }
+
+    // In theory we can dynamically decide whether to split in x or y based
+    // on aspect ratio.  In practice, changing split direction with this
+    // layout doesn't look very good.
+    //   var ysplit = (y2 - y1) > (x2 - x1);
+    var ysplit = true;
+
+    var space;  // Space available along layout axis.
+    if (ysplit)
+      space = (y2 - y1) * pixels_to_units;
+    else
+      space = (x2 - x1) * pixels_to_units;
+
+    var span = selectSpan(tree.children, space, start);
+    var end = span[0], rsum = span[1];
+
+    // Now that we've selected a span, lay out rectangles [start,end) in our
+    // available space.
+    var x = x1, y = y1;
+    for (var i = start; i < end; ++i) {
+      child = tree.children[i];
+      if (!child.dom) {
+        child.parent = tree;
+        child.dom = makeDom(child, level + 1);
+        tree.dom.appendChild(child.dom);
+      } else {
+        child.dom.style.zIndex = 1;
+      }
+      var size = child.data['$area'];
+      var frac = size / rsum;
+      if (ysplit) {
+        width = rsum / space;
+        height = size / width;
+      } else {
+        height = rsum / space;
+        width = size / height;
+      }
+      width /= pixels_to_units;
+      height /= pixels_to_units;
+      width = Math.round(width);
+      height = Math.round(height);
+      position(child.dom, x, y, width, height);
+      if ('children' in child) {
+        layout(child, level + 1, width, height);
+      }
+      if (ysplit)
+        y += height;
+      else
+        x += width;
+    }
+
+    // Shrink our available space based on the amount we used.
+    if (ysplit)
+      x1 += Math.round((rsum / space) / pixels_to_units);
+    else
+      y1 += Math.round((rsum / space) / pixels_to_units);
+
+    // end points one past where we ended, which is where we want to
+    // begin the next iteration, but subtract one to balance the ++ in
+    // the loop.
+    start = end - 1;
+  }
+}
+
+function appendTreemap(dom, data) {
+  var style = getComputedStyle(dom, null);
+  var width = parseInt(style.width);
+  var height = parseInt(style.height);
+  if (!data.dom)
+    makeDom(data, 0);
+  dom.appendChild(data.dom);
+  position(data.dom, 0, 0, width, height);
+  layout(data, 0, width, height);
+}
\ No newline at end of file