c5ab1e39054bdededb4fa9ae523428b457501c05
[WebKit-https.git] / Tools / TestResultServer / static-dashboards / dashboard_base.js
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 // @fileoverview Base JS file for pages that want to parse the results JSON
30 // from the testing bots. This deals with generic utility functions, visible
31 // history, popups and appending the script elements for the JSON files.
32 //
33 // The calling page is expected to implement the following "abstract"
34 // functions/objects:
35 var g_pageLoadStartTime = Date.now();
36 var g_resourceLoader;
37
38 // Generates the contents of the dashboard. The page should override this with
39 // a function that generates the page assuming all resources have loaded.
40 function generatePage() {}
41
42 // Takes a key and a value and sets the g_currentState[key] = value iff key is
43 // a valid hash parameter and the value is a valid value for that key.
44 //
45 // @return {boolean} Whether the key what inserted into the g_currentState.
46 function handleValidHashParameter(key, value)
47 {
48     return false;
49 }
50
51 // Default hash parameters for the page. The page should override this to create
52 // default states.
53 var g_defaultDashboardSpecificStateValues = {};
54
55
56 // The page should override this to modify page state due to
57 // changing query parameters.
58 // @param {Object} params New or modified query params as key: value.
59 // @return {boolean} Whether changing this parameter should cause generatePage to be called.
60 function handleQueryParameterChange(params)
61 {
62     return true;
63 }
64
65 //////////////////////////////////////////////////////////////////////////////
66 // CONSTANTS
67 //////////////////////////////////////////////////////////////////////////////
68 var GTEST_EXPECTATIONS_MAP_ = {
69     'P': 'PASS',
70     'F': 'FAIL',
71     'N': 'NO DATA',
72     'X': 'SKIPPED'
73 };
74
75 var LAYOUT_TEST_EXPECTATIONS_MAP_ = {
76     'P': 'PASS',
77     'N': 'NO DATA',
78     'X': 'SKIP',
79     'T': 'TIMEOUT',
80     'F': 'TEXT',
81     'C': 'CRASH',
82     'I': 'IMAGE',
83     'Z': 'IMAGE+TEXT',
84     // We used to glob a bunch of expectations into "O" as OTHER. Expectations
85     // are more precise now though and it just means MISSING.
86     'O': 'MISSING'
87 };
88
89 var FAILURE_EXPECTATIONS_ = {
90     'T': 1,
91     'F': 1,
92     'C': 1,
93     'I': 1,
94     'Z': 1
95 };
96
97 // Map of parameter to other parameter it invalidates.
98 var CROSS_DB_INVALIDATING_PARAMETERS = {
99     'testType': 'group'
100 };
101 var DB_SPECIFIC_INVALIDATING_PARAMETERS;
102
103 // Keys in the JSON files.
104 var WONTFIX_COUNTS_KEY = 'wontfixCounts';
105 var FIXABLE_COUNTS_KEY = 'fixableCounts';
106 var DEFERRED_COUNTS_KEY = 'deferredCounts';
107 var WONTFIX_DESCRIPTION = 'Tests never to be fixed (WONTFIX)';
108 var FIXABLE_DESCRIPTION = 'All tests for this release';
109 var DEFERRED_DESCRIPTION = 'All deferred tests (DEFER)';
110 var FIXABLE_COUNT_KEY = 'fixableCount';
111 var ALL_FIXABLE_COUNT_KEY = 'allFixableCount';
112 var CHROME_REVISIONS_KEY = 'chromeRevision';
113 var WEBKIT_REVISIONS_KEY = 'webkitRevision';
114 var TIMESTAMPS_KEY = 'secondsSinceEpoch';
115 var BUILD_NUMBERS_KEY = 'buildNumbers';
116 var TESTS_KEY = 'tests';
117 var ONE_DAY_SECONDS = 60 * 60 * 24;
118 var ONE_WEEK_SECONDS = ONE_DAY_SECONDS * 7;
119
120 // These should match the testtype uploaded to test-results.appspot.com.
121 // See http://test-results.appspot.com/testfile.
122 var TEST_TYPES = [
123     'base_unittests',
124     'browser_tests',
125     'cacheinvalidation_unittests',
126     'compositor_unittests',
127     'content_browsertests',
128     'content_unittests',
129     'courgette_unittests',
130     'crypto_unittests',
131     'googleurl_unittests',
132     'gfx_unittests',
133     'gl_tests',
134     'gpu_tests',
135     'gpu_unittests',
136     'installer_util_unittests',
137     'interactive_ui_tests',
138     'ipc_tests',
139     'jingle_unittests',
140     'layout-tests',
141     'media_unittests',
142     'mini_installer_test',
143     'net_unittests',
144     'printing_unittests',
145     'remoting_unittests',
146     'safe_browsing_tests',
147     'sql_unittests',
148     'sync_unit_tests',
149     'sync_integration_tests',
150     'test_shell_tests',
151     'ui_tests',
152     'unit_tests',
153     'views_unittests',
154     'webkit_unit_tests',
155     'androidwebview_instrumentation_tests',
156     'chromiumtestshell_instrumentation_tests',
157     'contentshell_instrumentation_tests',
158     'cc_unittests'
159 ];
160
161 var RELOAD_REQUIRING_PARAMETERS = ['showAllRuns', 'group', 'testType'];
162
163 // Enum for indexing into the run-length encoded results in the JSON files.
164 // 0 is where the count is length is stored. 1 is the value.
165 var RLE = {
166     LENGTH: 0,
167     VALUE: 1
168 }
169
170 function isFailingResult(value)
171 {
172     return 'FSTOCIZ'.indexOf(value) != -1;
173 }
174
175 // Takes a key and a value and sets the g_currentState[key] = value iff key is
176 // a valid hash parameter and the value is a valid value for that key. Handles
177 // cross-dashboard parameters then falls back to calling
178 // handleValidHashParameter for dashboard-specific parameters.
179 //
180 // @return {boolean} Whether the key what inserted into the g_currentState.
181 function handleValidHashParameterWrapper(key, value)
182 {
183     switch(key) {
184     case 'testType':
185         validateParameter(g_crossDashboardState, key, value,
186             function() { return TEST_TYPES.indexOf(value) != -1; });
187         return true;
188
189     case 'group':
190         validateParameter(g_crossDashboardState, key, value,
191             function() {
192               return value in LAYOUT_TESTS_BUILDER_GROUPS ||
193                   value in CHROMIUM_GPU_TESTS_BUILDER_GROUPS ||
194                   value in CHROMIUM_INSTRUMENTATION_TESTS_BUILDER_GROUPS ||
195                   value in CHROMIUM_GTESTS_BUILDER_GROUPS;
196             });
197         return true;
198
199     case 'useTestData':
200     case 'showAllRuns':
201         g_crossDashboardState[key] = value == 'true';
202         return true;
203
204     default:
205         return handleValidHashParameter(key, value);
206     }
207 }
208
209 var g_defaultCrossDashboardStateValues = {
210     group: null,
211     showAllRuns: false,
212     testType: 'layout-tests',
213     useTestData: false,
214 }
215
216 // Generic utility functions.
217 function $(id)
218 {
219     return document.getElementById(id);
220 }
221
222
223 function validateParameter(state, key, value, validateFn)
224 {
225     if (validateFn())
226         state[key] = value;
227     else
228         console.log(key + ' value is not valid: ' + value);
229 }
230
231 function queryHashAsMap()
232 {
233     var hash = window.location.hash;
234     var paramsList = hash ? hash.substring(1).split('&') : [];
235     var paramsMap = {};
236     var invalidKeys = [];
237     for (var i = 0; i < paramsList.length; i++) {
238         var thisParam = paramsList[i].split('=');
239         if (thisParam.length != 2) {
240             console.log('Invalid query parameter: ' + paramsList[i]);
241             continue;
242         }
243
244         paramsMap[thisParam[0]] = decodeURIComponent(thisParam[1]);
245     }
246
247     // FIXME: remove support for mapping from the master parameter to the group
248     // one once the waterfall starts to pass in the builder name instead.
249     if (paramsMap.master) {
250         paramsMap.group = LEGACY_BUILDER_MASTERS_TO_GROUPS[paramsMap.master];
251         if (!paramsMap.group)
252             console.log('ERROR: Unknown master name: ' + paramsMap.master);
253         window.location.hash = window.location.hash.replace('master=' + paramsMap.master, 'group=' + paramsMap.group);
254         delete paramsMap.master;
255     }
256
257     return paramsMap;
258 }
259
260 function parseParameter(parameters, key)
261 {
262     if (!(key in parameters))
263         return;
264     var value = parameters[key];
265     if (!handleValidHashParameterWrapper(key, value))
266         console.log("Invalid query parameter: " + key + '=' + value);
267 }
268
269 function parseCrossDashboardParameters()
270 {
271     g_crossDashboardState = {};
272     var parameters = queryHashAsMap();
273     for (parameterName in g_defaultCrossDashboardStateValues)
274         parseParameter(parameters, parameterName);
275
276     fillMissingValues(g_crossDashboardState, g_defaultCrossDashboardStateValues);
277 }
278
279 function parseDashboardSpecificParameters()
280 {
281     g_currentState = {};
282     var parameters = queryHashAsMap();
283     for (parameterName in g_defaultDashboardSpecificStateValues)
284         parseParameter(parameters, parameterName);
285 }
286
287 // @return {boolean} Whether to generate the page.
288 function parseParameters()
289 {
290     var oldCrossDashboardState = g_crossDashboardState;
291     var oldDashboardSpecificState = g_currentState;
292
293     parseCrossDashboardParameters();
294     
295     // Some parameters require loading different JSON files when the value changes. Do a reload.
296     if (Object.keys(oldCrossDashboardState).length) {
297         for (var key in g_crossDashboardState) {
298             if (oldCrossDashboardState[key] != g_crossDashboardState[key] && RELOAD_REQUIRING_PARAMETERS.indexOf(key) != -1) {
299                 window.location.reload();
300                 return false;
301             }
302         }
303     }
304
305     parseDashboardSpecificParameters();
306     var dashboardSpecificDiffState = diffStates(oldDashboardSpecificState, g_currentState);
307
308     fillMissingValues(g_currentState, g_defaultDashboardSpecificStateValues);
309
310     // FIXME: dashboard_base shouldn't know anything about specific dashboard specific keys.
311     if (dashboardSpecificDiffState.builder)
312         delete g_currentState.tests;
313     if (g_currentState.tests)
314         delete g_currentState.builder;
315
316     var shouldGeneratePage = true;
317     if (Object.keys(dashboardSpecificDiffState).length)
318         shouldGeneratePage = handleQueryParameterChange(dashboardSpecificDiffState);
319     return shouldGeneratePage;
320 }
321
322 function diffStates(oldState, newState)
323 {
324     // If there is no old state, everything in the current state is new.
325     if (!oldState)
326         return newState;
327
328     var changedParams = {};
329     for (curKey in newState) {
330         var oldVal = oldState[curKey];
331         var newVal = newState[curKey];
332         // Add new keys or changed values.
333         if (!oldVal || oldVal != newVal)
334             changedParams[curKey] = newVal;
335     }
336     return changedParams;
337 }
338
339 function defaultValue(key)
340 {
341     if (key in g_defaultDashboardSpecificStateValues)
342         return g_defaultDashboardSpecificStateValues[key];
343     return g_defaultCrossDashboardStateValues[key];
344 }
345
346 function fillMissingValues(to, from)
347 {
348     for (var state in from) {
349         if (!(state in to))
350             to[state] = from[state];
351     }
352 }
353
354 // FIXME: Rename this to g_dashboardSpecificState;
355 var g_currentState = {};
356 var g_crossDashboardState = {};
357 parseCrossDashboardParameters();
358
359 function isLayoutTestResults()
360 {
361     return g_crossDashboardState.testType == 'layout-tests';
362 }
363
364 function isGPUTestResults()
365 {
366     return g_crossDashboardState.testType == 'gpu_tests';
367 }
368
369 function currentBuilderGroupCategory()
370 {
371     switch (g_crossDashboardState.testType) {
372     case 'gl_tests':
373     case 'gpu_tests':
374         return CHROMIUM_GPU_TESTS_BUILDER_GROUPS;
375     case 'layout-tests':
376         return LAYOUT_TESTS_BUILDER_GROUPS;
377     case 'test_shell_tests':
378     case 'webkit_unit_tests':
379         return TEST_SHELL_TESTS_BUILDER_GROUPS;
380     case 'androidwebview_instrumentation_tests':
381     case 'chromiumtestshell_instrumentation_tests':
382     case 'contentshell_instrumentation_tests':
383         return CHROMIUM_INSTRUMENTATION_TESTS_BUILDER_GROUPS;
384     case 'cc_unittests':
385         return CC_UNITTEST_BUILDER_GROUPS;
386     default:
387         return CHROMIUM_GTESTS_BUILDER_GROUPS;
388     }
389 }
390
391 function currentBuilderGroupName()
392 {
393     return g_crossDashboardState.group || Object.keys(currentBuilderGroupCategory())[0];
394 }
395
396 function currentBuilderGroup()
397 {
398     return currentBuilderGroupCategory()[currentBuilderGroupName()];
399 }
400
401 function currentBuilders()
402 {
403     return currentBuilderGroup().builders;
404 }
405
406 function isTipOfTreeWebKitBuilder()
407 {
408     return currentBuilderGroup().isToTWebKit;
409 }
410
411 var g_resultsByBuilder = {};
412 var g_expectationsByPlatform = {};
413
414 // TODO(aboxhall): figure out whether this is a performance bottleneck and
415 // change calling code to understand the trie structure instead if necessary.
416 function flattenTrie(trie, prefix)
417 {
418     var result = {};
419     for (var name in trie) {
420         var fullName = prefix ? prefix + "/" + name : name;
421         var data = trie[name];
422         if ("results" in data)
423             result[fullName] = data;
424         else {
425             var partialResult = flattenTrie(data, fullName);
426             for (var key in partialResult) {
427                 result[key] = partialResult[key];
428             }
429         }
430     }
431     return result;
432 }
433
434 function isTreeMap()
435 {
436     return string.endsWith(window.location.pathname, 'treemap.html');
437 }
438
439 function isFlakinessDashboard()
440 {
441     return string.endsWith(window.location.pathname, 'flakiness_dashboard.html');
442 }
443
444 // String of error messages to display to the user.
445 var g_errorMessages = '';
446
447 // Record a new error message.
448 // @param {string} errorMsg The message to show to the user.
449 function addError(errorMsg)
450 {
451     g_errorMessages += errorMsg + '<br>';
452 }
453
454
455 // If there are errors, show big and red UI for errors so as to be noticed.
456 function showErrors()
457 {
458     var errors = $('errors');
459
460     if (!g_errorMessages) {
461         if (errors)
462             errors.parentNode.removeChild(errors);
463         return;
464     }
465
466     if (!errors) {
467         errors = document.createElement('H2');
468         errors.style.color = 'red';
469         errors.id = 'errors';
470         document.body.appendChild(errors);
471     }
472
473     errors.innerHTML = g_errorMessages;
474 }
475
476 function resourceLoadingComplete(errorMsgs)
477 {
478     if (errorMsgs)
479         addError(errorMsgs)
480
481     handleLocationChange();
482 }
483
484 function handleLocationChange()
485 {
486     if (!g_resourceLoader.isLoadingComplete())
487         return;
488
489     if (parseParameters())
490         generatePage();
491 }
492
493 window.onhashchange = handleLocationChange;
494
495 function combinedDashboardState()
496 {
497     var combinedState = Object.create(g_currentState);
498     for (var key in g_crossDashboardState)
499         combinedState[key] = g_crossDashboardState[key];
500     return combinedState;    
501 }
502
503 function invalidateQueryParameters(queryParamsAsState) {
504     for (var key in queryParamsAsState) {
505         if (key in CROSS_DB_INVALIDATING_PARAMETERS)
506             delete g_crossDashboardState[CROSS_DB_INVALIDATING_PARAMETERS[key]];
507         if (key in DB_SPECIFIC_INVALIDATING_PARAMETERS)
508             delete g_currentState[DB_SPECIFIC_INVALIDATING_PARAMETERS[key]];
509     }
510 }
511
512 // Sets the page state. Takes varargs of key, value pairs.
513 function setQueryParameter(var_args)
514 {
515     var queryParamsAsState = {};
516     for (var i = 0; i < arguments.length; i += 2) {
517         var key = arguments[i];
518         queryParamsAsState[key] = arguments[i + 1];
519     }
520
521     invalidateQueryParameters(queryParamsAsState);
522
523     var newState = combinedDashboardState();
524     for (var key in queryParamsAsState) {
525         newState[key] = queryParamsAsState[key];
526     }
527
528     // Note: We use window.location.hash rather that window.location.replace
529     // because of bugs in Chrome where extra entries were getting created
530     // when back button was pressed and full page navigation was occuring.
531     // FIXME: file those bugs.
532     window.location.hash = permaLinkURLHash(newState);
533 }
534
535 function permaLinkURLHash(opt_state)
536 {
537     var state = opt_state || combinedDashboardState();
538     return '#' + joinParameters(state);
539 }
540
541 function joinParameters(stateObject)
542 {
543     var state = [];
544     for (var key in stateObject) {
545         var value = stateObject[key];
546         if (value != defaultValue(key))
547             state.push(key + '=' + encodeURIComponent(value));
548     }
549     return state.join('&');
550 }
551
552 function logTime(msg, startTime)
553 {
554     console.log(msg + ': ' + (Date.now() - startTime));
555 }
556
557 function hidePopup()
558 {
559     var popup = $('popup');
560     if (popup)
561         popup.parentNode.removeChild(popup);
562 }
563
564 function showPopup(target, html)
565 {
566     var popup = $('popup');
567     if (!popup) {
568         popup = document.createElement('div');
569         popup.id = 'popup';
570         document.body.appendChild(popup);
571     }
572
573     // Set html first so that we can get accurate size metrics on the popup.
574     popup.innerHTML = html;
575
576     var targetRect = target.getBoundingClientRect();
577
578     var x = Math.min(targetRect.left - 10, document.documentElement.clientWidth - popup.offsetWidth);
579     x = Math.max(0, x);
580     popup.style.left = x + document.body.scrollLeft + 'px';
581
582     var y = targetRect.top + targetRect.height;
583     if (y + popup.offsetHeight > document.documentElement.clientHeight)
584         y = targetRect.top - popup.offsetHeight;
585     y = Math.max(0, y);
586     popup.style.top = y + document.body.scrollTop + 'px';
587 }
588
589 // Create a new function with some of its arguements
590 // pre-filled.
591 // Taken from goog.partial in the Closure library.
592 // @param {Function} fn A function to partially apply.
593 // @param {...*} var_args Additional arguments that are partially
594 //         applied to fn.
595 // @return {!Function} A partially-applied form of the function bind() was
596 //         invoked as a method of.
597 function partial(fn, var_args)
598 {
599     var args = Array.prototype.slice.call(arguments, 1);
600     return function() {
601         // Prepend the bound arguments to the current arguments.
602         var newArgs = Array.prototype.slice.call(arguments);
603         newArgs.unshift.apply(newArgs, args);
604         return fn.apply(this, newArgs);
605     };
606 };
607
608 // Returns the appropriate expectatiosn map for the current testType.
609 function expectationsMap()
610 {
611     return isLayoutTestResults() ? LAYOUT_TEST_EXPECTATIONS_MAP_ : GTEST_EXPECTATIONS_MAP_;
612 }
613
614 function toggleQueryParameter(param)
615 {
616     setQueryParameter(param, !queryParameterValue(param));
617 }
618
619 function queryParameterValue(parameter)
620 {
621     return g_currentState[parameter] || g_crossDashboardState[parameter];
622 }
623
624 function checkboxHTML(queryParameter, label, isChecked, opt_extraJavaScript)
625 {
626     var js = opt_extraJavaScript || '';
627     return '<label style="padding-left: 2em">' +
628         '<input type="checkbox" onchange="toggleQueryParameter(\'' + queryParameter + '\');' + js + '" ' +
629             (isChecked ? 'checked' : '') + '>' + label +
630         '</label> ';
631 }
632
633 function selectHTML(label, queryParameter, options)
634 {
635     var html = '<label style="padding-left: 2em">' + label + ': ' +
636         '<select onchange="setQueryParameter(\'' + queryParameter + '\', this[this.selectedIndex].value)">';
637
638     for (var i = 0; i < options.length; i++) {
639         var value = options[i];
640         html += '<option value="' + value + '" ' +
641             (queryParameterValue(queryParameter) == value ? 'selected' : '') +
642             '>' + value + '</option>'
643     }
644     html += '</select></label> ';
645     return html;
646 }
647
648 // Returns the HTML for the select element to switch to different testTypes.
649 function htmlForTestTypeSwitcher(opt_noBuilderMenu, opt_extraHtml, opt_includeNoneBuilder)
650 {
651     var html = '<div style="border-bottom:1px dashed">';
652     html += '' +
653         htmlForDashboardLink('Stats', 'aggregate_results.html') +
654         htmlForDashboardLink('Timeline', 'timeline_explorer.html') +
655         htmlForDashboardLink('Results', 'flakiness_dashboard.html') +
656         htmlForDashboardLink('Treemap', 'treemap.html');
657
658     html += selectHTML('Test type', 'testType', TEST_TYPES);
659
660     if (!opt_noBuilderMenu) {
661         var buildersForMenu = Object.keys(currentBuilders());
662         if (opt_includeNoneBuilder)
663             buildersForMenu.unshift('--------------');
664         html += selectHTML('Builder', 'builder', buildersForMenu);
665     }
666
667     html += selectHTML('Group', 'group',
668         Object.keys(currentBuilderGroupCategory()));
669
670     if (!isTreeMap())
671         html += checkboxHTML('showAllRuns', 'Show all runs', g_crossDashboardState.showAllRuns);
672
673     if (opt_extraHtml)
674         html += opt_extraHtml;
675     return html + '</div>';
676 }
677
678 function loadDashboard(fileName)
679 {
680     var pathName = window.location.pathname;
681     pathName = pathName.substring(0, pathName.lastIndexOf('/') + 1);
682     window.location = pathName + fileName + window.location.hash;
683 }
684
685 function htmlForTopLink(html, onClick, isSelected)
686 {
687     var cssText = isSelected ? 'font-weight: bold;' : 'color:blue;text-decoration:underline;cursor:pointer;';
688     cssText += 'margin: 0 5px;';
689     return '<span style="' + cssText + '" onclick="' + onClick + '">' + html + '</span>';
690 }
691
692 function htmlForDashboardLink(html, fileName)
693 {
694     var pathName = window.location.pathname;
695     var currentFileName = pathName.substring(pathName.lastIndexOf('/') + 1);
696     var isSelected = currentFileName == fileName;
697     var onClick = 'loadDashboard(\'' + fileName + '\')';
698     return htmlForTopLink(html, onClick, isSelected);
699 }
700
701 function revisionLink(results, index, key, singleUrlTemplate, rangeUrlTemplate)
702 {
703     var currentRevision = parseInt(results[key][index], 10);
704     var previousRevision = parseInt(results[key][index + 1], 10);
705
706     function singleUrl()
707     {
708         return singleUrlTemplate.replace('<rev>', currentRevision);
709     }
710
711     function rangeUrl()
712     {
713         return rangeUrlTemplate.replace('<rev1>', currentRevision).replace('<rev2>', previousRevision + 1);
714     }
715
716     if (currentRevision == previousRevision)
717         return 'At <a href="' + singleUrl() + '">r' + currentRevision    + '</a>';
718     else if (currentRevision - previousRevision == 1)
719         return '<a href="' + singleUrl() + '">r' + currentRevision    + '</a>';
720     else
721         return '<a href="' + rangeUrl() + '">r' + (previousRevision + 1) + ' to r' + currentRevision + '</a>';
722 }
723
724 function chromiumRevisionLink(results, index)
725 {
726     return revisionLink(
727         results,
728         index,
729         CHROME_REVISIONS_KEY,
730         'http://src.chromium.org/viewvc/chrome?view=rev&revision=<rev>',
731         'http://build.chromium.org/f/chromium/perf/dashboard/ui/changelog.html?url=/trunk/src&range=<rev2>:<rev1>&mode=html');
732 }
733
734 function webKitRevisionLink(results, index)
735 {
736     return revisionLink(
737         results,
738         index,
739         WEBKIT_REVISIONS_KEY,
740         'http://trac.webkit.org/changeset/<rev>',
741         'http://trac.webkit.org/log/trunk/?rev=<rev1>&stop_rev=<rev2>&limit=100&verbose=on');
742 }
743
744 // "Decompresses" the RLE-encoding of test results so that we can query it
745 // by build index and test name.
746 //
747 // @param {Object} results results for the current builder
748 // @return Object with these properties:
749 //     - testNames: array mapping test index to test names.
750 //     - resultsByBuild: array of builds, for each build a (sparse) array of test results by test index.
751 //     - flakyTests: array with the boolean value true at test indices that are considered flaky (more than one single-build failure).
752 //     - flakyDeltasByBuild: array of builds, for each build a count of flaky test results by expectation, as well as a total.
753 function decompressResults(builderResults)
754 {
755     var builderTestResults = builderResults[TESTS_KEY];
756     var buildCount = builderResults[FIXABLE_COUNTS_KEY].length;
757     var resultsByBuild = new Array(buildCount);
758     var flakyDeltasByBuild = new Array(buildCount);
759
760     // Pre-sizing the test result arrays for each build saves us ~250ms
761     var testCount = 0;
762     for (var testName in builderTestResults)
763         testCount++;
764     for (var i = 0; i < buildCount; i++) {
765         resultsByBuild[i] = new Array(testCount);
766         resultsByBuild[i][testCount - 1] = undefined;
767         flakyDeltasByBuild[i] = {};
768     }
769
770     // Using indices instead of the full test names for each build saves us
771     // ~1500ms
772     var testIndex = 0;
773     var testNames = new Array(testCount);
774     var flakyTests = new Array(testCount);
775
776     // Decompress and "invert" test results (by build instead of by test) and
777     // determine which are flaky.
778     for (var testName in builderTestResults) {
779         var oneBuildFailureCount = 0;
780
781         testNames[testIndex] = testName;
782         var testResults = builderTestResults[testName].results;
783         for (var i = 0, rleResult, currentBuildIndex = 0; (rleResult = testResults[i]) && currentBuildIndex < buildCount; i++) {
784             var count = rleResult[RLE.LENGTH];
785             var value = rleResult[RLE.VALUE];
786
787             if (count == 1 && value in FAILURE_EXPECTATIONS_)
788                 oneBuildFailureCount++;
789
790             for (var j = 0; j < count; j++) {
791                 resultsByBuild[currentBuildIndex++][testIndex] = value;
792                 if (currentBuildIndex == buildCount)
793                     break;
794             }
795         }
796
797         if (oneBuildFailureCount > 2)
798             flakyTests[testIndex] = true;
799
800         testIndex++;
801     }
802
803     // Now that we know which tests are flaky, count the test results that are
804     // from flaky tests for each build.
805     testIndex = 0;
806     for (var testName in builderTestResults) {
807         if (!flakyTests[testIndex++])
808             continue;
809
810         var testResults = builderTestResults[testName].results;
811         for (var i = 0, rleResult, currentBuildIndex = 0; (rleResult = testResults[i]) && currentBuildIndex < buildCount; i++) {
812             var count = rleResult[RLE.LENGTH];
813             var value = rleResult[RLE.VALUE];
814
815             for (var j = 0; j < count; j++) {
816                 var buildTestResults = flakyDeltasByBuild[currentBuildIndex++];
817                 function addFlakyDelta(key)
818                 {
819                     if (!(key in buildTestResults))
820                         buildTestResults[key] = 0;
821                     buildTestResults[key]++;
822                 }
823                 addFlakyDelta(value);
824                 if (value != 'P' && value != 'N')
825                     addFlakyDelta('total');
826                 if (currentBuildIndex == buildCount)
827                     break;
828             }
829         }
830     }
831
832     return {
833         testNames: testNames,
834         resultsByBuild: resultsByBuild,
835         flakyTests: flakyTests,
836         flakyDeltasByBuild: flakyDeltasByBuild
837     };
838 }
839
840 document.addEventListener('mousedown', function(e) {
841     // Clear the open popup, unless the click was inside the popup.
842     var popup = $('popup');
843     if (popup && e.target != popup && !(popup.compareDocumentPosition(e.target) & 16))
844         hidePopup();
845 }, false);
846
847 window.addEventListener('load', function() {
848     // This doesn't seem totally accurate as there is a race between
849     // onload firing and the last script tag being executed.
850     logTime('Time to load JS', g_pageLoadStartTime);
851     g_resourceLoader = new loader.Loader();
852     g_resourceLoader.load();
853 }, false);