initial import
[vuplus_webkit] / Tools / TestResultServer / static-dashboards / dashboard_base.js
1 // Copyright (C) 2011 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
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_defaultStateValues = {};
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     'L': 'FLAKY',
71     'N': 'NO DATA',
72     'X': 'DISABLED'
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 // Keys in the JSON files.
98 var WONTFIX_COUNTS_KEY = 'wontfixCounts';
99 var FIXABLE_COUNTS_KEY = 'fixableCounts';
100 var DEFERRED_COUNTS_KEY = 'deferredCounts';
101 var WONTFIX_DESCRIPTION = 'Tests never to be fixed (WONTFIX)';
102 var FIXABLE_DESCRIPTION = 'All tests for this release';
103 var DEFERRED_DESCRIPTION = 'All deferred tests (DEFER)';
104 var FIXABLE_COUNT_KEY = 'fixableCount';
105 var ALL_FIXABLE_COUNT_KEY = 'allFixableCount';
106 var CHROME_REVISIONS_KEY = 'chromeRevision';
107 var WEBKIT_REVISIONS_KEY = 'webkitRevision';
108 var TIMESTAMPS_KEY = 'secondsSinceEpoch';
109 var BUILD_NUMBERS_KEY = 'buildNumbers';
110 var TESTS_KEY = 'tests';
111 var TWO_WEEKS_SECONDS = 60 * 60 * 24 * 14;
112
113 // These should match the testtype uploaded to test-results.appspot.com.
114 // See http://test-results.appspot.com/testfile.
115 var TEST_TYPES = ['app_unittests', 'base_unittests', 'browser_tests',
116         'cache_invalidation_unittests', 'courgette_unittests',
117         'crypto_unittests', 'googleurl_unittests', 'gpu_unittests',
118         'installer_util_unittests', 'interactive_ui_tests', 'ipc_tests',
119         'jingle_unittests', 'layout-tests', 'media_unittests',
120         'mini_installer_test', 'nacl_ui_tests', 'net_unittests',
121         'printing_unittests', 'remoting_unittests', 'safe_browsing_tests',
122         'sync_unit_tests', 'sync_integration_tests',
123         'test_shell_tests', 'ui_tests', 'unit_tests'];
124
125 var RELOAD_REQUIRING_PARAMETERS = ['showAllRuns', 'group', 'testType'];
126
127 // Enum for indexing into the run-length encoded results in the JSON files.
128 // 0 is where the count is length is stored. 1 is the value.
129 var RLE = {
130     LENGTH: 0,
131     VALUE: 1
132 }
133
134 var TEST_RESULTS_SERVER = 'http://test-results.appspot.com/';
135
136 function isFailingResult(value)
137 {
138     return 'FSTOCIZ'.indexOf(value) != -1;
139 }
140
141 // Takes a key and a value and sets the g_currentState[key] = value iff key is
142 // a valid hash parameter and the value is a valid value for that key. Handles
143 // cross-dashboard parameters then falls back to calling
144 // handleValidHashParameter for dashboard-specific parameters.
145 //
146 // @return {boolean} Whether the key what inserted into the g_currentState.
147 function handleValidHashParameterWrapper(key, value)
148 {
149     switch(key) {
150     case 'testType':
151         validateParameter(g_currentState, key, value,
152             function() { return TEST_TYPES.indexOf(value) != -1; });
153         return true;
154
155     case 'group':
156         validateParameter(g_currentState, key, value,
157             function() { return value in LAYOUT_TESTS_BUILDER_GROUPS; });
158         return true;
159
160     // FIXME: remove support for this parameter once the waterfall starts to
161     // pass in the builder name instead.
162     case 'master':
163         validateParameter(g_currentState, key, value,
164             function() { return value in LEGACY_BUILDER_MASTERS_TO_GROUPS; });
165         return true;
166
167     case 'builder':
168         validateParameter(g_currentState, key, value,
169             function() { return value in g_builders; });
170         return true;
171
172     case 'useTestData':
173     case 'showAllRuns':
174         g_currentState[key] = value == 'true';
175         return true;
176
177     case 'buildDir':
178         g_currentState['testType'] = 'layout-test-results';
179         if (value === 'Debug' || value == 'Release') {
180             g_currentState[key] = value;
181             return true;
182         } else
183             return false;
184
185     default:
186         return handleValidHashParameter(key, value);
187     }
188 }
189
190 var g_defaultCrossDashboardStateValues = {
191     group: '@ToT - chromium.org',
192     showAllRuns: false,
193     testType: 'layout-tests',
194     buildDir : ''
195 }
196
197 // Generic utility functions.
198 function $(id)
199 {
200     return document.getElementById(id);
201 }
202
203 function stringContains(a, b)
204 {
205     return a.indexOf(b) != -1;
206 }
207
208 function caseInsensitiveContains(a, b)
209 {
210     return a.match(new RegExp(b, 'i'));
211 }
212
213 function startsWith(a, b)
214 {
215     return a.indexOf(b) == 0;
216 }
217
218 function endsWith(a, b)
219 {
220     return a.lastIndexOf(b) == a.length - b.length;
221 }
222
223 function isValidName(str)
224 {
225     return str.match(/[A-Za-z0-9\-\_,]/);
226 }
227
228 function trimString(str)
229 {
230     return str.replace(/^\s+|\s+$/g, '');
231 }
232
233 function collapseWhitespace(str)
234 {
235     return str.replace(/\s+/g, ' ');
236 }
237
238 function request(url, success, error, opt_isBinaryData)
239 {
240     console.log('XMLHttpRequest: ' + url);
241     var xhr = new XMLHttpRequest();
242     xhr.open('GET', url, true);
243     if (opt_isBinaryData)
244         xhr.overrideMimeType('text/plain; charset=x-user-defined');
245     xhr.onreadystatechange = function(e) {
246         if (xhr.readyState == 4) {
247             if (xhr.status == 200)
248                 success(xhr);
249             else
250                 error(xhr);
251         }
252     }
253     xhr.send();
254 }
255
256 function validateParameter(state, key, value, validateFn)
257 {
258     if (validateFn())
259         state[key] = value;
260     else
261         console.log(key + ' value is not valid: ' + value);
262 }
263
264 function parseParameters(parameterStr)
265 {
266     g_oldState = g_currentState;
267     g_currentState = {};
268
269     var hash = window.location.hash;
270     var paramsList = hash ? hash.substring(1).split('&') : [];
271     var paramsMap = {};
272     var invalidKeys = [];
273     for (var i = 0; i < paramsList.length; i++) {
274         var thisParam = paramsList[i].split('=');
275         if (thisParam.length != 2) {
276             console.log('Invalid query parameter: ' + paramsList[i]);
277             continue;
278         }
279
280         paramsMap[thisParam[0]] = decodeURIComponent(thisParam[1]);
281     }
282
283     function parseParam(key)
284     {
285         var value = paramsMap[key];
286         if (!handleValidHashParameterWrapper(key, value))
287             invalidKeys.push(key + '=' + value);
288     }
289
290     // Parse builder param last, since the list of valid builders depends on the other parameters.
291     for (var key in paramsMap) {
292         if (key != 'builder')
293             parseParam(key);
294     }
295     if ('builder' in paramsMap) {
296         if (!g_builders) {
297             var tempState = {};
298             for (var key in g_currentState)
299                 tempState[key] = g_currentState[key];
300             fillMissingValues(tempState, g_defaultCrossDashboardStateValues);
301             fillMissingValues(tempState, g_defaultStateValues);
302             initBuilders(tempState);
303         }
304         parseParam('builder');
305     }
306
307     if (invalidKeys.length)
308         console.log("Invalid query parameters: " + invalidKeys.join(','));
309
310     var diffState = diffStates();
311
312     fillMissingValues(g_currentState, g_defaultCrossDashboardStateValues);
313     fillMissingValues(g_currentState, g_defaultStateValues);
314
315     // Some parameters require loading different JSON files when the value changes. Do a reload.
316     if (g_oldState) {
317         for (var key in g_currentState) {
318             if (g_oldState[key] != g_currentState[key] && RELOAD_REQUIRING_PARAMETERS.indexOf(key) != -1)
319                 window.location.reload();
320         }
321     }
322
323     return diffState;
324 }
325
326 // Find the difference of g_currentState with g_oldState.
327 // @return {Object} key:values of the new or changed params.
328 function diffStates()
329 {
330     // If there is no old state, everything in the current state is new.
331     if (!g_oldState)
332         return g_currentState;
333
334     var changedParams = {};
335     for (curKey in g_currentState) {
336         var oldVal = g_oldState[curKey];
337         var newVal = g_currentState[curKey];
338         // Add new keys or changed values.
339         if (!oldVal || oldVal != newVal)
340             changedParams[curKey] = newVal;
341     }
342     return changedParams;
343 }
344
345 function defaultValue(key)
346 {
347     if (key in g_defaultStateValues)
348         return g_defaultStateValues[key];
349     return g_defaultCrossDashboardStateValues[key];
350 }
351
352 function fillMissingValues(to, from)
353 {
354     for (var state in from) {
355         if (!(state in to))
356             to[state] = from[state];
357     }
358 }
359
360 // Load a script.
361 // @param {string} path Path to the script to load.
362 // @param {Function=} opt_onError Optional function to call if the script
363 //         does not load.
364 // @param {Function=} opt_onLoad Optional function to call when the script
365 //         is loaded.    Called with the script element as its 1st argument.
366 function appendScript(path, opt_onError, opt_onLoad)
367 {
368     var script = document.createElement('script');
369     script.src = path;
370     if (opt_onLoad) {
371         script.onreadystatechange = function() {
372             if (this.readyState == 'complete')
373                 opt_onLoad(script);
374         };
375         script.onload = function() { opt_onLoad(script); };
376     }
377     if (opt_onError)
378         script.onerror = opt_onError;
379     document.getElementsByTagName('head')[0].appendChild(script);
380 }
381
382 // Permalinkable state of the page.
383 var g_currentState;
384
385 // Saved value of previous g_currentState. This is used to detect changing from
386 // one set of builders to another, which requires reloading the page.
387 var g_oldState;
388
389 // Parse cross-dashboard parameters before using them.
390 parseParameters();
391
392 function isLayoutTestResults()
393 {
394     return g_currentState.testType == 'layout-tests';
395 }
396
397 function currentBuilderGroup(opt_state)
398 {
399     var state = opt_state || g_currentState;
400     switch (state.testType) {
401     case 'layout-tests':
402         return LAYOUT_TESTS_BUILDER_GROUPS[state.group]
403         break;
404     case 'app_unittests':
405     case 'base_unittests':
406     case 'browser_tests':
407     case 'cacheinvalidation_unittests':
408     case 'courgette_unittests':
409     case 'crypto_unittests':
410     case 'googleurl_unittests':
411     case 'gpu_unittests':
412     case 'installer_util_unittests':
413     case 'interactive_ui_tests':
414     case 'ipc_tests':
415     case 'jingle_unittests':
416     case 'media_unittests':
417     case 'mini_installer_test':
418     case 'nacl_ui_tests':
419     case 'net_unittests':
420     case 'printing_unittests':
421     case 'remoting_unittests':
422     case 'safe_browsing_tests':
423     case 'sync_unit_tests':
424     case 'sync_integration_tests':
425     case 'test_shell_tests':
426     case 'ui_tests':
427     case 'unit_tests':
428         return G_TESTS_BUILDER_GROUP;
429         break;
430     default:
431         console.log('invalid testType parameter: ' + state.testType);
432     }
433 }
434
435 function builderMaster(builderName)
436 {
437     return BUILDER_TO_MASTER[builderName];
438 }
439
440 function isTipOfTreeWebKitBuilder()
441 {
442     return currentBuilderGroup().isToTWebKit;
443 }
444
445 var g_defaultBuilderName, g_builders, g_expectationsBuilder;
446 function initBuilders(state)
447 {
448     if (state.buildDir) {
449         // If buildDir is set, point to the results.json in the local tree. Useful for debugging changes to the python JSON generator.
450         g_defaultBuilderName = 'DUMMY_BUILDER_NAME';
451         g_builders = {'DUMMY_BUILDER_NAME': ''};
452         var loc = document.location.toString();
453         var offset = loc.indexOf('webkit/');
454     } else {
455         // FIXME: remove support for mapping from the master parameter to the group
456         // one once the waterfall starts to pass in the builder name instead.
457         if (state.master) {
458             state.group = LEGACY_BUILDER_MASTERS_TO_GROUPS[state.master];
459             window.location.hash = window.location.hash.replace('master=' + state.master, 'group=' + state.group);
460             delete state.master;
461         }
462         currentBuilderGroup(state).setup();
463     }
464 }
465 initBuilders(g_currentState);
466
467 // Append JSON script elements.
468 var g_resultsByBuilder = {};
469 var g_expectations;
470 function ADD_RESULTS(builds)
471 {
472     for (var builderName in builds) {
473         if (builderName == 'version')
474             continue;
475
476         // If a test suite stops being run on a given builder, we don't want to show it.
477         // Assume any builder without a run in two weeks for a given test suite isn't
478         // running that suite anymore.
479         // FIXME: Grab which bots run which tests directly from the buildbot JSON instead.
480         var lastRunSeconds = builds[builderName].secondsSinceEpoch[0];
481         if ((Date.now() / 1000) - lastRunSeconds > TWO_WEEKS_SECONDS)
482             continue;
483
484         g_resultsByBuilder[builderName] = builds[builderName];
485     }
486
487     handleResourceLoad();
488 }
489
490 function pathToBuilderResultsFile(builderName)
491 {
492     return TEST_RESULTS_SERVER + 'testfile?builder=' + builderName +
493             '&master=' + builderMaster(builderName).name +
494             '&testtype=' + g_currentState.testType + '&name=';
495 }
496
497 // FIXME: Make the dashboard understand different ports' expectations files.
498 var CHROMIUM_EXPECTATIONS_URL = 'http://svn.webkit.org/repository/webkit/trunk/LayoutTests/platform/chromium/test_expectations.txt';
499
500 function requestExpectationsFile()
501 {
502     request(CHROMIUM_EXPECTATIONS_URL, function(xhr) {
503         g_waitingOnExpectations = false;
504         g_expectations = xhr.responseText;
505         handleResourceLoad();
506     },
507     function() {
508         console.error('Could not load expectations file from ' + CHROMIUM_EXPECTATIONS_URL);
509     });
510 }
511
512 var g_waitingOnExpectations = isLayoutTestResults() && !isTreeMap();
513
514 function isTreeMap()
515 {
516     return endsWith(window.location.pathname, 'treemap.html');
517 }
518
519 function appendJSONScriptElementFor(builderName)
520 {
521     var resultsFilename;
522     if (isTreeMap())
523         resultsFilename = 'times_ms.json';
524     else if (g_currentState.showAllRuns)
525         resultsFilename = 'results.json';
526     else
527         resultsFilename = 'results-small.json';
528
529     appendScript(pathToBuilderResultsFile(builderName) + resultsFilename,
530             partial(handleResourceLoadError, builderName),
531             partial(handleScriptLoaded, builderName));
532 }
533
534 function appendJSONScriptElements()
535 {
536     clearErrors();
537
538     if (isTreeMap())
539         return;
540
541     parseParameters();
542
543     if (g_currentState.useTestData) {
544         appendScript('flakiness_dashboard_tests.js');
545         return;
546     }
547
548     for (var builderName in g_builders)
549         appendJSONScriptElementFor(builderName);
550
551     if (g_waitingOnExpectations)
552         requestExpectationsFile();
553 }
554
555 var g_hasDoneInitialPageGeneration = false;
556 // String of error messages to display to the user.
557 var g_errorMessages = '';
558
559 function handleResourceLoad()
560 {
561     // In case we load a results.json that's not in the list of builders,
562     // make sure to only call handleLocationChange once from the resource loads.
563     if (!g_hasDoneInitialPageGeneration)
564         handleLocationChange();
565 }
566
567 function handleScriptLoaded(builderName, script)
568 {
569     // We need this work-around for webkit.org/b/50589.
570     if (!g_resultsByBuilder[builderName]) {
571         var error = new Error("Builder data was empty");
572         error.target = script;
573         handleResourceLoadError(builderName, error);
574     }
575 }
576
577 // Handle resource loading errors - 404s, 500s, etc.    Recover from the error to
578 // still show as much data as possible, but show some UI about the failure, and
579 // do not try using this resource again until user refreshes.
580 //
581 // @param {string} builderName Name of builder that the resource failed for.
582 // @param {Event} e The error event.
583 function handleResourceLoadError(builderName, e)
584 {
585     var error = e.target.src + ' failed to load for ' + builderName + '.';
586
587     if (isLayoutTestResults()) {
588         console.error(error);
589         addError(error);
590     } else {
591         // Avoid to show error/warning messages for non-layout tests. We may be
592         // checking the builders that are not running the tests.
593         console.info('info:' + error);
594     }
595
596     // Remove this builder from builders, so we don't try to use the
597     // data that isn't there.
598     delete g_builders[builderName];
599
600     // Change the default builder name if it has been deleted.
601     if (g_defaultBuilderName == builderName) {
602         g_defaultBuilderName = null;
603         for (var availableBuilderName in g_builders) {
604             g_defaultBuilderName = availableBuilderName;
605             g_defaultStateValues.builder = availableBuilderName;
606             break;
607         }
608         if (!g_defaultBuilderName) {
609             var error = 'No tests results found for ' + g_currentState.testType + '. Reload the page to try fetching it again.';
610             console.error(error);
611             addError(error);
612         }
613     }
614
615     // Proceed as if the resource had loaded.
616     handleResourceLoad();
617 }
618
619
620 // Record a new error message.
621 // @param {string} errorMsg The message to show to the user.
622 function addError(errorMsg)
623 {
624     g_errorMessages += errorMsg + '<br />';
625 }
626
627 // Clear out error and warning messages.
628 function clearErrors()
629 {
630     g_errorMessages = '';
631 }
632
633 // If there are errors, show big and red UI for errors so as to be noticed.
634 function showErrors()
635 {
636     var errors = $('errors');
637
638     if (!g_errorMessages) {
639         if (errors)
640             errors.parentNode.removeChild(errors);
641         return;
642     }
643
644     if (!errors) {
645         errors = document.createElement('H2');
646         errors.style.color = 'red';
647         errors.id = 'errors';
648         document.body.appendChild(errors);
649     }
650
651     errors.innerHTML = g_errorMessages;
652 }
653
654 // @return {boolean} Whether the json files have all completed loading.
655 function haveJsonFilesLoaded()
656 {
657     if (g_waitingOnExpectations)
658         return false;
659
660     if (isTreeMap())
661         return true;
662
663     for (var builder in g_builders) {
664         if (!g_resultsByBuilder[builder])
665             return false;
666     }
667     return true;
668 }
669
670 function handleLocationChange()
671 {
672     if(!haveJsonFilesLoaded())
673         return;
674
675     g_hasDoneInitialPageGeneration = true;
676
677     var params = parseParameters();
678     var shouldGeneratePage = true;
679     if (Object.keys(params).length)
680         shouldGeneratePage = handleQueryParameterChange(params);
681
682     var newHash = permaLinkURLHash();
683     var winHash = window.location.hash || "#";
684     // Make sure the location is the same as the state we are using internally.
685     // These get out of sync if processQueryParamChange changed state.
686     if (newHash != winHash) {
687         // This will cause another hashchange, and when we loop
688         // back through here next time, we'll go through generatePage.
689         window.location.hash = newHash;
690     } else if (shouldGeneratePage)
691         generatePage();
692 }
693
694 window.onhashchange = handleLocationChange;
695
696 // Sets the page state. Takes varargs of key, value pairs.
697 function setQueryParameter(var_args)
698 {
699     var state = Object.create(g_currentState);
700     for (var i = 0; i < arguments.length; i += 2) {
701         var key = arguments[i];
702         state[key] = arguments[i + 1];
703     }
704     // Note: We use window.location.hash rather that window.location.replace
705     // because of bugs in Chrome where extra entries were getting created
706     // when back button was pressed and full page navigation was occuring.
707     // FIXME: file those bugs.
708     window.location.hash = permaLinkURLHash(state);
709 }
710
711 function permaLinkURLHash(opt_state)
712 {
713     var state = opt_state || g_currentState;
714     return '#' + joinParameters(state);
715 }
716
717 function joinParameters(stateObject)
718 {
719     var state = [];
720     for (var key in stateObject) {
721         var value = stateObject[key];
722         if (value != defaultValue(key))
723             state.push(key + '=' + encodeURIComponent(value));
724     }
725     return state.join('&');
726 }
727
728 function logTime(msg, startTime)
729 {
730     console.log(msg + ': ' + (Date.now() - startTime));
731 }
732
733 function hidePopup()
734 {
735     var popup = $('popup');
736     if (popup)
737         popup.parentNode.removeChild(popup);
738 }
739
740 function showPopup(e, html)
741 {
742     var popup = $('popup');
743     if (!popup) {
744         popup = document.createElement('div');
745         popup.id = 'popup';
746         document.body.appendChild(popup);
747     }
748
749     // Set html first so that we can get accurate size metrics on the popup.
750     popup.innerHTML = html;
751
752     var targetRect = e.target.getBoundingClientRect();
753
754     var x = Math.min(targetRect.left - 10, document.documentElement.clientWidth - popup.offsetWidth);
755     x = Math.max(0, x);
756     popup.style.left = x + document.body.scrollLeft + 'px';
757
758     var y = targetRect.top + targetRect.height;
759     if (y + popup.offsetHeight > document.documentElement.clientHeight)
760         y = targetRect.top - popup.offsetHeight;
761     y = Math.max(0, y);
762     popup.style.top = y + document.body.scrollTop + 'px';
763 }
764
765 // Create a new function with some of its arguements
766 // pre-filled.
767 // Taken from goog.partial in the Closure library.
768 // @param {Function} fn A function to partially apply.
769 // @param {...*} var_args Additional arguments that are partially
770 //         applied to fn.
771 // @return {!Function} A partially-applied form of the function bind() was
772 //         invoked as a method of.
773 function partial(fn, var_args)
774 {
775     var args = Array.prototype.slice.call(arguments, 1);
776     return function() {
777         // Prepend the bound arguments to the current arguments.
778         var newArgs = Array.prototype.slice.call(arguments);
779         newArgs.unshift.apply(newArgs, args);
780         return fn.apply(this, newArgs);
781     };
782 };
783
784 // Returns the appropriate expectatiosn map for the current testType.
785 function expectationsMap()
786 {
787     return isLayoutTestResults() ? LAYOUT_TEST_EXPECTATIONS_MAP_ : GTEST_EXPECTATIONS_MAP_;
788 }
789
790 function toggleQueryParameter(param)
791 {
792     setQueryParameter(param, !g_currentState[param]);
793 }
794
795 function checkboxHTML(queryParameter, label, isChecked, opt_extraJavaScript)
796 {
797     var js = opt_extraJavaScript || '';
798     return '<label style="padding-left: 2em">' +
799         '<input type="checkbox" onchange="toggleQueryParameter(\'' + queryParameter + '\');' + js + '" ' +
800             (isChecked ? 'checked' : '') + '>' + label +
801         '</label> ';
802 }
803
804 function selectHTML(label, queryParameter, options)
805 {
806     var html = '<label style="padding-left: 2em">' + label + ': ' +
807         '<select onchange="setQueryParameter(\'' + queryParameter + '\', this[this.selectedIndex].value)">';
808
809     for (var i = 0; i < options.length; i++) {
810         var value = options[i];
811         html += '<option value="' + value + '" ' +
812             (g_currentState[queryParameter] == value ? 'selected' : '') +
813             '>' + value + '</option>'
814     }
815     html += '</select></label> ';
816     return html;
817 }
818
819 // Returns the HTML for the select element to switch to different testTypes.
820 function htmlForTestTypeSwitcher(opt_noBuilderMenu, opt_extraHtml, opt_includeNoneBuilder)
821 {
822     var html = '<div style="border-bottom:1px dashed">';
823     html += '' +
824         htmlForDashboardLink('Stats', 'aggregate_results.html') +
825         htmlForDashboardLink('Timeline', 'timeline_explorer.html') +
826         htmlForDashboardLink('Results', 'flakiness_dashboard.html') +
827         htmlForDashboardLink('Treemap', 'treemap.html');
828
829     html += selectHTML('Test type', 'testType', TEST_TYPES);
830
831     if (!opt_noBuilderMenu) {
832         var buildersForMenu = Object.keys(g_builders);
833         if (opt_includeNoneBuilder)
834             buildersForMenu.unshift('--------------');
835         html += selectHTML('Builder', 'builder', buildersForMenu);
836     }
837
838     if (isLayoutTestResults())
839         html += selectHTML('Group', 'group', Object.keys(LAYOUT_TESTS_BUILDER_GROUPS));
840
841     if (!isTreeMap())
842         html += checkboxHTML('showAllRuns', 'Show all runs', g_currentState.showAllRuns);
843
844     if (opt_extraHtml)
845         html += opt_extraHtml;
846     return html + '</div>';
847 }
848
849 function selectBuilder(builder)
850 {
851     setQueryParameter('builder', builder);
852 }
853
854 function loadDashboard(fileName)
855 {
856     var pathName = window.location.pathname;
857     pathName = pathName.substring(0, pathName.lastIndexOf('/') + 1);
858     window.location = pathName + fileName + window.location.hash;
859 }
860
861 function htmlForTopLink(html, onClick, isSelected)
862 {
863     var cssText = isSelected ? 'font-weight: bold;' : 'color:blue;text-decoration:underline;cursor:pointer;';
864     cssText += 'margin: 0 5px;';
865     return '<span style="' + cssText + '" onclick="' + onClick + '">' + html + '</span>';
866 }
867
868 function htmlForDashboardLink(html, fileName)
869 {
870     var pathName = window.location.pathname;
871     var currentFileName = pathName.substring(pathName.lastIndexOf('/') + 1);
872     var isSelected = currentFileName == fileName;
873     var onClick = 'loadDashboard(\'' + fileName + '\')';
874     return htmlForTopLink(html, onClick, isSelected);
875 }
876
877 function revisionLink(results, index, key, singleUrlTemplate, rangeUrlTemplate)
878 {
879     var currentRevision = parseInt(results[key][index], 10);
880     var previousRevision = parseInt(results[key][index + 1], 10);
881
882     function singleUrl()
883     {
884         return singleUrlTemplate.replace('<rev>', currentRevision);
885     }
886
887     function rangeUrl()
888     {
889         return rangeUrlTemplate.replace('<rev1>', currentRevision).replace('<rev2>', previousRevision + 1);
890     }
891
892     if (currentRevision == previousRevision)
893         return 'At <a href="' + singleUrl() + '">r' + currentRevision    + '</a>';
894     else if (currentRevision - previousRevision == 1)
895         return '<a href="' + singleUrl() + '">r' + currentRevision    + '</a>';
896     else
897         return '<a href="' + rangeUrl() + '">r' + (previousRevision + 1) + ' to r' + currentRevision + '</a>';
898 }
899
900 function chromiumRevisionLink(results, index)
901 {
902     return revisionLink(
903         results,
904         index,
905         CHROME_REVISIONS_KEY,
906         'http://src.chromium.org/viewvc/chrome?view=rev&revision=<rev>',
907         'http://build.chromium.org/f/chromium/perf/dashboard/ui/changelog.html?url=/trunk/src&range=<rev2>:<rev1>&mode=html');
908 }
909
910 function webKitRevisionLink(results, index)
911 {
912     return revisionLink(
913         results,
914         index,
915         WEBKIT_REVISIONS_KEY,
916         'http://trac.webkit.org/changeset/<rev>',
917         'http://trac.webkit.org/log/trunk/?rev=<rev1>&stop_rev=<rev2>&limit=100&verbose=on');
918 }
919
920 // "Decompresses" the RLE-encoding of test results so that we can query it
921 // by build index and test name.
922 //
923 // @param {Object} results results for the current builder
924 // @return Object with these properties:
925 //     - testNames: array mapping test index to test names.
926 //     - resultsByBuild: array of builds, for each build a (sparse) array of test results by test index.
927 //     - flakyTests: array with the boolean value true at test indices that are considered flaky (more than one single-build failure).
928 //     - flakyDeltasByBuild: array of builds, for each build a count of flaky test results by expectation, as well as a total.
929 function decompressResults(builderResults)
930 {
931     var builderTestResults = builderResults[TESTS_KEY];
932     var buildCount = builderResults[FIXABLE_COUNTS_KEY].length;
933     var resultsByBuild = new Array(buildCount);
934     var flakyDeltasByBuild = new Array(buildCount);
935
936     // Pre-sizing the test result arrays for each build saves us ~250ms
937     var testCount = 0;
938     for (var testName in builderTestResults)
939         testCount++;
940     for (var i = 0; i < buildCount; i++) {
941         resultsByBuild[i] = new Array(testCount);
942         resultsByBuild[i][testCount - 1] = undefined;
943         flakyDeltasByBuild[i] = {};
944     }
945
946     // Using indices instead of the full test names for each build saves us
947     // ~1500ms
948     var testIndex = 0;
949     var testNames = new Array(testCount);
950     var flakyTests = new Array(testCount);
951
952     // Decompress and "invert" test results (by build instead of by test) and
953     // determine which are flaky.
954     for (var testName in builderTestResults) {
955         var oneBuildFailureCount = 0;
956
957         testNames[testIndex] = testName;
958         var testResults = builderTestResults[testName].results;
959         for (var i = 0, rleResult, currentBuildIndex = 0; (rleResult = testResults[i]) && currentBuildIndex < buildCount; i++) {
960             var count = rleResult[RLE.LENGTH];
961             var value = rleResult[RLE.VALUE];
962
963             if (count == 1 && value in FAILURE_EXPECTATIONS_)
964                 oneBuildFailureCount++;
965
966             for (var j = 0; j < count; j++) {
967                 resultsByBuild[currentBuildIndex++][testIndex] = value;
968                 if (currentBuildIndex == buildCount)
969                     break;
970             }
971         }
972
973         if (oneBuildFailureCount > 2)
974             flakyTests[testIndex] = true;
975
976         testIndex++;
977     }
978
979     // Now that we know which tests are flaky, count the test results that are
980     // from flaky tests for each build.
981     testIndex = 0;
982     for (var testName in builderTestResults) {
983         if (!flakyTests[testIndex++])
984             continue;
985
986         var testResults = builderTestResults[testName].results;
987         for (var i = 0, rleResult, currentBuildIndex = 0; (rleResult = testResults[i]) && currentBuildIndex < buildCount; i++) {
988             var count = rleResult[RLE.LENGTH];
989             var value = rleResult[RLE.VALUE];
990
991             for (var j = 0; j < count; j++) {
992                 var buildTestResults = flakyDeltasByBuild[currentBuildIndex++];
993                 function addFlakyDelta(key)
994                 {
995                     if (!(key in buildTestResults))
996                         buildTestResults[key] = 0;
997                     buildTestResults[key]++;
998                 }
999                 addFlakyDelta(value);
1000                 if (value != 'P' && value != 'N')
1001                     addFlakyDelta('total');
1002                 if (currentBuildIndex == buildCount)
1003                     break;
1004             }
1005         }
1006     }
1007
1008     return {
1009         testNames: testNames,
1010         resultsByBuild: resultsByBuild,
1011         flakyTests: flakyTests,
1012         flakyDeltasByBuild: flakyDeltasByBuild
1013     };
1014 }
1015
1016
1017 appendJSONScriptElements();
1018
1019 document.addEventListener('mousedown', function(e) {
1020     // Clear the open popup, unless the click was inside the popup.
1021     var popup = $('popup');
1022     if (popup && e.target != popup && !(popup.compareDocumentPosition(e.target) & 16))
1023         hidePopup();
1024 }, false);
1025
1026 window.addEventListener('load', function() {
1027     // This doesn't seem totally accurate as there is a race between
1028     // onload firing and the last script tag being executed.
1029     logTime('Time to load JS', g_pageLoadStartTime);
1030 }, false);