1 // Copyright (C) 2011 Google Inc. All rights reserved.
3 // Redistribution and use in source and binary forms, with or without
4 // modification, are permitted provided that the following conditions are
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
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.
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.
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.
33 // The calling page is expected to implement the following "abstract"
35 var g_pageLoadStartTime = Date.now();
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() {}
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.
44 // @return {boolean} Whether the key what inserted into the g_currentState.
45 function handleValidHashParameter(key, value)
50 // Default hash parameters for the page. The page should override this to create
52 var g_defaultStateValues = {};
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)
64 //////////////////////////////////////////////////////////////////////////////
66 //////////////////////////////////////////////////////////////////////////////
67 var GTEST_EXPECTATIONS_MAP_ = {
75 var LAYOUT_TEST_EXPECTATIONS_MAP_ = {
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.
89 var FAILURE_EXPECTATIONS_ = {
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;
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'];
125 var RELOAD_REQUIRING_PARAMETERS = ['showAllRuns', 'group', 'testType'];
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.
134 var TEST_RESULTS_SERVER = 'http://test-results.appspot.com/';
136 function isFailingResult(value)
138 return 'FSTOCIZ'.indexOf(value) != -1;
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.
146 // @return {boolean} Whether the key what inserted into the g_currentState.
147 function handleValidHashParameterWrapper(key, value)
151 validateParameter(g_currentState, key, value,
152 function() { return TEST_TYPES.indexOf(value) != -1; });
156 validateParameter(g_currentState, key, value,
157 function() { return value in LAYOUT_TESTS_BUILDER_GROUPS; });
160 // FIXME: remove support for this parameter once the waterfall starts to
161 // pass in the builder name instead.
163 validateParameter(g_currentState, key, value,
164 function() { return value in LEGACY_BUILDER_MASTERS_TO_GROUPS; });
168 validateParameter(g_currentState, key, value,
169 function() { return value in g_builders; });
174 g_currentState[key] = value == 'true';
178 g_currentState['testType'] = 'layout-test-results';
179 if (value === 'Debug' || value == 'Release') {
180 g_currentState[key] = value;
186 return handleValidHashParameter(key, value);
190 var g_defaultCrossDashboardStateValues = {
191 group: '@ToT - chromium.org',
193 testType: 'layout-tests',
197 // Generic utility functions.
200 return document.getElementById(id);
203 function stringContains(a, b)
205 return a.indexOf(b) != -1;
208 function caseInsensitiveContains(a, b)
210 return a.match(new RegExp(b, 'i'));
213 function startsWith(a, b)
215 return a.indexOf(b) == 0;
218 function endsWith(a, b)
220 return a.lastIndexOf(b) == a.length - b.length;
223 function isValidName(str)
225 return str.match(/[A-Za-z0-9\-\_,]/);
228 function trimString(str)
230 return str.replace(/^\s+|\s+$/g, '');
233 function collapseWhitespace(str)
235 return str.replace(/\s+/g, ' ');
238 function request(url, success, error, opt_isBinaryData)
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)
256 function validateParameter(state, key, value, validateFn)
261 console.log(key + ' value is not valid: ' + value);
264 function parseParameters(parameterStr)
266 g_oldState = g_currentState;
269 var hash = window.location.hash;
270 var paramsList = hash ? hash.substring(1).split('&') : [];
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]);
280 paramsMap[thisParam[0]] = decodeURIComponent(thisParam[1]);
283 function parseParam(key)
285 var value = paramsMap[key];
286 if (!handleValidHashParameterWrapper(key, value))
287 invalidKeys.push(key + '=' + value);
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')
295 if ('builder' in paramsMap) {
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);
304 parseParam('builder');
307 if (invalidKeys.length)
308 console.log("Invalid query parameters: " + invalidKeys.join(','));
310 var diffState = diffStates();
312 fillMissingValues(g_currentState, g_defaultCrossDashboardStateValues);
313 fillMissingValues(g_currentState, g_defaultStateValues);
315 // Some parameters require loading different JSON files when the value changes. Do a reload.
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();
326 // Find the difference of g_currentState with g_oldState.
327 // @return {Object} key:values of the new or changed params.
328 function diffStates()
330 // If there is no old state, everything in the current state is new.
332 return g_currentState;
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;
342 return changedParams;
345 function defaultValue(key)
347 if (key in g_defaultStateValues)
348 return g_defaultStateValues[key];
349 return g_defaultCrossDashboardStateValues[key];
352 function fillMissingValues(to, from)
354 for (var state in from) {
356 to[state] = from[state];
361 // @param {string} path Path to the script to load.
362 // @param {Function=} opt_onError Optional function to call if the script
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)
368 var script = document.createElement('script');
371 script.onreadystatechange = function() {
372 if (this.readyState == 'complete')
375 script.onload = function() { opt_onLoad(script); };
378 script.onerror = opt_onError;
379 document.getElementsByTagName('head')[0].appendChild(script);
382 // Permalinkable state of the page.
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.
389 // Parse cross-dashboard parameters before using them.
392 function isLayoutTestResults()
394 return g_currentState.testType == 'layout-tests';
397 function currentBuilderGroup(opt_state)
399 var state = opt_state || g_currentState;
400 switch (state.testType) {
402 return LAYOUT_TESTS_BUILDER_GROUPS[state.group]
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':
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':
428 return G_TESTS_BUILDER_GROUP;
431 console.log('invalid testType parameter: ' + state.testType);
435 function builderMaster(builderName)
437 return BUILDER_TO_MASTER[builderName];
440 function isTipOfTreeWebKitBuilder()
442 return currentBuilderGroup().isToTWebKit;
445 var g_defaultBuilderName, g_builders, g_expectationsBuilder;
446 function initBuilders(state)
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/');
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.
458 state.group = LEGACY_BUILDER_MASTERS_TO_GROUPS[state.master];
459 window.location.hash = window.location.hash.replace('master=' + state.master, 'group=' + state.group);
462 currentBuilderGroup(state).setup();
465 initBuilders(g_currentState);
467 // Append JSON script elements.
468 var g_resultsByBuilder = {};
470 function ADD_RESULTS(builds)
472 for (var builderName in builds) {
473 if (builderName == 'version')
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)
484 g_resultsByBuilder[builderName] = builds[builderName];
487 handleResourceLoad();
490 function pathToBuilderResultsFile(builderName)
492 return TEST_RESULTS_SERVER + 'testfile?builder=' + builderName +
493 '&master=' + builderMaster(builderName).name +
494 '&testtype=' + g_currentState.testType + '&name=';
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';
500 function requestExpectationsFile()
502 request(CHROMIUM_EXPECTATIONS_URL, function(xhr) {
503 g_waitingOnExpectations = false;
504 g_expectations = xhr.responseText;
505 handleResourceLoad();
508 console.error('Could not load expectations file from ' + CHROMIUM_EXPECTATIONS_URL);
512 var g_waitingOnExpectations = isLayoutTestResults() && !isTreeMap();
516 return endsWith(window.location.pathname, 'treemap.html');
519 function appendJSONScriptElementFor(builderName)
523 resultsFilename = 'times_ms.json';
524 else if (g_currentState.showAllRuns)
525 resultsFilename = 'results.json';
527 resultsFilename = 'results-small.json';
529 appendScript(pathToBuilderResultsFile(builderName) + resultsFilename,
530 partial(handleResourceLoadError, builderName),
531 partial(handleScriptLoaded, builderName));
534 function appendJSONScriptElements()
543 if (g_currentState.useTestData) {
544 appendScript('flakiness_dashboard_tests.js');
548 for (var builderName in g_builders)
549 appendJSONScriptElementFor(builderName);
551 if (g_waitingOnExpectations)
552 requestExpectationsFile();
555 var g_hasDoneInitialPageGeneration = false;
556 // String of error messages to display to the user.
557 var g_errorMessages = '';
559 function handleResourceLoad()
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();
567 function handleScriptLoaded(builderName, script)
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);
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.
581 // @param {string} builderName Name of builder that the resource failed for.
582 // @param {Event} e The error event.
583 function handleResourceLoadError(builderName, e)
585 var error = e.target.src + ' failed to load for ' + builderName + '.';
587 if (isLayoutTestResults()) {
588 console.error(error);
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);
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];
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;
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);
615 // Proceed as if the resource had loaded.
616 handleResourceLoad();
620 // Record a new error message.
621 // @param {string} errorMsg The message to show to the user.
622 function addError(errorMsg)
624 g_errorMessages += errorMsg + '<br />';
627 // Clear out error and warning messages.
628 function clearErrors()
630 g_errorMessages = '';
633 // If there are errors, show big and red UI for errors so as to be noticed.
634 function showErrors()
636 var errors = $('errors');
638 if (!g_errorMessages) {
640 errors.parentNode.removeChild(errors);
645 errors = document.createElement('H2');
646 errors.style.color = 'red';
647 errors.id = 'errors';
648 document.body.appendChild(errors);
651 errors.innerHTML = g_errorMessages;
654 // @return {boolean} Whether the json files have all completed loading.
655 function haveJsonFilesLoaded()
657 if (g_waitingOnExpectations)
663 for (var builder in g_builders) {
664 if (!g_resultsByBuilder[builder])
670 function handleLocationChange()
672 if(!haveJsonFilesLoaded())
675 g_hasDoneInitialPageGeneration = true;
677 var params = parseParameters();
678 var shouldGeneratePage = true;
679 if (Object.keys(params).length)
680 shouldGeneratePage = handleQueryParameterChange(params);
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)
694 window.onhashchange = handleLocationChange;
696 // Sets the page state. Takes varargs of key, value pairs.
697 function setQueryParameter(var_args)
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];
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);
711 function permaLinkURLHash(opt_state)
713 var state = opt_state || g_currentState;
714 return '#' + joinParameters(state);
717 function joinParameters(stateObject)
720 for (var key in stateObject) {
721 var value = stateObject[key];
722 if (value != defaultValue(key))
723 state.push(key + '=' + encodeURIComponent(value));
725 return state.join('&');
728 function logTime(msg, startTime)
730 console.log(msg + ': ' + (Date.now() - startTime));
735 var popup = $('popup');
737 popup.parentNode.removeChild(popup);
740 function showPopup(e, html)
742 var popup = $('popup');
744 popup = document.createElement('div');
746 document.body.appendChild(popup);
749 // Set html first so that we can get accurate size metrics on the popup.
750 popup.innerHTML = html;
752 var targetRect = e.target.getBoundingClientRect();
754 var x = Math.min(targetRect.left - 10, document.documentElement.clientWidth - popup.offsetWidth);
756 popup.style.left = x + document.body.scrollLeft + 'px';
758 var y = targetRect.top + targetRect.height;
759 if (y + popup.offsetHeight > document.documentElement.clientHeight)
760 y = targetRect.top - popup.offsetHeight;
762 popup.style.top = y + document.body.scrollTop + 'px';
765 // Create a new function with some of its arguements
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
771 // @return {!Function} A partially-applied form of the function bind() was
772 // invoked as a method of.
773 function partial(fn, var_args)
775 var args = Array.prototype.slice.call(arguments, 1);
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);
784 // Returns the appropriate expectatiosn map for the current testType.
785 function expectationsMap()
787 return isLayoutTestResults() ? LAYOUT_TEST_EXPECTATIONS_MAP_ : GTEST_EXPECTATIONS_MAP_;
790 function toggleQueryParameter(param)
792 setQueryParameter(param, !g_currentState[param]);
795 function checkboxHTML(queryParameter, label, isChecked, opt_extraJavaScript)
797 var js = opt_extraJavaScript || '';
798 return '<label style="padding-left: 2em">' +
799 '<input type="checkbox" onchange="toggleQueryParameter(\'' + queryParameter + '\');' + js + '" ' +
800 (isChecked ? 'checked' : '') + '>' + label +
804 function selectHTML(label, queryParameter, options)
806 var html = '<label style="padding-left: 2em">' + label + ': ' +
807 '<select onchange="setQueryParameter(\'' + queryParameter + '\', this[this.selectedIndex].value)">';
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>'
815 html += '</select></label> ';
819 // Returns the HTML for the select element to switch to different testTypes.
820 function htmlForTestTypeSwitcher(opt_noBuilderMenu, opt_extraHtml, opt_includeNoneBuilder)
822 var html = '<div style="border-bottom:1px dashed">';
824 htmlForDashboardLink('Stats', 'aggregate_results.html') +
825 htmlForDashboardLink('Timeline', 'timeline_explorer.html') +
826 htmlForDashboardLink('Results', 'flakiness_dashboard.html') +
827 htmlForDashboardLink('Treemap', 'treemap.html');
829 html += selectHTML('Test type', 'testType', TEST_TYPES);
831 if (!opt_noBuilderMenu) {
832 var buildersForMenu = Object.keys(g_builders);
833 if (opt_includeNoneBuilder)
834 buildersForMenu.unshift('--------------');
835 html += selectHTML('Builder', 'builder', buildersForMenu);
838 if (isLayoutTestResults())
839 html += selectHTML('Group', 'group', Object.keys(LAYOUT_TESTS_BUILDER_GROUPS));
842 html += checkboxHTML('showAllRuns', 'Show all runs', g_currentState.showAllRuns);
845 html += opt_extraHtml;
846 return html + '</div>';
849 function selectBuilder(builder)
851 setQueryParameter('builder', builder);
854 function loadDashboard(fileName)
856 var pathName = window.location.pathname;
857 pathName = pathName.substring(0, pathName.lastIndexOf('/') + 1);
858 window.location = pathName + fileName + window.location.hash;
861 function htmlForTopLink(html, onClick, isSelected)
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>';
868 function htmlForDashboardLink(html, fileName)
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);
877 function revisionLink(results, index, key, singleUrlTemplate, rangeUrlTemplate)
879 var currentRevision = parseInt(results[key][index], 10);
880 var previousRevision = parseInt(results[key][index + 1], 10);
884 return singleUrlTemplate.replace('<rev>', currentRevision);
889 return rangeUrlTemplate.replace('<rev1>', currentRevision).replace('<rev2>', previousRevision + 1);
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>';
897 return '<a href="' + rangeUrl() + '">r' + (previousRevision + 1) + ' to r' + currentRevision + '</a>';
900 function chromiumRevisionLink(results, 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');
910 function webKitRevisionLink(results, 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');
920 // "Decompresses" the RLE-encoding of test results so that we can query it
921 // by build index and test name.
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)
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);
936 // Pre-sizing the test result arrays for each build saves us ~250ms
938 for (var testName in builderTestResults)
940 for (var i = 0; i < buildCount; i++) {
941 resultsByBuild[i] = new Array(testCount);
942 resultsByBuild[i][testCount - 1] = undefined;
943 flakyDeltasByBuild[i] = {};
946 // Using indices instead of the full test names for each build saves us
949 var testNames = new Array(testCount);
950 var flakyTests = new Array(testCount);
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;
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];
963 if (count == 1 && value in FAILURE_EXPECTATIONS_)
964 oneBuildFailureCount++;
966 for (var j = 0; j < count; j++) {
967 resultsByBuild[currentBuildIndex++][testIndex] = value;
968 if (currentBuildIndex == buildCount)
973 if (oneBuildFailureCount > 2)
974 flakyTests[testIndex] = true;
979 // Now that we know which tests are flaky, count the test results that are
980 // from flaky tests for each build.
982 for (var testName in builderTestResults) {
983 if (!flakyTests[testIndex++])
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];
991 for (var j = 0; j < count; j++) {
992 var buildTestResults = flakyDeltasByBuild[currentBuildIndex++];
993 function addFlakyDelta(key)
995 if (!(key in buildTestResults))
996 buildTestResults[key] = 0;
997 buildTestResults[key]++;
999 addFlakyDelta(value);
1000 if (value != 'P' && value != 'N')
1001 addFlakyDelta('total');
1002 if (currentBuildIndex == buildCount)
1009 testNames: testNames,
1010 resultsByBuild: resultsByBuild,
1011 flakyTests: flakyTests,
1012 flakyDeltasByBuild: flakyDeltasByBuild
1017 appendJSONScriptElements();
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))
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);