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