Home | History | Annotate | Download | only in static-dashboards
      1 // Copyright (C) 2012 Google Inc. All rights reserved.
      2 //
      3 // Redistribution and use in source and binary forms, with or without
      4 // modification, are permitted provided that the following conditions are
      5 // met:
      6 //
      7 //     * Redistributions of source code must retain the above copyright
      8 // notice, this list of conditions and the following disclaimer.
      9 //     * Redistributions in binary form must reproduce the above
     10 // copyright notice, this list of conditions and the following disclaimer
     11 // in the documentation and/or other materials provided with the
     12 // distribution.
     13 //     * Neither the name of Google Inc. nor the names of its
     14 // contributors may be used to endorse or promote products derived from
     15 // this software without specific prior written permission.
     16 //
     17 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     18 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     19 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     20 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     21 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     22 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     23 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     24 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     25 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     26 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     27 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     28 
     29 //////////////////////////////////////////////////////////////////////////////
     30 // CONSTANTS
     31 //////////////////////////////////////////////////////////////////////////////
     32 var FORWARD = 'forward';
     33 var BACKWARD = 'backward';
     34 var TEST_URL_BASE_PATH_FOR_BROWSING = 'http://src.chromium.org/viewvc/blink/trunk/LayoutTests/';
     35 var TEST_URL_BASE_PATH_FOR_XHR = 'http://src.chromium.org/blink/trunk/LayoutTests/';
     36 var TEST_RESULTS_BASE_PATH = 'https://storage.googleapis.com/chromium-layout-test-archives/';
     37 var GPU_RESULTS_BASE_PATH = 'http://chromium-browser-gpu-tests.commondatastorage.googleapis.com/runs/'
     38 
     39 var RELEASE_TIMEOUT = 6;
     40 var DEBUG_TIMEOUT = 12;
     41 var SLOW_MULTIPLIER = 5;
     42 
     43 // FIXME: Figure out how to make this not be hard-coded.
     44 // Probably just include in the results.json files and get it from there.
     45 var VIRTUAL_SUITES = {
     46     'virtual/gpu/fast/canvas': 'fast/canvas',
     47     'virtual/gpu/canvas/philip': 'canvas/philip',
     48     'virtual/threaded/compositing/visibility': 'compositing/visibility',
     49     'virtual/threaded/compositing/webgl': 'compositing/webgl',
     50     'virtual/gpu/fast/hidpi': 'fast/hidpi',
     51     'virtual/softwarecompositing': 'compositing',
     52     'virtual/deferred/fast/images': 'fast/images',
     53     'virtual/gpu/compositedscrolling/overflow': 'compositing/overflow',
     54     'virtual/gpu/compositedscrolling/scrollbars': 'scrollbars',
     55 };
     56 
     57 var ACTUAL_RESULT_SUFFIXES = ['expected.txt', 'expected.png', 'actual.txt', 'actual.png', 'diff.txt', 'diff.png', 'wdiff.html', 'crash-log.txt'];
     58 
     59 var EXPECTATIONS_ORDER = ACTUAL_RESULT_SUFFIXES.filter(function(suffix) {
     60     return !string.endsWith(suffix, 'png');
     61 }).map(function(suffix) {
     62     return suffix.split('.')[0]
     63 });
     64 
     65 var resourceLoader;
     66 
     67 function generatePage(historyInstance)
     68 {
     69     if (historyInstance.crossDashboardState.useTestData)
     70         return;
     71 
     72     document.body.innerHTML = '<div id="loading-ui">LOADING...</div>';
     73     resourceLoader.showErrors();
     74 
     75     // tests expands to all tests that match the CSV list.
     76     // result expands to all tests that ever have the given result
     77     if (historyInstance.dashboardSpecificState.tests || historyInstance.dashboardSpecificState.result)
     78         generatePageForIndividualTests(individualTests());
     79     else
     80         generatePageForBuilder(historyInstance.dashboardSpecificState.builder || currentBuilderGroup().defaultBuilder());
     81 
     82     for (var builder in currentBuilders())
     83         processTestResultsForBuilderAsync(builder);
     84 
     85     postHeightChangedMessage();
     86 }
     87 
     88 function handleValidHashParameter(historyInstance, key, value)
     89 {
     90     switch(key) {
     91     case 'result':
     92     case 'tests':
     93         history.validateParameter(historyInstance.dashboardSpecificState, key, value,
     94             function() {
     95                 return string.isValidName(value);
     96             });
     97         return true;
     98 
     99     case 'builder':
    100         history.validateParameter(historyInstance.dashboardSpecificState, key, value,
    101             function() {
    102                 return value in currentBuilders();
    103             });
    104 
    105         return true;
    106 
    107     case 'sortColumn':
    108         history.validateParameter(historyInstance.dashboardSpecificState, key, value,
    109             function() {
    110                 // Get all possible headers since the actual used set of headers
    111                 // depends on the values in historyInstance.dashboardSpecificState, which are currently being set.
    112                 var getAllTableHeaders = true;
    113                 var headers = tableHeaders(getAllTableHeaders);
    114                 for (var i = 0; i < headers.length; i++) {
    115                     if (value == sortColumnFromTableHeader(headers[i]))
    116                         return true;
    117                 }
    118                 return value == 'test' || value == 'builder';
    119             });
    120         return true;
    121 
    122     case 'sortOrder':
    123         history.validateParameter(historyInstance.dashboardSpecificState, key, value,
    124             function() {
    125                 return value == FORWARD || value == BACKWARD;
    126             });
    127         return true;
    128 
    129     case 'resultsHeight':
    130     case 'revision':
    131         history.validateParameter(historyInstance.dashboardSpecificState, key, Number(value),
    132             function() {
    133                 return value.match(/^\d+$/);
    134             });
    135         return true;
    136 
    137     case 'showChrome':
    138     case 'showExpectations':
    139     case 'showFlaky':
    140     case 'showLargeExpectations':
    141     case 'showNonFlaky':
    142     case 'showSlow':
    143     case 'showSkip':
    144     case 'showUnexpectedPasses':
    145     case 'showWontFix':
    146         historyInstance.dashboardSpecificState[key] = value == 'true';
    147         return true;
    148 
    149     default:
    150         return false;
    151     }
    152 }
    153 
    154 // @param {Object} params New or modified query parameters as key: value.
    155 function handleQueryParameterChange(historyInstance, params)
    156 {
    157     for (key in params) {
    158         if (key == 'tests') {
    159             // Entering cross-builder view, only keep valid keys for that view.
    160             for (var currentKey in historyInstance.dashboardSpecificState) {
    161               if (isInvalidKeyForCrossBuilderView(currentKey)) {
    162                 delete historyInstance.dashboardSpecificState[currentKey];
    163               }
    164             }
    165         } else if (isInvalidKeyForCrossBuilderView(key)) {
    166             delete historyInstance.dashboardSpecificState.tests;
    167             delete historyInstance.dashboardSpecificState.result;
    168         }
    169     }
    170 
    171     return true;
    172 }
    173 
    174 var defaultDashboardSpecificStateValues = {
    175     sortOrder: BACKWARD,
    176     sortColumn: 'flakiness',
    177     showExpectations: false,
    178     // FIXME: Show flaky tests by default if you have a builder picked.
    179     // Ideally, we'd fix the dashboard to not pick a default builder and have
    180     // you pick one. In the interim, this is a good way to make the default
    181     // page load faster since we don't need to generate/layout a large table.
    182     showFlaky: false,
    183     showLargeExpectations: false,
    184     showChrome: true,
    185     showWontFix: false,
    186     showNonFlaky: false,
    187     showSkip: false,
    188     showUnexpectedPasses: false,
    189     resultsHeight: 300,
    190     revision: null,
    191     tests: '',
    192     result: '',
    193     builder: null
    194 };
    195 
    196 var DB_SPECIFIC_INVALIDATING_PARAMETERS = {
    197     'tests' : 'builder',
    198     'testType': 'builder',
    199     'group': 'builder'
    200 };
    201 
    202 var flakinessConfig = {
    203     defaultStateValues: defaultDashboardSpecificStateValues,
    204     generatePage: generatePage,
    205     handleValidHashParameter: handleValidHashParameter,
    206     handleQueryParameterChange: handleQueryParameterChange,
    207     invalidatingHashParameters: DB_SPECIFIC_INVALIDATING_PARAMETERS
    208 };
    209 
    210 // FIXME(jparent): Eventually remove all usage of global history object.
    211 var g_history = new history.History(flakinessConfig);
    212 g_history.parseCrossDashboardParameters();
    213 
    214 //////////////////////////////////////////////////////////////////////////////
    215 // GLOBALS
    216 //////////////////////////////////////////////////////////////////////////////
    217 
    218 var g_perBuilderFailures = {};
    219 // Maps test path to an array of {builder, testResults} objects.
    220 var g_testToResultsMap = {};
    221 
    222 function createResultsObjectForTest(test, builder)
    223 {
    224     return {
    225         test: test,
    226         builder: builder,
    227         // HTML for display of the results in the flakiness column
    228         html: '',
    229         flipCount: 0,
    230         slowestTime: 0,
    231         isFlaky: false,
    232         bugs: [],
    233         expectations : '',
    234         rawResults: '',
    235         // List of all the results the test actually has.
    236         actualResults: []
    237     };
    238 }
    239 
    240 var TestTrie = function(builders, resultsByBuilder)
    241 {
    242     this._trie = {};
    243 
    244     for (var builder in builders) {
    245         if (!resultsByBuilder[builder]) {
    246             console.warn("No results for builder: ", builder)
    247             continue;
    248         }
    249         var testsForBuilder = resultsByBuilder[builder].tests;
    250         for (var test in testsForBuilder)
    251             this._addTest(test.split('/'), this._trie);
    252     }
    253 }
    254 
    255 TestTrie.prototype.forEach = function(callback, startingTriePath)
    256 {
    257     var testsTrie = this._trie;
    258     if (startingTriePath) {
    259         var splitPath = startingTriePath.split('/');
    260         while (splitPath.length && testsTrie)
    261             testsTrie = testsTrie[splitPath.shift()];
    262     }
    263 
    264     if (!testsTrie)
    265         return;
    266 
    267     function traverse(trie, triePath) {
    268         if (trie == true)
    269             callback(triePath);
    270         else {
    271             for (var member in trie)
    272                 traverse(trie[member], triePath ? triePath + '/' + member : member);
    273         }
    274     }
    275     traverse(testsTrie, startingTriePath);
    276 }
    277 
    278 TestTrie.prototype._addTest = function(test, trie)
    279 {
    280     var rootComponent = test.shift();
    281     if (!test.length) {
    282         if (!trie[rootComponent])
    283             trie[rootComponent] = true;
    284         return;
    285     }
    286 
    287     if (!trie[rootComponent] || trie[rootComponent] == true)
    288         trie[rootComponent] = {};
    289     this._addTest(test, trie[rootComponent]);
    290 }
    291 
    292 // Map of all tests to true values. This is just so we can have the list of
    293 // all tests across all the builders.
    294 var g_allTestsTrie;
    295 
    296 function getAllTestsTrie()
    297 {
    298     if (!g_allTestsTrie)
    299         g_allTestsTrie = new TestTrie(currentBuilders(), g_resultsByBuilder);
    300 
    301     return g_allTestsTrie;
    302 }
    303 
    304 // Returns an array of tests to be displayed in the individual tests view.
    305 // Note that a directory can be listed as a test, so we expand that into all
    306 // tests in the directory.
    307 function individualTests()
    308 {
    309     if (g_history.dashboardSpecificState.result)
    310         return allTestsWithResult(g_history.dashboardSpecificState.result);
    311 
    312     if (!g_history.dashboardSpecificState.tests)
    313         return [];
    314 
    315     return individualTestsForSubstringList();
    316 }
    317 
    318 function splitTestList()
    319 {
    320     // Convert windows slashes to unix slashes and spaces/newlines to commas.
    321     var tests = g_history.dashboardSpecificState.tests.replace(/\\/g, '/').replace('\n', ' ').replace(/\s+/g, ',');
    322     return tests.split(',');
    323 }
    324 
    325 function individualTestsForSubstringList()
    326 {
    327     var testList = splitTestList();
    328     // If listing a lot of tests, assume you've passed in an explicit list of tests
    329     // instead of patterns to match against. The matching code below is super slow.
    330     //
    331     // Also, when showChrome is false, we're embedding the dashboard elsewhere and
    332     // an explicit test list is passed in. In that case, we don't want
    333     // a search for compositing/foo.html to also show virtual/softwarecompositing/foo.html.
    334     if (testList.length > 10 || !g_history.dashboardSpecificState.showChrome)
    335         return testList;
    336 
    337     // Put the tests into an object first and then move them into an array
    338     // as a way of deduping.
    339     var testsMap = {};
    340     for (var i = 0; i < testList.length; i++) {
    341         var path = testList[i];
    342 
    343         // Ignore whitespace entries as they'd match every test.
    344         if (path.match(/^\s*$/))
    345             continue;
    346 
    347         var hasAnyMatches = false;
    348         getAllTestsTrie().forEach(function(triePath) {
    349             if (string.caseInsensitiveContains(triePath, path)) {
    350                 testsMap[triePath] = 1;
    351                 hasAnyMatches = true;
    352             }
    353         });
    354 
    355         // If a path doesn't match any tests, then assume it's a full path
    356         // to a test that passes on all builders.
    357         if (!hasAnyMatches)
    358             testsMap[path] = 1;
    359     }
    360 
    361     var testsArray = [];
    362     for (var test in testsMap)
    363         testsArray.push(test);
    364 
    365     return testsArray;
    366 }
    367 
    368 function allTestsWithResult(result)
    369 {
    370     processTestRunsForAllBuilders();
    371     var retVal = [];
    372 
    373     getAllTestsTrie().forEach(function(triePath) {
    374         for (var i = 0; i < g_testToResultsMap[triePath].length; i++) {
    375             if (g_testToResultsMap[triePath][i].actualResults.indexOf(result.toUpperCase()) != -1) {
    376                 retVal.push(triePath);
    377                 break;
    378             }
    379         }
    380     });
    381 
    382     return retVal;
    383 }
    384 
    385 function processTestResultsForBuilderAsync(builder)
    386 {
    387     setTimeout(function() { processTestRunsForBuilder(builder); }, 0);
    388 }
    389 
    390 function processTestRunsForAllBuilders()
    391 {
    392     for (var builder in currentBuilders())
    393         processTestRunsForBuilder(builder);
    394 }
    395 
    396 function processTestRunsForBuilder(builderName)
    397 {
    398     if (g_perBuilderFailures[builderName])
    399       return;
    400 
    401     if (!g_resultsByBuilder[builderName]) {
    402         console.error('No tests found for ' + builderName);
    403         g_perBuilderFailures[builderName] = [];
    404         return;
    405     }
    406 
    407     var failures = [];
    408     var allTestsForThisBuilder = g_resultsByBuilder[builderName].tests;
    409 
    410     for (var test in allTestsForThisBuilder) {
    411         var resultsForTest = createResultsObjectForTest(test, builderName);
    412 
    413         var rawTest = g_resultsByBuilder[builderName].tests[test];
    414         resultsForTest.rawTimes = rawTest.times;
    415         var rawResults = rawTest.results;
    416         resultsForTest.rawResults = rawResults;
    417 
    418         if (rawTest.expected)
    419             resultsForTest.expectations = rawTest.expected;
    420 
    421         if (rawTest.bugs)
    422             resultsForTest.bugs = rawTest.bugs;
    423 
    424         var failureMap = g_resultsByBuilder[builderName][results.FAILURE_MAP];
    425         // FIXME: Switch to resultsByBuild
    426         var times = resultsForTest.rawTimes;
    427         var numTimesSeen = 0;
    428         var numResultsSeen = 0;
    429         var resultsIndex = 0;
    430         var resultsMap = {}
    431 
    432         for (var i = 0; i < times.length; i++) {
    433             numTimesSeen += times[i][results.RLE.LENGTH];
    434 
    435             while (rawResults[resultsIndex] && numTimesSeen > (numResultsSeen + rawResults[resultsIndex][results.RLE.LENGTH])) {
    436                 numResultsSeen += rawResults[resultsIndex][results.RLE.LENGTH];
    437                 resultsIndex++;
    438             }
    439 
    440             if (rawResults && rawResults[resultsIndex]) {
    441                 var result = rawResults[resultsIndex][results.RLE.VALUE];
    442                 resultsMap[failureMap[result]] = true;
    443             }
    444 
    445             resultsForTest.slowestTime = Math.max(resultsForTest.slowestTime, times[i][results.RLE.VALUE]);
    446         }
    447 
    448         resultsForTest.actualResults = Object.keys(resultsMap);
    449 
    450         results.determineFlakiness(failureMap, rawResults, resultsForTest);
    451         failures.push(resultsForTest);
    452 
    453         if (!g_testToResultsMap[test])
    454             g_testToResultsMap[test] = [];
    455         g_testToResultsMap[test].push(resultsForTest);
    456     }
    457 
    458     g_perBuilderFailures[builderName] = failures;
    459 }
    460 
    461 function linkHTMLToOpenWindow(url, text)
    462 {
    463     return '<a href="' + url + '" target="_blank">' + text + '</a>';
    464 }
    465 
    466 // Returns whether the result for index'th result for testName on builder was
    467 // a failure.
    468 function isFailure(builder, testName, index)
    469 {
    470     var currentIndex = 0;
    471     var rawResults = g_resultsByBuilder[builder].tests[testName].results;
    472     var failureMap = g_resultsByBuilder[builder][results.FAILURE_MAP];
    473     for (var i = 0; i < rawResults.length; i++) {
    474         currentIndex += rawResults[i][results.RLE.LENGTH];
    475         if (currentIndex > index)
    476             return results.isFailingResult(failureMap, rawResults[i][results.RLE.VALUE]);
    477     }
    478     console.error('Index exceeds number of results: ' + index);
    479 }
    480 
    481 // Returns an array of indexes for all builds where this test failed.
    482 function indexesForFailures(builder, testName)
    483 {
    484     var rawResults = g_resultsByBuilder[builder].tests[testName].results;
    485     var buildNumbers = g_resultsByBuilder[builder].buildNumbers;
    486     var failureMap = g_resultsByBuilder[builder][results.FAILURE_MAP];
    487     var index = 0;
    488     var failures = [];
    489     for (var i = 0; i < rawResults.length; i++) {
    490         var numResults = rawResults[i][results.RLE.LENGTH];
    491         if (results.isFailingResult(failureMap, rawResults[i][results.RLE.VALUE])) {
    492             for (var j = 0; j < numResults; j++)
    493                 failures.push(index + j);
    494         }
    495         index += numResults;
    496     }
    497     return failures;
    498 }
    499 
    500 // Returns the path to the failure log for this non-webkit test.
    501 function pathToFailureLog(testName)
    502 {
    503     return '/steps/' + g_history.crossDashboardState.testType + '/logs/' + testName.split('.')[1]
    504 }
    505 
    506 function showPopupForBuild(e, builder, index, opt_testName)
    507 {
    508     var html = '';
    509 
    510     var time = g_resultsByBuilder[builder].secondsSinceEpoch[index];
    511     if (time) {
    512         var date = new Date(time * 1000);
    513         html += date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
    514     }
    515 
    516     var buildNumber = g_resultsByBuilder[builder].buildNumbers[index];
    517     var master = builders.master(builder);
    518     var buildBasePath = master.logPath(builder, buildNumber);
    519 
    520     html += '<ul><li>' + linkHTMLToOpenWindow(buildBasePath, 'Build log');
    521 
    522     if (g_resultsByBuilder[builder][results.BLINK_REVISIONS])
    523         html += '</li><li>Blink: ' + ui.html.blinkRevisionLink(g_resultsByBuilder[builder], index) + '</li>';
    524 
    525     html += '</li><li>Chromium: ' + ui.html.chromiumRevisionLink(g_resultsByBuilder[builder], index) + '</li>';
    526 
    527     var chromeRevision = g_resultsByBuilder[builder].chromeRevision[index];
    528     if (chromeRevision && g_history.isLayoutTestResults()) {
    529         html += '<li><a href="' + TEST_RESULTS_BASE_PATH + currentBuilders()[builder] +
    530             '/' + buildNumber + '/layout-test-results.zip">layout-test-results.zip</a></li>';
    531     }
    532 
    533     if (!g_history.isLayoutTestResults() && opt_testName && isFailure(builder, opt_testName, index))
    534         html += '<li>' + linkHTMLToOpenWindow(buildBasePath + pathToFailureLog(opt_testName), 'Failure log') + '</li>';
    535 
    536     html += '</ul>';
    537     ui.popup.show(e.target, html);
    538 }
    539 
    540 function classNameForFailureString(failure)
    541 {
    542     return failure.replace(/(\+|\ )/, '');
    543 }
    544 
    545 function htmlForTestResults(test)
    546 {
    547     var html = '';
    548     var testResults = test.rawResults.concat();
    549     var times = test.rawTimes.concat();
    550     var builder = test.builder;
    551     var master = builders.master(builder);
    552     var buildNumbers = g_resultsByBuilder[builder].buildNumbers;
    553 
    554     var indexToReplaceCurrentResult = -1;
    555     var indexToReplaceCurrentTime = -1;
    556     for (var i = 0; i < buildNumbers.length; i++) {
    557         var currentResultArray, currentTimeArray, innerHTML, resultString;
    558 
    559         if (i > indexToReplaceCurrentResult) {
    560             currentResultArray = testResults.shift();
    561             if (currentResultArray) {
    562                 resultString = g_resultsByBuilder[builder][results.FAILURE_MAP][currentResultArray[results.RLE.VALUE]];
    563                 indexToReplaceCurrentResult += currentResultArray[results.RLE.LENGTH];
    564             } else {
    565                 resultString = results.NO_DATA;
    566                 indexToReplaceCurrentResult += buildNumbers.length;
    567             }
    568         }
    569 
    570         if (i > indexToReplaceCurrentTime) {
    571             currentTimeArray = times.shift();
    572             var currentTime = 0;
    573             if (currentResultArray) {
    574               currentTime = currentTimeArray[results.RLE.VALUE];
    575               indexToReplaceCurrentTime += currentTimeArray[results.RLE.LENGTH];
    576             } else
    577               indexToReplaceCurrentTime += buildNumbers.length;
    578 
    579             innerHTML = currentTime || '&nbsp;';
    580         }
    581 
    582         html += '<td title="' + resultString + '. Click for more info." class="results ' + classNameForFailureString(resultString) +
    583           '" onclick=\'showPopupForBuild(event, "' + builder + '",' + i + ',"' + test.test + '")\'>' + innerHTML;
    584     }
    585     return html;
    586 }
    587 
    588 function shouldShowTest(testResult)
    589 {
    590     if (!g_history.isLayoutTestResults())
    591         return true;
    592 
    593     if (testResult.expectations == 'WONTFIX')
    594         return g_history.dashboardSpecificState.showWontFix;
    595 
    596     if (testResult.expectations == results.SKIP)
    597         return g_history.dashboardSpecificState.showSkip;
    598 
    599     if (testResult.isFlaky)
    600         return g_history.dashboardSpecificState.showFlaky;
    601 
    602     return g_history.dashboardSpecificState.showNonFlaky;
    603 }
    604 
    605 function createBugHTML(test)
    606 {
    607     var symptom = test.isFlaky ? 'flaky' : 'failing';
    608     var title = encodeURIComponent('Layout Test ' + test.test + ' is ' + symptom);
    609     var description = encodeURIComponent('The following layout test is ' + symptom + ' on ' +
    610         '[insert platform]\n\n' + test.test + '\n\nProbable cause:\n\n' +
    611         '[insert probable cause]');
    612 
    613     url = 'https://code.google.com/p/chromium/issues/entry?template=Layout%20Test%20Failure&summary=' + title + '&comment=' + description;
    614     return '<a href="' + url + '">File new bug</a>';
    615 }
    616 
    617 function isCrossBuilderView()
    618 {
    619     return g_history.dashboardSpecificState.tests || g_history.dashboardSpecificState.result;
    620 }
    621 
    622 function tableHeaders(opt_getAll)
    623 {
    624     var headers = [];
    625     if (isCrossBuilderView() || opt_getAll)
    626         headers.push('builder');
    627 
    628     if (!isCrossBuilderView() || opt_getAll)
    629         headers.push('test');
    630 
    631     if (g_history.isLayoutTestResults() || opt_getAll)
    632         headers.push('bugs', 'expectations');
    633 
    634     headers.push('slowest run', 'flakiness (numbers are runtimes in seconds)');
    635     return headers;
    636 }
    637 
    638 function linkifyBugs(bugs)
    639 {
    640     var html = '';
    641     bugs.forEach(function(bug) {
    642         var bugHtml;
    643         if (string.startsWith(bug, 'Bug('))
    644             bugHtml = bug;
    645         else
    646             bugHtml = '<a href="http://' + bug + '">' + bug + '</a>';
    647         html += '<div>' + bugHtml + '</div>'
    648     });
    649     return html;
    650 }
    651 
    652 function htmlForSingleTestRow(test, showBuilderNames)
    653 {
    654     var headers = tableHeaders();
    655     var html = '';
    656     for (var i = 0; i < headers.length; i++) {
    657         var header = headers[i];
    658         if (string.startsWith(header, 'test') || string.startsWith(header, 'builder')) {
    659             var testCellClassName = 'test-link' + (showBuilderNames ? ' builder-name' : '');
    660             var testCellHTML = showBuilderNames ? test.builder : '<span class="link" onclick="g_history.setQueryParameter(\'tests\',\'' + test.test +'\');">' + test.test + '</span>';
    661             html += '<tr><td class="' + testCellClassName + '">' + testCellHTML;
    662         } else if (string.startsWith(header, 'bugs'))
    663             // FIXME: linkify bugs.
    664             html += '<td class=options-container>' + (linkifyBugs(test.bugs) || createBugHTML(test));
    665         else if (string.startsWith(header, 'expectations'))
    666             html += '<td class=options-container>' + test.expectations;
    667         else if (string.startsWith(header, 'slowest'))
    668             html += '<td>' + (test.slowestTime ? test.slowestTime + 's' : '');
    669         else if (string.startsWith(header, 'flakiness'))
    670             html += htmlForTestResults(test);
    671     }
    672     return html;
    673 }
    674 
    675 function sortColumnFromTableHeader(headerText)
    676 {
    677     return headerText.split(' ', 1)[0];
    678 }
    679 
    680 function htmlForTableColumnHeader(headerName, opt_fillColSpan)
    681 {
    682     // Use the first word of the header title as the sortkey
    683     var thisSortValue = sortColumnFromTableHeader(headerName);
    684     var arrowHTML = thisSortValue == g_history.dashboardSpecificState.sortColumn ?
    685         '<span class=' + g_history.dashboardSpecificState.sortOrder + '>' + (g_history.dashboardSpecificState.sortOrder == FORWARD ? '&uarr;' : '&darr;' ) + '</span>' : '';
    686     return '<th sortValue=' + thisSortValue +
    687         // Extend last th through all the rest of the columns.
    688         (opt_fillColSpan ? ' colspan=10000' : '') +
    689         // Extra span here is so flex boxing actually centers.
    690         // There's probably a better way to do this with CSS only though.
    691         '><div class=table-header-content><span></span>' + arrowHTML +
    692         '<span class=header-text>' + headerName + '</span>' + arrowHTML + '</div></th>';
    693 }
    694 
    695 function htmlForTestTable(rowsHTML, opt_excludeHeaders)
    696 {
    697     var html = '<table class=test-table>';
    698     if (!opt_excludeHeaders) {
    699         html += '<thead><tr>';
    700         var headers = tableHeaders();
    701         for (var i = 0; i < headers.length; i++)
    702             html += htmlForTableColumnHeader(headers[i], i == headers.length - 1);
    703         html += '</tr></thead>';
    704     }
    705     return html + '<tbody>' + rowsHTML + '</tbody></table>';
    706 }
    707 
    708 function appendHTML(html)
    709 {
    710     // InnerHTML to a div that's not in the document. This is
    711     // ~300ms faster in Safari 4 and Chrome 4 on mac.
    712     var div = document.createElement('div');
    713     div.innerHTML = html;
    714     document.body.appendChild(div);
    715     postHeightChangedMessage();
    716 }
    717 
    718 function alphanumericCompare(column, reverse)
    719 {
    720     return reversibleCompareFunction(function(a, b) {
    721         // Put null entries at the bottom
    722         var a = a[column] ? String(a[column]) : 'z';
    723         var b = b[column] ? String(b[column]) : 'z';
    724 
    725         if (a < b)
    726             return -1;
    727         else if (a == b)
    728             return 0;
    729         else
    730             return 1;
    731     }, reverse);
    732 }
    733 
    734 function numericSort(column, reverse)
    735 {
    736     return reversibleCompareFunction(function(a, b) {
    737         a = parseFloat(a[column]);
    738         b = parseFloat(b[column]);
    739         return a - b;
    740     }, reverse);
    741 }
    742 
    743 function reversibleCompareFunction(compare, reverse)
    744 {
    745     return function(a, b) {
    746         return compare(reverse ? b : a, reverse ? a : b);
    747     };
    748 }
    749 
    750 function changeSort(e)
    751 {
    752     var target = e.currentTarget;
    753     e.preventDefault();
    754 
    755     var sortValue = target.getAttribute('sortValue');
    756     while (target && target.tagName != 'TABLE')
    757         target = target.parentNode;
    758 
    759     var sort = 'sortColumn';
    760     var orderKey = 'sortOrder';
    761     if (sortValue == g_history.dashboardSpecificState[sort] && g_history.dashboardSpecificState[orderKey] == FORWARD)
    762         order = BACKWARD;
    763     else
    764         order = FORWARD;
    765 
    766     g_history.setQueryParameter(sort, sortValue, orderKey, order);
    767 }
    768 
    769 function sortTests(tests, column, order)
    770 {
    771     var resultsProperty, sortFunctionGetter;
    772     if (column == 'flakiness') {
    773         sortFunctionGetter = numericSort;
    774         resultsProperty = 'flipCount';
    775     } else if (column == 'slowest') {
    776         sortFunctionGetter = numericSort;
    777         resultsProperty = 'slowestTime';
    778     } else {
    779         sortFunctionGetter = alphanumericCompare;
    780         resultsProperty = column;
    781     }
    782 
    783     tests.sort(sortFunctionGetter(resultsProperty, order == BACKWARD));
    784 }
    785 
    786 function htmlForIndividualTestOnAllBuilders(test)
    787 {
    788     processTestRunsForAllBuilders();
    789 
    790     var testResults = g_testToResultsMap[test];
    791     if (!testResults)
    792         return '<div class="not-found">Test not found. Either it does not exist, is skipped or passes on all recorded runs.</div>';
    793 
    794     var html = '';
    795     var shownBuilders = [];
    796     for (var j = 0; j < testResults.length; j++) {
    797         shownBuilders.push(testResults[j].builder);
    798         var showBuilderNames = true;
    799         html += htmlForSingleTestRow(testResults[j], showBuilderNames);
    800     }
    801 
    802     var skippedBuilders = []
    803     for (builder in currentBuilders()) {
    804         if (shownBuilders.indexOf(builder) == -1)
    805             skippedBuilders.push(builder);
    806     }
    807 
    808     var skippedBuildersHtml = '';
    809     if (skippedBuilders.length) {
    810         skippedBuildersHtml = '<div>The following builders either don\'t run this test (e.g. it\'s skipped) or all recorded runs passed:</div>' +
    811             '<div class=skipped-builder-list><div class=skipped-builder>' + skippedBuilders.join('</div><div class=skipped-builder>') + '</div></div>';
    812     }
    813 
    814     return htmlForTestTable(html) + skippedBuildersHtml;
    815 }
    816 
    817 function htmlForIndividualTestOnAllBuildersWithResultsLinks(test)
    818 {
    819     processTestRunsForAllBuilders();
    820 
    821     var testResults = g_testToResultsMap[test];
    822     var html = '';
    823     html += htmlForIndividualTestOnAllBuilders(test);
    824 
    825     html += '<div class=expectations test=' + test + '><div>' +
    826         linkHTMLToToggleState('showExpectations', 'results')
    827 
    828     if (g_history.isLayoutTestResults() || g_history.isGPUTestResults()) {
    829         if (g_history.isLayoutTestResults())
    830             html += ' | ' + linkHTMLToToggleState('showLargeExpectations', 'large thumbnails');
    831             html += ' | <b>Only shows actual results/diffs from the most recent *failure* on each bot.</b>';
    832     } else {
    833       html += ' | <span>Results height:<input ' +
    834           'onchange="g_history.setQueryParameter(\'resultsHeight\',this.value)" value="' +
    835           g_history.dashboardSpecificState.resultsHeight + '" style="width:2.5em">px</span>';
    836     }
    837     html += '</div></div>';
    838     return html;
    839 }
    840 
    841 function maybeAddPngChecksum(expectationDiv, pngUrl)
    842 {
    843     // pngUrl gets served from the browser cache since we just loaded it in an
    844     // <img> tag.
    845     loader.request(pngUrl,
    846         function(xhr) {
    847             // Convert the first 2k of the response to a byte string.
    848             var bytes = xhr.responseText.substring(0, 2048);
    849             for (var position = 0; position < bytes.length; ++position)
    850                 bytes[position] = bytes[position] & 0xff;
    851 
    852             // Look for the comment.
    853             var commentKey = 'tEXtchecksum\x00';
    854             var checksumPosition = bytes.indexOf(commentKey);
    855             if (checksumPosition == -1)
    856                 return;
    857 
    858             var checksum = bytes.substring(checksumPosition + commentKey.length, checksumPosition + commentKey.length + 32);
    859             var checksumContainer = document.createElement('span');
    860             checksumContainer.innerText = 'Embedded checksum: ' + checksum;
    861             checksumContainer.setAttribute('class', 'pngchecksum');
    862             expectationDiv.parentNode.appendChild(checksumContainer);
    863         },
    864         function(xhr) {},
    865         true);
    866 }
    867 
    868 function getOrCreate(className, parent)
    869 {
    870     var element = parent.querySelector('.' + className);
    871     if (!element) {
    872         element = document.createElement('div');
    873         element.className = className;
    874         parent.appendChild(element);
    875     }
    876     return element;
    877 }
    878 
    879 function handleExpectationsItemLoad(title, item, itemType, parent)
    880 {
    881     item.className = 'expectation';
    882     if (g_history.dashboardSpecificState.showLargeExpectations)
    883         item.className += ' large';
    884 
    885     var titleContainer = document.createElement('h3');
    886     titleContainer.className = 'expectations-title';
    887     titleContainer.textContent = title;
    888 
    889     var itemContainer = document.createElement('span');
    890     itemContainer.appendChild(titleContainer);
    891     itemContainer.className = 'expectations-item ' + title;
    892     itemContainer.appendChild(item);
    893 
    894     // Separate text and image results into separate divs..
    895     var typeContainer = getOrCreate(itemType, parent);
    896 
    897     // Insert results in a consistent order.
    898     var index = EXPECTATIONS_ORDER.indexOf(title);
    899     while (index < EXPECTATIONS_ORDER.length) {
    900         index++;
    901         var elementAfter = typeContainer.querySelector('.' + EXPECTATIONS_ORDER[index]);
    902         if (elementAfter) {
    903             typeContainer.insertBefore(itemContainer, elementAfter);
    904             break;
    905         }
    906     }
    907     if (!itemContainer.parentNode)
    908         typeContainer.appendChild(itemContainer);
    909 
    910     handleFinishedLoadingExpectations(parent);
    911 }
    912 
    913 function addExpectationItem(expectationsContainers, parentContainer, url, opt_builder)
    914 {
    915     // Group expectations by builder, putting test and reference files first.
    916     var builder = opt_builder || "Test and reference files";
    917     var container = expectationsContainers[builder];
    918 
    919     if (!container) {
    920         container = document.createElement('div');
    921         container.className = 'expectations-container';
    922         container.setAttribute('data-builder', builder);
    923         parentContainer.appendChild(container);
    924         expectationsContainers[builder] = container;
    925     }
    926 
    927     var numUnloaded = container.getAttribute('data-unloaded') || 0;
    928     container.setAttribute('data-unloaded', ++numUnloaded);
    929 
    930     var isImage = url.match(/\.png$/);
    931 
    932     var appendExpectationsItem = function(item) {
    933         var itemType = isImage ? 'image' : 'text';
    934         handleExpectationsItemLoad(expectationsTitle(url), item, itemType, container);
    935     };
    936 
    937     var handleLoadError = function() {
    938         handleFinishedLoadingExpectations(container);
    939     };
    940 
    941     if (isImage) {
    942         var dummyNode = document.createElement('img');
    943         dummyNode.onload = function() {
    944             var item = dummyNode;
    945             maybeAddPngChecksum(item, url);
    946             appendExpectationsItem(item);
    947         }
    948         dummyNode.onerror = handleLoadError;
    949         dummyNode.src = url;
    950     } else {
    951         loader.request(url,
    952             function(xhr) {
    953                 var item = document.createElement('pre');
    954                 if (string.endsWith(url, '-wdiff.html'))
    955                     item.innerHTML = xhr.responseText;
    956                 else
    957                     item.textContent = xhr.responseText;
    958                 appendExpectationsItem(item);
    959             },
    960             handleLoadError);
    961     }
    962 }
    963 
    964 function handleFinishedLoadingExpectations(container)
    965 {
    966     var numUnloaded = container.getAttribute('data-unloaded') - 1;
    967     container.setAttribute('data-unloaded', numUnloaded);
    968     if (numUnloaded)
    969         return;
    970 
    971     if (!container.firstChild) {
    972         container.remove();
    973         return;
    974     }
    975 
    976     var builder = container.getAttribute('data-builder');
    977     if (!builder)
    978         return;
    979 
    980     var header = document.createElement('h2');
    981     header.textContent = builder;
    982     container.insertBefore(header, container.firstChild);
    983 }
    984 
    985 function expectationsTitle(url)
    986 {
    987     var matchingSuffixes = ACTUAL_RESULT_SUFFIXES.filter(function(suffix) {
    988         return string.endsWith(url, suffix);
    989     });
    990 
    991     if (matchingSuffixes.length)
    992         return matchingSuffixes[0].split('.')[0];
    993 
    994     var parts = url.split('/');
    995     return parts[parts.length - 1];
    996 }
    997 
    998 function loadExpectations(expectationsContainer)
    999 {
   1000     var test = expectationsContainer.getAttribute('test');
   1001     if (g_history.isLayoutTestResults())
   1002         loadExpectationsLayoutTests(test, expectationsContainer);
   1003     else {
   1004         var testResults = g_testToResultsMap[test];
   1005         for (var i = 0; i < testResults.length; i++)
   1006             if (g_history.isGPUTestResults())
   1007                 loadGPUResultsForBuilder(testResults[i].builder, test, expectationsContainer);
   1008             else
   1009                 loadNonWebKitResultsForBuilder(testResults[i].builder, test, expectationsContainer);
   1010     }
   1011 }
   1012 
   1013 function gpuResultsPath(chromeRevision, builder)
   1014 {
   1015   return chromeRevision + '_' + builder.replace(/[^A-Za-z0-9]+/g, '_');
   1016 }
   1017 
   1018 function loadGPUResultsForBuilder(builder, test, expectationsContainer)
   1019 {
   1020     var container = document.createElement('div');
   1021     container.className = 'expectations-container';
   1022     container.innerHTML = '<div><b>' + builder + '</b></div>';
   1023     expectationsContainer.appendChild(container);
   1024 
   1025     var failureIndex = indexesForFailures(builder, test)[0];
   1026 
   1027     var buildNumber = g_resultsByBuilder[builder].buildNumbers[failureIndex];
   1028     var pathToLog = builders.master(builder).logPath(builder, buildNumber) + pathToFailureLog(test);
   1029 
   1030     var chromeRevision = g_resultsByBuilder[builder].chromeRevision[failureIndex];
   1031     var resultsUrl = GPU_RESULTS_BASE_PATH + gpuResultsPath(chromeRevision, builder);
   1032     var filename = test.split(/\./)[1] + '.png';
   1033 
   1034     appendNonWebKitResults(container, pathToLog, 'non-webkit-results');
   1035     appendNonWebKitResults(container, resultsUrl + '/gen/' + filename, 'gpu-test-results', 'Generated');
   1036     appendNonWebKitResults(container, resultsUrl + '/ref/' + filename, 'gpu-test-results', 'Reference');
   1037     appendNonWebKitResults(container, resultsUrl + '/diff/' + filename, 'gpu-test-results', 'Diff');
   1038 }
   1039 
   1040 function loadNonWebKitResultsForBuilder(builder, test, expectationsContainer)
   1041 {
   1042     var failureIndexes = indexesForFailures(builder, test);
   1043     var container = document.createElement('div');
   1044     container.innerHTML = '<div><b>' + builder + '</b></div>';
   1045     expectationsContainer.appendChild(container);
   1046     for (var i = 0; i < failureIndexes.length; i++) {
   1047         // FIXME: This doesn't seem to work anymore. Did the paths change?
   1048         // Once that's resolved, see if we need to try each gtest modifier prefix as well.
   1049         var buildNumber = g_resultsByBuilder[builder].buildNumbers[failureIndexes[i]];
   1050         var pathToLog = builders.master(builder).logPath(builder, buildNumber) + pathToFailureLog(test);
   1051         appendNonWebKitResults(container, pathToLog, 'non-webkit-results');
   1052     }
   1053 }
   1054 
   1055 function appendNonWebKitResults(container, url, itemClassName, opt_title)
   1056 {
   1057     // Use a script tag to detect whether the URL 404s.
   1058     // Need to use a script tag since the URL is cross-domain.
   1059     var dummyNode = document.createElement('script');
   1060     dummyNode.src = url;
   1061 
   1062     dummyNode.onload = function() {
   1063         var item = document.createElement('iframe');
   1064         item.src = dummyNode.src;
   1065         item.className = itemClassName;
   1066         item.style.height = g_history.dashboardSpecificState.resultsHeight + 'px';
   1067 
   1068         if (opt_title) {
   1069             var childContainer = document.createElement('div');
   1070             childContainer.style.display = 'inline-block';
   1071             var title = document.createElement('div');
   1072             title.textContent = opt_title;
   1073             childContainer.appendChild(title);
   1074             childContainer.appendChild(item);
   1075             container.replaceChild(childContainer, dummyNode);
   1076         } else
   1077             container.replaceChild(item, dummyNode);
   1078     }
   1079     dummyNode.onerror = function() {
   1080         container.removeChild(dummyNode);
   1081     }
   1082 
   1083     container.appendChild(dummyNode);
   1084 }
   1085 
   1086 function lookupVirtualTestSuite(test) {
   1087     for (var suite in VIRTUAL_SUITES) {
   1088         if (test.indexOf(suite) != -1)
   1089             return suite;
   1090     }
   1091     return '';
   1092 }
   1093 
   1094 function baseTest(test, suite) {
   1095     base = VIRTUAL_SUITES[suite];
   1096     return base ? test.replace(suite, base) : test;
   1097 }
   1098 
   1099 function loadTestAndReferenceFiles(expectationsContainers, expectationsContainer, test) {
   1100     var testWithoutSuffix = test.substring(0, test.lastIndexOf('.'));
   1101     var reftest_html_file = testWithoutSuffix + "-expected.html";
   1102     var reftest_mismatch_html_file = testWithoutSuffix + "-expected-mismatch.html";
   1103 
   1104     var suite = lookupVirtualTestSuite(test);
   1105     if (suite) {
   1106         loadTestAndReferenceFiles(expectationsContainers, expectationsContainer, baseTest(test, suite));
   1107         return;
   1108     }
   1109 
   1110     addExpectationItem(expectationsContainers, expectationsContainer, TEST_URL_BASE_PATH_FOR_XHR + test);
   1111     addExpectationItem(expectationsContainers, expectationsContainer, TEST_URL_BASE_PATH_FOR_XHR + reftest_html_file);
   1112     addExpectationItem(expectationsContainers, expectationsContainer, TEST_URL_BASE_PATH_FOR_XHR + reftest_mismatch_html_file);
   1113 }
   1114 
   1115 function loadExpectationsLayoutTests(test, expectationsContainer)
   1116 {
   1117     // Map from file extension to container div for expectations of that type.
   1118     var expectationsContainers = {};
   1119     loadTestAndReferenceFiles(expectationsContainers, expectationsContainer, test);
   1120 
   1121     var testWithoutSuffix = test.substring(0, test.lastIndexOf('.'));
   1122 
   1123     for (var builder in currentBuilders()) {
   1124         var actualResultsBase = TEST_RESULTS_BASE_PATH + currentBuilders()[builder] + '/results/layout-test-results/';
   1125         ACTUAL_RESULT_SUFFIXES.forEach(function(suffix) {{
   1126             addExpectationItem(expectationsContainers, expectationsContainer, actualResultsBase + testWithoutSuffix + '-' + suffix, builder);
   1127         }})
   1128     }
   1129 
   1130     // Add a clearing element so floated elements don't bleed out of their
   1131     // containing block.
   1132     var br = document.createElement('br');
   1133     br.style.clear = 'both';
   1134     expectationsContainer.appendChild(br);
   1135 }
   1136 
   1137 function appendExpectations()
   1138 {
   1139     var expectations = g_history.dashboardSpecificState.showExpectations ? document.getElementsByClassName('expectations') : [];
   1140     g_chunkedActionState = {
   1141         items: expectations,
   1142         index: 0
   1143     }
   1144     performChunkedAction(function(expectation) {
   1145             loadExpectations(expectation);
   1146             postHeightChangedMessage();
   1147         },
   1148         hideLoadingUI,
   1149         expectations);
   1150 }
   1151 
   1152 function hideLoadingUI()
   1153 {
   1154     var loadingDiv = $('loading-ui');
   1155     if (loadingDiv)
   1156         loadingDiv.style.display = 'none';
   1157     postHeightChangedMessage();
   1158 }
   1159 
   1160 function generatePageForIndividualTests(tests)
   1161 {
   1162     console.log('Number of tests: ' + tests.length);
   1163     if (g_history.dashboardSpecificState.showChrome)
   1164         appendHTML(htmlForNavBar());
   1165     performChunkedAction(function(test) {
   1166             appendHTML(htmlForIndividualTest(test));
   1167         },
   1168         appendExpectations,
   1169         tests);
   1170     if (g_history.dashboardSpecificState.showChrome) {
   1171         $('tests-input').value = g_history.dashboardSpecificState.tests;
   1172         $('result-input').value = g_history.dashboardSpecificState.result;
   1173     }
   1174 }
   1175 
   1176 var g_chunkedActionRequestId;
   1177 function performChunkedAction(action, onComplete, items, opt_index) {
   1178     if (g_chunkedActionRequestId)
   1179         cancelAnimationFrame(g_chunkedActionRequestId);
   1180 
   1181     var index = opt_index || 0;
   1182     g_chunkedActionRequestId = requestAnimationFrame(function() {
   1183         if (index < items.length) {
   1184             action(items[index]);
   1185             performChunkedAction(action, onComplete, items, ++index);
   1186         } else {
   1187             onComplete();
   1188         }
   1189     });
   1190 }
   1191 
   1192 function htmlForIndividualTest(test)
   1193 {
   1194     var testNameHtml = '';
   1195     if (g_history.dashboardSpecificState.showChrome) {
   1196         if (g_history.isLayoutTestResults()) {
   1197             var suite = lookupVirtualTestSuite(test);
   1198             var base = suite ? baseTest(test, suite) : test;
   1199             var versionControlUrl = TEST_URL_BASE_PATH_FOR_BROWSING + base;
   1200             testNameHtml += '<h2>' + linkHTMLToOpenWindow(versionControlUrl, test) + '</h2>';
   1201         } else
   1202             testNameHtml += '<h2>' + test + '</h2>';
   1203     }
   1204 
   1205     return testNameHtml + htmlForIndividualTestOnAllBuildersWithResultsLinks(test);
   1206 }
   1207 
   1208 function setTestsParameter(input)
   1209 {
   1210     g_history.setQueryParameter('tests', input.value);
   1211 }
   1212 
   1213 function htmlForNavBar()
   1214 {
   1215     var extraHTML = '';
   1216     var html = ui.html.testTypeSwitcher(false, extraHTML, isCrossBuilderView());
   1217     html += '<div class=forms><form id=result-form ' +
   1218         'onsubmit="g_history.setQueryParameter(\'result\', result.value);' +
   1219         'return false;">Show all tests with result: ' +
   1220         '<input name=result placeholder="e.g. CRASH" id=result-input>' +
   1221         '</form><span>Show tests on all platforms: </span>' +
   1222         // Use a textarea to avoid the 32k limit on the length of inputs.
   1223         '<textarea name=tests ' +
   1224         'placeholder="Comma or space-separated list of tests or partial ' +
   1225         'paths to show test results across all builders, e.g., ' +
   1226         'foo/bar.html,foo/baz,domstorage" id=tests-input onchange="setTestsParameter(this)" ' +
   1227         'onkeydown="if (event.keyCode == 13) { setTestsParameter(this); return false; }"></textarea>' +
   1228         '<span class=link onclick="showLegend()">Show legend [type ?]</span></div>';
   1229     return html;
   1230 }
   1231 
   1232 function checkBoxToToggleState(key, text)
   1233 {
   1234     var stateEnabled = g_history.dashboardSpecificState[key];
   1235     return '<label><input type=checkbox ' + (stateEnabled ? 'checked ' : '') + 'onclick="g_history.setQueryParameter(\'' + key + '\', ' + !stateEnabled + ')">' + text + '</label> ';
   1236 }
   1237 
   1238 function linkHTMLToToggleState(key, linkText)
   1239 {
   1240     var stateEnabled = g_history.dashboardSpecificState[key];
   1241     return '<span class=link onclick="g_history.setQueryParameter(\'' + key + '\', ' + !stateEnabled + ')">' + (stateEnabled ? 'Hide' : 'Show') + ' ' + linkText + '</span>';
   1242 }
   1243 
   1244 function headerForTestTableHtml()
   1245 {
   1246     return '<h2 style="display:inline-block">Failing tests</h2>' +
   1247         checkBoxToToggleState('showFlaky', 'Show flaky') +
   1248         checkBoxToToggleState('showNonFlaky', 'Show non-flaky') +
   1249         checkBoxToToggleState('showSkip', 'Show Skip') +
   1250         checkBoxToToggleState('showWontFix', 'Show WontFix');
   1251 }
   1252 
   1253 function generatePageForBuilder(builderName)
   1254 {
   1255     processTestRunsForBuilder(builderName);
   1256 
   1257     var filteredResults = g_perBuilderFailures[builderName].filter(shouldShowTest);
   1258     sortTests(filteredResults, g_history.dashboardSpecificState.sortColumn, g_history.dashboardSpecificState.sortOrder);
   1259 
   1260     var testsHTML = '';
   1261     if (filteredResults.length) {
   1262         var tableRowsHTML = '';
   1263         var showBuilderNames = false;
   1264         for (var i = 0; i < filteredResults.length; i++)
   1265             tableRowsHTML += htmlForSingleTestRow(filteredResults[i], showBuilderNames)
   1266         testsHTML = htmlForTestTable(tableRowsHTML);
   1267     } else {
   1268         if (g_history.isLayoutTestResults())
   1269             testsHTML += '<div>Fill in one of the text inputs or checkboxes above to show failures.</div>';
   1270         else
   1271             testsHTML += '<div>No tests have failed!</div>';
   1272     }
   1273 
   1274     var html = htmlForNavBar();
   1275 
   1276     if (g_history.isLayoutTestResults())
   1277         html += headerForTestTableHtml();
   1278 
   1279     html += '<br>' + testsHTML;
   1280     appendHTML(html);
   1281 
   1282     var ths = document.getElementsByTagName('th');
   1283     for (var i = 0; i < ths.length; i++) {
   1284         ths[i].addEventListener('click', changeSort, false);
   1285         ths[i].className = "sortable";
   1286     }
   1287 
   1288     hideLoadingUI();
   1289 }
   1290 
   1291 var VALID_KEYS_FOR_CROSS_BUILDER_VIEW = {
   1292     tests: 1,
   1293     result: 1,
   1294     showChrome: 1,
   1295     showExpectations: 1,
   1296     showLargeExpectations: 1,
   1297     resultsHeight: 1,
   1298     revision: 1
   1299 };
   1300 
   1301 function isInvalidKeyForCrossBuilderView(key)
   1302 {
   1303     return !(key in VALID_KEYS_FOR_CROSS_BUILDER_VIEW) && !(key in history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES);
   1304 }
   1305 
   1306 function hideLegend()
   1307 {
   1308     var legend = $('legend');
   1309     if (legend)
   1310         legend.parentNode.removeChild(legend);
   1311 }
   1312 
   1313 function showLegend()
   1314 {
   1315     var legend = $('legend');
   1316     if (!legend) {
   1317         legend = document.createElement('div');
   1318         legend.id = 'legend';
   1319         document.body.appendChild(legend);
   1320     }
   1321 
   1322     var html = '<div id=legend-toggle onclick="hideLegend()">Hide ' +
   1323         'legend [type esc]</div><div id=legend-contents>';
   1324 
   1325     // Just grab the first failureMap. Technically, different builders can have different maps if they
   1326     // haven't all cycled after the map was changed, but meh.
   1327     var failureMap = g_resultsByBuilder[Object.keys(g_resultsByBuilder)[0]][results.FAILURE_MAP];
   1328     for (var expectation in failureMap) {
   1329         var failureString = failureMap[expectation];
   1330         html += '<div class=' + classNameForFailureString(failureString) + '>' + failureString + '</div>';
   1331     }
   1332 
   1333     if (g_history.isLayoutTestResults()) {
   1334       html += '</div><br style="clear:both">' +
   1335           '</div>';
   1336 
   1337       html += '<div>RELEASE TIMEOUTS:</div>' +
   1338           htmlForSlowTimes(RELEASE_TIMEOUT) +
   1339           '<div>DEBUG TIMEOUTS:</div>' +
   1340           htmlForSlowTimes(DEBUG_TIMEOUT);
   1341     }
   1342 
   1343     legend.innerHTML = html;
   1344 }
   1345 
   1346 function htmlForSlowTimes(minTime)
   1347 {
   1348     return '<ul><li>' + minTime + ' seconds</li><li>' +
   1349         SLOW_MULTIPLIER * minTime + ' seconds if marked Slow in TestExpectations</li></ul>';
   1350 }
   1351 
   1352 function postHeightChangedMessage()
   1353 {
   1354     if (window == parent)
   1355         return;
   1356 
   1357     var root = document.documentElement;
   1358     var height = root.offsetHeight;
   1359     if (root.offsetWidth < root.scrollWidth) {
   1360         // We have a horizontal scrollbar. Include it in the height.
   1361         var dummyNode = document.createElement('div');
   1362         dummyNode.style.overflow = 'scroll';
   1363         document.body.appendChild(dummyNode);
   1364         var scrollbarWidth = dummyNode.offsetHeight - dummyNode.clientHeight;
   1365         document.body.removeChild(dummyNode);
   1366         height += scrollbarWidth;
   1367     }
   1368     parent.postMessage({command: 'heightChanged', height: height}, '*')
   1369 }
   1370 
   1371 if (window != parent)
   1372     window.addEventListener('blur', ui.popup.hide);
   1373 
   1374 document.addEventListener('keydown', function(e) {
   1375     if (e.keyIdentifier == 'U+003F' || e.keyIdentifier == 'U+00BF') {
   1376         // WebKit MAC retursn 3F. WebKit WIN returns BF. This is a bug!
   1377         // ? key
   1378         showLegend();
   1379     } else if (e.keyIdentifier == 'U+001B') {
   1380         // escape key
   1381         hideLegend();
   1382         ui.popup.hide();
   1383     }
   1384 }, false);
   1385 
   1386 window.addEventListener('load', function() {
   1387     resourceLoader = new loader.Loader();
   1388     resourceLoader.load();
   1389 }, false);
   1390