blob: 6ec0c8e4292cf578689d19ec0cd0267e8b6d7ceb [file] [log] [blame]
// Copyright (C) 2012 Google Inc. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// @fileoverview Base JS file for pages that want to parse the results JSON
// from the testing bots. This deals with generic utility functions, visible
// history, popups and appending the script elements for the JSON files.
//
// The calling page is expected to implement the following "abstract"
// functions/objects:
var g_pageLoadStartTime = Date.now();
var g_resourceLoader;
// Generates the contents of the dashboard. The page should override this with
// a function that generates the page assuming all resources have loaded.
function generatePage() {}
// Takes a key and a value and sets the g_currentState[key] = value iff key is
// a valid hash parameter and the value is a valid value for that key.
//
// @return {boolean} Whether the key what inserted into the g_currentState.
function handleValidHashParameter(key, value)
{
return false;
}
// Default hash parameters for the page. The page should override this to create
// default states.
var g_defaultDashboardSpecificStateValues = {};
// The page should override this to modify page state due to
// changing query parameters.
// @param {Object} params New or modified query params as key: value.
// @return {boolean} Whether changing this parameter should cause generatePage to be called.
function handleQueryParameterChange(params)
{
return true;
}
//////////////////////////////////////////////////////////////////////////////
// CONSTANTS
//////////////////////////////////////////////////////////////////////////////
var GTEST_EXPECTATIONS_MAP_ = {
'P': 'PASS',
'F': 'FAIL',
'N': 'NO DATA',
'X': 'SKIPPED'
};
var LAYOUT_TEST_EXPECTATIONS_MAP_ = {
'P': 'PASS',
'N': 'NO DATA',
'X': 'SKIP',
'T': 'TIMEOUT',
'F': 'TEXT',
'C': 'CRASH',
'I': 'IMAGE',
'Z': 'IMAGE+TEXT',
// We used to glob a bunch of expectations into "O" as OTHER. Expectations
// are more precise now though and it just means MISSING.
'O': 'MISSING'
};
var FAILURE_EXPECTATIONS_ = {
'T': 1,
'F': 1,
'C': 1,
'I': 1,
'Z': 1
};
// Keys in the JSON files.
var WONTFIX_COUNTS_KEY = 'wontfixCounts';
var FIXABLE_COUNTS_KEY = 'fixableCounts';
var DEFERRED_COUNTS_KEY = 'deferredCounts';
var WONTFIX_DESCRIPTION = 'Tests never to be fixed (WONTFIX)';
var FIXABLE_DESCRIPTION = 'All tests for this release';
var DEFERRED_DESCRIPTION = 'All deferred tests (DEFER)';
var FIXABLE_COUNT_KEY = 'fixableCount';
var ALL_FIXABLE_COUNT_KEY = 'allFixableCount';
var CHROME_REVISIONS_KEY = 'chromeRevision';
var WEBKIT_REVISIONS_KEY = 'webkitRevision';
var TIMESTAMPS_KEY = 'secondsSinceEpoch';
var BUILD_NUMBERS_KEY = 'buildNumbers';
var TESTS_KEY = 'tests';
var ONE_DAY_SECONDS = 60 * 60 * 24;
var ONE_WEEK_SECONDS = ONE_DAY_SECONDS * 7;
// These should match the testtype uploaded to test-results.appspot.com.
// See http://test-results.appspot.com/testfile.
var TEST_TYPES = [
'base_unittests',
'browser_tests',
'cacheinvalidation_unittests',
'compositor_unittests',
'content_browsertests',
'content_unittests',
'courgette_unittests',
'crypto_unittests',
'googleurl_unittests',
'gfx_unittests',
'gl_tests',
'gpu_tests',
'gpu_unittests',
'installer_util_unittests',
'interactive_ui_tests',
'ipc_tests',
'jingle_unittests',
'layout-tests',
'media_unittests',
'mini_installer_test',
'net_unittests',
'printing_unittests',
'remoting_unittests',
'safe_browsing_tests',
'sql_unittests',
'sync_unit_tests',
'sync_integration_tests',
'test_shell_tests',
'ui_tests',
'unit_tests',
'views_unittests',
'webkit_unit_tests',
];
var RELOAD_REQUIRING_PARAMETERS = ['showAllRuns', 'group', 'testType'];
// Enum for indexing into the run-length encoded results in the JSON files.
// 0 is where the count is length is stored. 1 is the value.
var RLE = {
LENGTH: 0,
VALUE: 1
}
function isFailingResult(value)
{
return 'FSTOCIZ'.indexOf(value) != -1;
}
// Takes a key and a value and sets the g_currentState[key] = value iff key is
// a valid hash parameter and the value is a valid value for that key. Handles
// cross-dashboard parameters then falls back to calling
// handleValidHashParameter for dashboard-specific parameters.
//
// @return {boolean} Whether the key what inserted into the g_currentState.
function handleValidHashParameterWrapper(key, value)
{
switch(key) {
case 'testType':
validateParameter(g_crossDashboardState, key, value,
function() { return TEST_TYPES.indexOf(value) != -1; });
return true;
case 'group':
validateParameter(g_crossDashboardState, key, value,
function() {
return value in LAYOUT_TESTS_BUILDER_GROUPS ||
value in CHROMIUM_GPU_TESTS_BUILDER_GROUPS ||
value in CHROMIUM_GTESTS_BUILDER_GROUPS;
});
return true;
// FIXME: This should probably be stored on g_crossDashboardState like everything else in this function.
case 'builder':
validateParameter(g_currentState, key, value,
function() { return value in g_builders; });
return true;
case 'useTestData':
case 'showAllRuns':
g_crossDashboardState[key] = value == 'true';
return true;
case 'buildDir':
if (value === 'Debug' || value == 'Release') {
g_crossDashboardState['testType'] = 'layout-tests';
g_crossDashboardState[key] = value;
return true;
} else
return false;
default:
return handleValidHashParameter(key, value);
}
}
var g_defaultCrossDashboardStateValues = {
group: '@ToT - chromium.org',
showAllRuns: false,
testType: 'layout-tests',
buildDir: '',
useTestData: false,
}
// Generic utility functions.
function $(id)
{
return document.getElementById(id);
}
function stringContains(a, b)
{
return a.indexOf(b) != -1;
}
function caseInsensitiveContains(a, b)
{
return a.match(new RegExp(b, 'i'));
}
function startsWith(a, b)
{
return a.indexOf(b) == 0;
}
function endsWith(a, b)
{
return a.lastIndexOf(b) == a.length - b.length;
}
function isValidName(str)
{
return str.match(/[A-Za-z0-9\-\_,]/);
}
function trimString(str)
{
return str.replace(/^\s+|\s+$/g, '');
}
function collapseWhitespace(str)
{
return str.replace(/\s+/g, ' ');
}
function validateParameter(state, key, value, validateFn)
{
if (validateFn())
state[key] = value;
else
console.log(key + ' value is not valid: ' + value);
}
function queryHashAsMap()
{
var hash = window.location.hash;
var paramsList = hash ? hash.substring(1).split('&') : [];
var paramsMap = {};
var invalidKeys = [];
for (var i = 0; i < paramsList.length; i++) {
var thisParam = paramsList[i].split('=');
if (thisParam.length != 2) {
console.log('Invalid query parameter: ' + paramsList[i]);
continue;
}
paramsMap[thisParam[0]] = decodeURIComponent(thisParam[1]);
}
// FIXME: remove support for mapping from the master parameter to the group
// one once the waterfall starts to pass in the builder name instead.
if (paramsMap.master) {
paramsMap.group = LEGACY_BUILDER_MASTERS_TO_GROUPS[paramsMap.master];
if (!paramsMap.group)
console.log('ERROR: Unknown master name: ' + paramsMap.master);
window.location.hash = window.location.hash.replace('master=' + paramsMap.master, 'group=' + paramsMap.group);
delete paramsMap.master;
}
return paramsMap;
}
function parseParameter(parameters, key)
{
if (!(key in parameters))
return;
var value = parameters[key];
if (!handleValidHashParameterWrapper(key, value))
console.log("Invalid query parameter: " + key + '=' + value);
}
function parseCrossDashboardParameters()
{
g_crossDashboardState = {};
var parameters = queryHashAsMap();
for (parameterName in g_defaultCrossDashboardStateValues)
parseParameter(parameters, parameterName);
fillMissingValues(g_crossDashboardState, g_defaultCrossDashboardStateValues);
if (currentBuilderGroup() === undefined)
g_crossDashboardState.group = g_defaultCrossDashboardStateValues.group;
}
function parseDashboardSpecificParameters()
{
g_currentState = {};
var parameters = queryHashAsMap();
for (parameterName in g_defaultDashboardSpecificStateValues)
parseParameter(parameters, parameterName);
}
function parseParameters()
{
var oldCrossDashboardState = g_crossDashboardState;
var oldDashboardSpecificState = g_currentState;
parseCrossDashboardParameters();
parseDashboardSpecificParameters();
parseParameter(queryHashAsMap(), 'builder');
var crossDashboardDiffState = diffStates(oldCrossDashboardState, g_crossDashboardState);
var dashboardSpecificDiffState = diffStates(oldDashboardSpecificState, g_currentState);
fillMissingValues(g_currentState, g_defaultDashboardSpecificStateValues);
if (!g_crossDashboardState.useTestData)
fillMissingValues(g_currentState, {'builder': g_defaultBuilderName});
// FIXME: dashboard_base shouldn't know anything about specific dashboard specific keys.
if (dashboardSpecificDiffState.builder)
delete g_currentState.tests;
if (g_currentState.tests)
delete g_currentState.builder;
// Some parameters require loading different JSON files when the value changes. Do a reload.
if (Object.keys(oldCrossDashboardState).length) {
for (var key in g_crossDashboardState) {
if (oldCrossDashboardState[key] != g_crossDashboardState[key] && RELOAD_REQUIRING_PARAMETERS.indexOf(key) != -1)
window.location.reload();
}
}
return dashboardSpecificDiffState;
}
function diffStates(oldState, newState)
{
// If there is no old state, everything in the current state is new.
if (!oldState)
return newState;
var changedParams = {};
for (curKey in newState) {
var oldVal = oldState[curKey];
var newVal = newState[curKey];
// Add new keys or changed values.
if (!oldVal || oldVal != newVal)
changedParams[curKey] = newVal;
}
return changedParams;
}
function defaultValue(key)
{
if (key in g_defaultDashboardSpecificStateValues)
return g_defaultDashboardSpecificStateValues[key];
return g_defaultCrossDashboardStateValues[key];
}
function fillMissingValues(to, from)
{
for (var state in from) {
if (!(state in to))
to[state] = from[state];
}
}
// FIXME: Rename this to g_dashboardSpecificState;
var g_currentState = {};
var g_crossDashboardState = {};
parseCrossDashboardParameters();
function isLayoutTestResults()
{
return g_crossDashboardState.testType == 'layout-tests';
}
function isGPUTestResults()
{
return g_crossDashboardState.testType == 'gpu_tests';
}
function currentBuilderGroupCategory()
{
switch (g_crossDashboardState.testType) {
case 'gl_tests':
case 'gpu_tests':
return CHROMIUM_GPU_TESTS_BUILDER_GROUPS;
case 'layout-tests':
return LAYOUT_TESTS_BUILDER_GROUPS;
case 'test_shell_tests':
case 'webkit_unit_tests':
return TEST_SHELL_TESTS_BUILDER_GROUPS;
default:
return CHROMIUM_GTESTS_BUILDER_GROUPS;
}
}
function currentBuilderGroup()
{
return currentBuilderGroupCategory()[g_crossDashboardState.group]
}
function builderMaster(builderName)
{
return BUILDER_TO_MASTER[builderName];
}
function isTipOfTreeWebKitBuilder()
{
return currentBuilderGroup().isToTWebKit;
}
var g_defaultBuilderName, g_builders;
function initBuilders()
{
if (g_crossDashboardState.buildDir) {
// If buildDir is set, point to the results.json in the local tree. Useful for debugging changes to the python JSON generator.
g_defaultBuilderName = 'DUMMY_BUILDER_NAME';
g_builders = {'DUMMY_BUILDER_NAME': ''};
var loc = document.location.toString();
var offset = loc.indexOf('webkit/');
} else
currentBuilderGroup().setup();
}
var g_resultsByBuilder = {};
var g_expectationsByPlatform = {};
var g_staleBuilders = [];
var g_buildersThatFailedToLoad = [];
// TODO(aboxhall): figure out whether this is a performance bottleneck and
// change calling code to understand the trie structure instead if necessary.
function flattenTrie(trie, prefix)
{
var result = {};
for (var name in trie) {
var fullName = prefix ? prefix + "/" + name : name;
var data = trie[name];
if ("results" in data)
result[fullName] = data;
else {
var partialResult = flattenTrie(data, fullName);
for (var key in partialResult) {
result[key] = partialResult[key];
}
}
}
return result;
}
function isTreeMap()
{
return endsWith(window.location.pathname, 'treemap.html');
}
function isFlakinessDashboard()
{
return endsWith(window.location.pathname, 'flakiness_dashboard.html');
}
var g_hasDoneInitialPageGeneration = false;
// String of error messages to display to the user.
var g_errorMessages = '';
// Record a new error message.
// @param {string} errorMsg The message to show to the user.
function addError(errorMsg)
{
g_errorMessages += errorMsg + '<br>';
}
// Clear out error and warning messages.
function clearErrors()
{
g_errorMessages = '';
}
// If there are errors, show big and red UI for errors so as to be noticed.
function showErrors()
{
var errors = $('errors');
if (!g_errorMessages) {
if (errors)
errors.parentNode.removeChild(errors);
return;
}
if (!errors) {
errors = document.createElement('H2');
errors.style.color = 'red';
errors.id = 'errors';
document.body.appendChild(errors);
}
errors.innerHTML = g_errorMessages;
}
function addBuilderLoadErrors()
{
if (g_hasDoneInitialPageGeneration)
return;
if (g_buildersThatFailedToLoad.length)
addError('ERROR: Failed to get data from ' + g_buildersThatFailedToLoad.toString() + '.');
if (g_staleBuilders.length)
addError('ERROR: Data from ' + g_staleBuilders.toString() + ' is more than 1 day stale.');
}
function resourceLoadingComplete()
{
g_resourceLoader = null;
handleLocationChange();
}
function handleLocationChange()
{
if (g_resourceLoader)
return;
addBuilderLoadErrors();
g_hasDoneInitialPageGeneration = true;
var params = parseParameters();
var shouldGeneratePage = true;
if (Object.keys(params).length)
shouldGeneratePage = handleQueryParameterChange(params);
var newHash = permaLinkURLHash();
var winHash = window.location.hash || "#";
// Make sure the location is the same as the state we are using internally.
// These get out of sync if processQueryParamChange changed state.
if (newHash != winHash) {
// This will cause another hashchange, and when we loop
// back through here next time, we'll go through generatePage.
window.location.hash = newHash;
} else if (shouldGeneratePage)
generatePage();
}
window.onhashchange = handleLocationChange;
function combinedDashboardState()
{
var combinedState = Object.create(g_currentState);
for (var key in g_crossDashboardState)
combinedState[key] = g_crossDashboardState[key];
return combinedState;
}
// Sets the page state. Takes varargs of key, value pairs.
function setQueryParameter(var_args)
{
var state = combinedDashboardState();
for (var i = 0; i < arguments.length; i += 2) {
var key = arguments[i];
state[key] = arguments[i + 1];
}
// Note: We use window.location.hash rather that window.location.replace
// because of bugs in Chrome where extra entries were getting created
// when back button was pressed and full page navigation was occuring.
// FIXME: file those bugs.
window.location.hash = permaLinkURLHash(state);
}
function permaLinkURLHash(opt_state)
{
var state = opt_state || combinedDashboardState();
return '#' + joinParameters(state);
}
function joinParameters(stateObject)
{
var state = [];
for (var key in stateObject) {
var value = stateObject[key];
if (value != defaultValue(key))
state.push(key + '=' + encodeURIComponent(value));
}
return state.join('&');
}
function logTime(msg, startTime)
{
console.log(msg + ': ' + (Date.now() - startTime));
}
function hidePopup()
{
var popup = $('popup');
if (popup)
popup.parentNode.removeChild(popup);
}
function showPopup(target, html)
{
var popup = $('popup');
if (!popup) {
popup = document.createElement('div');
popup.id = 'popup';
document.body.appendChild(popup);
}
// Set html first so that we can get accurate size metrics on the popup.
popup.innerHTML = html;
var targetRect = target.getBoundingClientRect();
var x = Math.min(targetRect.left - 10, document.documentElement.clientWidth - popup.offsetWidth);
x = Math.max(0, x);
popup.style.left = x + document.body.scrollLeft + 'px';
var y = targetRect.top + targetRect.height;
if (y + popup.offsetHeight > document.documentElement.clientHeight)
y = targetRect.top - popup.offsetHeight;
y = Math.max(0, y);
popup.style.top = y + document.body.scrollTop + 'px';
}
// Create a new function with some of its arguements
// pre-filled.
// Taken from goog.partial in the Closure library.
// @param {Function} fn A function to partially apply.
// @param {...*} var_args Additional arguments that are partially
// applied to fn.
// @return {!Function} A partially-applied form of the function bind() was
// invoked as a method of.
function partial(fn, var_args)
{
var args = Array.prototype.slice.call(arguments, 1);
return function() {
// Prepend the bound arguments to the current arguments.
var newArgs = Array.prototype.slice.call(arguments);
newArgs.unshift.apply(newArgs, args);
return fn.apply(this, newArgs);
};
};
// Returns the appropriate expectatiosn map for the current testType.
function expectationsMap()
{
return isLayoutTestResults() ? LAYOUT_TEST_EXPECTATIONS_MAP_ : GTEST_EXPECTATIONS_MAP_;
}
function toggleQueryParameter(param)
{
setQueryParameter(param, !queryParameterValue(param));
}
function queryParameterValue(parameter)
{
return g_currentState[parameter] || g_crossDashboardState[parameter];
}
function checkboxHTML(queryParameter, label, isChecked, opt_extraJavaScript)
{
var js = opt_extraJavaScript || '';
return '<label style="padding-left: 2em">' +
'<input type="checkbox" onchange="toggleQueryParameter(\'' + queryParameter + '\');' + js + '" ' +
(isChecked ? 'checked' : '') + '>' + label +
'</label> ';
}
function selectHTML(label, queryParameter, options)
{
var html = '<label style="padding-left: 2em">' + label + ': ' +
'<select onchange="setQueryParameter(\'' + queryParameter + '\', this[this.selectedIndex].value)">';
for (var i = 0; i < options.length; i++) {
var value = options[i];
html += '<option value="' + value + '" ' +
(queryParameterValue(queryParameter) == value ? 'selected' : '') +
'>' + value + '</option>'
}
html += '</select></label> ';
return html;
}
// Returns the HTML for the select element to switch to different testTypes.
function htmlForTestTypeSwitcher(opt_noBuilderMenu, opt_extraHtml, opt_includeNoneBuilder)
{
var html = '<div style="border-bottom:1px dashed">';
html += '' +
htmlForDashboardLink('Stats', 'aggregate_results.html') +
htmlForDashboardLink('Timeline', 'timeline_explorer.html') +
htmlForDashboardLink('Results', 'flakiness_dashboard.html') +
htmlForDashboardLink('Treemap', 'treemap.html');
html += selectHTML('Test type', 'testType', TEST_TYPES);
if (!opt_noBuilderMenu) {
var buildersForMenu = Object.keys(g_builders);
if (opt_includeNoneBuilder)
buildersForMenu.unshift('--------------');
html += selectHTML('Builder', 'builder', buildersForMenu);
}
html += selectHTML('Group', 'group',
Object.keys(currentBuilderGroupCategory()));
if (!isTreeMap())
html += checkboxHTML('showAllRuns', 'Show all runs', g_crossDashboardState.showAllRuns);
if (opt_extraHtml)
html += opt_extraHtml;
return html + '</div>';
}
function selectBuilder(builder)
{
setQueryParameter('builder', builder);
}
function loadDashboard(fileName)
{
var pathName = window.location.pathname;
pathName = pathName.substring(0, pathName.lastIndexOf('/') + 1);
window.location = pathName + fileName + window.location.hash;
}
function htmlForTopLink(html, onClick, isSelected)
{
var cssText = isSelected ? 'font-weight: bold;' : 'color:blue;text-decoration:underline;cursor:pointer;';
cssText += 'margin: 0 5px;';
return '<span style="' + cssText + '" onclick="' + onClick + '">' + html + '</span>';
}
function htmlForDashboardLink(html, fileName)
{
var pathName = window.location.pathname;
var currentFileName = pathName.substring(pathName.lastIndexOf('/') + 1);
var isSelected = currentFileName == fileName;
var onClick = 'loadDashboard(\'' + fileName + '\')';
return htmlForTopLink(html, onClick, isSelected);
}
function revisionLink(results, index, key, singleUrlTemplate, rangeUrlTemplate)
{
var currentRevision = parseInt(results[key][index], 10);
var previousRevision = parseInt(results[key][index + 1], 10);
function singleUrl()
{
return singleUrlTemplate.replace('<rev>', currentRevision);
}
function rangeUrl()
{
return rangeUrlTemplate.replace('<rev1>', currentRevision).replace('<rev2>', previousRevision + 1);
}
if (currentRevision == previousRevision)
return 'At <a href="' + singleUrl() + '">r' + currentRevision + '</a>';
else if (currentRevision - previousRevision == 1)
return '<a href="' + singleUrl() + '">r' + currentRevision + '</a>';
else
return '<a href="' + rangeUrl() + '">r' + (previousRevision + 1) + ' to r' + currentRevision + '</a>';
}
function chromiumRevisionLink(results, index)
{
return revisionLink(
results,
index,
CHROME_REVISIONS_KEY,
'http://src.chromium.org/viewvc/chrome?view=rev&revision=<rev>',
'http://build.chromium.org/f/chromium/perf/dashboard/ui/changelog.html?url=/trunk/src&range=<rev2>:<rev1>&mode=html');
}
function webKitRevisionLink(results, index)
{
return revisionLink(
results,
index,
WEBKIT_REVISIONS_KEY,
'http://trac.webkit.org/changeset/<rev>',
'http://trac.webkit.org/log/trunk/?rev=<rev1>&stop_rev=<rev2>&limit=100&verbose=on');
}
// "Decompresses" the RLE-encoding of test results so that we can query it
// by build index and test name.
//
// @param {Object} results results for the current builder
// @return Object with these properties:
// - testNames: array mapping test index to test names.
// - resultsByBuild: array of builds, for each build a (sparse) array of test results by test index.
// - flakyTests: array with the boolean value true at test indices that are considered flaky (more than one single-build failure).
// - flakyDeltasByBuild: array of builds, for each build a count of flaky test results by expectation, as well as a total.
function decompressResults(builderResults)
{
var builderTestResults = builderResults[TESTS_KEY];
var buildCount = builderResults[FIXABLE_COUNTS_KEY].length;
var resultsByBuild = new Array(buildCount);
var flakyDeltasByBuild = new Array(buildCount);
// Pre-sizing the test result arrays for each build saves us ~250ms
var testCount = 0;
for (var testName in builderTestResults)
testCount++;
for (var i = 0; i < buildCount; i++) {
resultsByBuild[i] = new Array(testCount);
resultsByBuild[i][testCount - 1] = undefined;
flakyDeltasByBuild[i] = {};
}
// Using indices instead of the full test names for each build saves us
// ~1500ms
var testIndex = 0;
var testNames = new Array(testCount);
var flakyTests = new Array(testCount);
// Decompress and "invert" test results (by build instead of by test) and
// determine which are flaky.
for (var testName in builderTestResults) {
var oneBuildFailureCount = 0;
testNames[testIndex] = testName;
var testResults = builderTestResults[testName].results;
for (var i = 0, rleResult, currentBuildIndex = 0; (rleResult = testResults[i]) && currentBuildIndex < buildCount; i++) {
var count = rleResult[RLE.LENGTH];
var value = rleResult[RLE.VALUE];
if (count == 1 && value in FAILURE_EXPECTATIONS_)
oneBuildFailureCount++;
for (var j = 0; j < count; j++) {
resultsByBuild[currentBuildIndex++][testIndex] = value;
if (currentBuildIndex == buildCount)
break;
}
}
if (oneBuildFailureCount > 2)
flakyTests[testIndex] = true;
testIndex++;
}
// Now that we know which tests are flaky, count the test results that are
// from flaky tests for each build.
testIndex = 0;
for (var testName in builderTestResults) {
if (!flakyTests[testIndex++])
continue;
var testResults = builderTestResults[testName].results;
for (var i = 0, rleResult, currentBuildIndex = 0; (rleResult = testResults[i]) && currentBuildIndex < buildCount; i++) {
var count = rleResult[RLE.LENGTH];
var value = rleResult[RLE.VALUE];
for (var j = 0; j < count; j++) {
var buildTestResults = flakyDeltasByBuild[currentBuildIndex++];
function addFlakyDelta(key)
{
if (!(key in buildTestResults))
buildTestResults[key] = 0;
buildTestResults[key]++;
}
addFlakyDelta(value);
if (value != 'P' && value != 'N')
addFlakyDelta('total');
if (currentBuildIndex == buildCount)
break;
}
}
}
return {
testNames: testNames,
resultsByBuild: resultsByBuild,
flakyTests: flakyTests,
flakyDeltasByBuild: flakyDeltasByBuild
};
}
document.addEventListener('mousedown', function(e) {
// Clear the open popup, unless the click was inside the popup.
var popup = $('popup');
if (popup && e.target != popup && !(popup.compareDocumentPosition(e.target) & 16))
hidePopup();
}, false);
window.addEventListener('load', function() {
// This doesn't seem totally accurate as there is a race between
// onload firing and the last script tag being executed.
logTime('Time to load JS', g_pageLoadStartTime);
g_resourceLoader = new loader.Loader();
g_resourceLoader.load();
}, false);