diff options
author | dfalcantara@chromium.org <dfalcantara@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-04-14 18:33:10 +0000 |
---|---|---|
committer | dfalcantara@chromium.org <dfalcantara@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-04-14 18:33:10 +0000 |
commit | 1200e7e53f1a133481b82287dd88393f473227c3 (patch) | |
tree | b6bcb7ae9da07999db765fb078eb97042d6c384a | |
parent | 2b609d7d55a824522fac1f227c60e5acb8bbbfc1 (diff) | |
download | chromium_src-1200e7e53f1a133481b82287dd88393f473227c3.zip chromium_src-1200e7e53f1a133481b82287dd88393f473227c3.tar.gz chromium_src-1200e7e53f1a133481b82287dd88393f473227c3.tar.bz2 |
Sheriffing dashboard updates
* Brought the code closer in line with the Google JS style guide
http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml
Ended up changing random_variable_names to randomVariableNames and
other renames everywhere.
* Broke apart all of the functions into more logical files so that
classes have functions that operate on the data more sanely.
* Added an index_android.html that focuses on the Android bots.
Also introduces a function that gets overridden to allow filtering
out which bots are uninteresting on the Chromium trees.
* Realized that Android's internal repos use git, which sadly don't
correspond to SVN revisions. This means that the Android dashboard
can't use nifty the test failure dash yet. Removed it from the Android
dashboard until there's a workaround.
* Reduced the number of builds shown on the tree since it's unlikely that
people are super interested in something 5 builds back.
* Fixed the offline status so that it doesn't mess up the table formatting.
* Adds the missing "warnings" bot status.
* Random styling changes to make the dashboard feel shiny, hip, and new.
R=scottmg@chromium.org
Review URL: https://codereview.chromium.org/235863003
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@263676 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | tools/sheriffing/botinfo.js | 227 | ||||
-rw-r--r-- | tools/sheriffing/buildinfo.js | 137 | ||||
-rw-r--r-- | tools/sheriffing/failureinfo.js | 191 | ||||
-rw-r--r-- | tools/sheriffing/functions.js | 854 | ||||
-rw-r--r-- | tools/sheriffing/index.html | 85 | ||||
-rw-r--r-- | tools/sheriffing/index_android.html | 128 | ||||
-rw-r--r-- | tools/sheriffing/statuspageinfo.js | 61 | ||||
-rw-r--r-- | tools/sheriffing/style.css | 40 | ||||
-rw-r--r-- | tools/sheriffing/waterfallinfo.js | 134 |
9 files changed, 1024 insertions, 833 deletions
diff --git a/tools/sheriffing/botinfo.js b/tools/sheriffing/botinfo.js new file mode 100644 index 0000000..9b7e1a1 --- /dev/null +++ b/tools/sheriffing/botinfo.js @@ -0,0 +1,227 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** Information about a particular bot. */ +function BotInfo(name, category) { + // Chop off any digits at the beginning of category names. + if (category && category.length > 0) { + var splitterIndex = category.indexOf('|'); + if (splitterIndex != -1) { + category = category.substr(0, splitterIndex); + } + + while (category[0] >= '0' && category[0] <= '9') { + category = category.substr(1, category.length); + } + } + + this.buildNumbersRunning = null; + this.builds = {}; + this.category = category; + this.inFlight = 0; + this.isSteadyGreen = false; + this.name = name; + this.numUpdatesOffline = 0; + this.state = ''; +} + +/** Update info about the bot, including info about the builder's builds. */ +BotInfo.prototype.update = function(rootJsonUrl, builderJson) { + // Update the builder's state. + this.buildNumbersRunning = builderJson.currentBuilds; + this.numPendingBuilds = builderJson.pendingBuilds; + this.state = builderJson.state; + + // Check if an offline bot is still offline. + if (this.state == 'offline') { + this.numUpdatesOffline++; + console.log(this.name + ' has been offline for ' + + this.numUpdatesOffline + ' update(s) in a row'); + } else { + this.numUpdatesOffline = 0; + } + + // Send asynchronous requests to get info about the builder's last builds. + var lastCompletedBuildNumber = + this.guessLastCompletedBuildNumber(builderJson); + if (lastCompletedBuildNumber) { + var startNumber = lastCompletedBuildNumber - NUM_PREVIOUS_BUILDS_TO_SHOW; + for (var buildNumber = startNumber; + buildNumber <= lastCompletedBuildNumber; + ++buildNumber) { + if (buildNumber < 0) continue; + + // Use cached state after the builder indicates that it has finished. + if (this.builds[buildNumber] && + this.builds[buildNumber].state != 'running') { + gNumRequestsIgnored++; + continue; + } + + this.requestJson(rootJsonUrl, buildNumber); + } + } +}; + +/** Request and save data about a particular build. */ +BotInfo.prototype.requestJson = function(rootJsonUrl, buildNumber) { + this.inFlight++; + gNumRequestsInFlight++; + + var botInfo = this; + var url = rootJsonUrl + 'builders/' + this.name + '/builds/' + buildNumber; + var request = new XMLHttpRequest(); + request.open('GET', url, true); + request.onreadystatechange = function() { + if (request.readyState == 4 && request.status == 200) { + botInfo.inFlight--; + gNumRequestsInFlight--; + + var json = JSON.parse(request.responseText); + botInfo.builds[json.number] = new BuildInfo(json); + botInfo.updateIsSteadyGreen(); + gWaterfallDataIsDirty = true; + } + }; + request.send(null); +}; + +/** Guess the last known build a builder finished. */ +BotInfo.prototype.guessLastCompletedBuildNumber = function(builderJson) { + // The cached builds line doesn't store every build so we can't just take the + // last number. + var buildNumbersRunning = builderJson.currentBuilds; + this.buildNumbersRunning = buildNumbersRunning; + + var buildNumbersCached = builderJson.cachedBuilds; + if (buildNumbersRunning && buildNumbersRunning.length > 0) { + var maxBuildNumber = + Math.max(buildNumbersCached[buildNumbersCached.length - 1], + buildNumbersRunning[buildNumbersRunning.length - 1]); + + var completedBuildNumber = maxBuildNumber; + while (buildNumbersRunning.indexOf(completedBuildNumber) != -1 && + completedBuildNumber >= 0) { + completedBuildNumber--; + } + return completedBuildNumber; + } else { + // Nothing's currently building. Assume the last cached build is correct. + return buildNumbersCached[buildNumbersCached.length - 1]; + } +}; + +/** + * Returns true IFF the last few builds are all green. + * Also alerts the user if the last completed build goes red after being + * steadily green (if desired). + */ +BotInfo.prototype.updateIsSteadyGreen = function() { + var ascendingBuildNumbers = Object.keys(this.builds); + ascendingBuildNumbers.sort(); + + var lastNumber = + ascendingBuildNumbers.length - 1 - NUM_PREVIOUS_BUILDS_TO_SHOW; + for (var j = ascendingBuildNumbers.length - 1; + j >= 0 && j >= lastNumber; + --j) { + var buildNumber = ascendingBuildNumbers[j]; + if (!buildNumber) continue; + + var buildInfo = this.builds[buildNumber]; + if (!buildInfo) continue; + + // Running builds throw heuristics out of whack. Keep the bot visible. + if (buildInfo.state == 'running') return false; + + if (buildInfo.state != 'success') { + if (this.isSteadyGreen && + document.getElementById('checkbox-alert-steady-red').checked) { + alert(this.name + + ' has failed for the first time in a while. Consider looking.'); + } + this.isSteadyGreen = false; + return; + } + } + + this.isSteadyGreen = true; + return; +}; + +/** Creates HTML elements to display info about this bot. */ +BotInfo.prototype.createHtml = function(waterfallBaseUrl) { + var botRowElement = document.createElement('tr'); + + // Insert a cell for the bot category. + var categoryCellElement = botRowElement.insertCell(-1); + categoryCellElement.innerHTML = this.category; + categoryCellElement.className = 'category'; + + // Insert a cell for the bot name. + var botUrl = waterfallBaseUrl + this.name; + var botElement = document.createElement('a'); + botElement.href = botUrl; + botElement.innerHTML = this.name; + + var nameCell = botRowElement.insertCell(-1); + nameCell.appendChild(botElement); + nameCell.className = 'bot-name' + (this.inFlight > 0 ? ' in-flight' : ''); + + // Create a cell to show how many CLs are waiting for a build. + var pendingCell = botRowElement.insertCell(-1); + pendingCell.className = 'pending-count'; + pendingCell.title = 'Pending builds: ' + this.numPendingBuilds; + if (this.numPendingBuilds) { + pendingCell.innerHTML = '+' + this.numPendingBuilds; + } + + // Create a cell to indicate what the bot is currently doing. + var runningElement = botRowElement.insertCell(-1); + if (this.buildNumbersRunning && this.buildNumbersRunning.length > 0) { + // Display the number of the highest numbered running build. + this.buildNumbersRunning.sort(); + var numRunning = this.buildNumbersRunning.length; + var buildNumber = this.buildNumbersRunning[numRunning - 1]; + var buildUrl = botUrl + '/builds/' + buildNumber; + createBuildHtml(runningElement, + buildUrl, + buildNumber, + 'Builds running: ' + numRunning, + null, + 'running'); + } else if (this.state == 'offline' && this.numUpdatesOffline >= 3) { + // The bot's supposedly offline. Waits a few updates since a bot can be + // marked offline in between builds and during reboots. + createBuildHtml(runningElement, + botUrl, + 'offline', + 'Offline for: ' + this.numUpdatesOffline, + null, + 'offline'); + } + + // Display information on the builds we have. + // This assumes that the build number always increases, but this is a bad + // assumption since builds get parallelized. + var buildNumbers = Object.keys(this.builds); + buildNumbers.sort(); + for (var j = buildNumbers.length - 1; + j >= 0 && j >= buildNumbers.length - 1 - NUM_PREVIOUS_BUILDS_TO_SHOW; + --j) { + var buildNumber = buildNumbers[j]; + if (!buildNumber) continue; + + var buildInfo = this.builds[buildNumber]; + if (!buildInfo) continue; + + var buildNumberCell = botRowElement.insertCell(-1); + var isLastBuild = (j == buildNumbers.length - 1); + + // Create and append the cell. + this.builds[buildNumber].createHtml(buildNumberCell, botUrl, isLastBuild); + } + + return botRowElement; +}; diff --git a/tools/sheriffing/buildinfo.js b/tools/sheriffing/buildinfo.js new file mode 100644 index 0000000..016725e --- /dev/null +++ b/tools/sheriffing/buildinfo.js @@ -0,0 +1,137 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** Information about a particular build. */ +function BuildInfo(json) { + // Parse out the status message for the build. + var statusText; + if (json.currentStep) { + statusText = 'running ' + json.currentStep.name; + } else { + statusText = json.text.join(' '); + } + + // Determine what state the build is in. + var state; + if (statusText.indexOf('exception') != -1) { + state = 'exception'; + } else if (statusText.indexOf('running') != -1) { + state = 'running'; + } else if (statusText.indexOf('successful') != -1) { + state = 'success'; + } else if (statusText.indexOf('failed') != -1) { + state = 'failed'; + } else if (statusText.indexOf('offline') != -1) { + state = 'offline'; + } else if (statusText.indexOf('warnings') != -1) { + state = 'warnings'; + } else { + state = 'unknown'; + } + + var failures = (state == 'failed') ? this.parseFailures(json) : null; + + this.number = json.number; + this.state = state; + this.failures = failures; + this.statusText = statusText; + this.truncatedStatusText = truncateStatusText(statusText); +} + +/** Save data about failed tests to perform blamelist intersections. */ +BuildInfo.prototype.parseFailures = function(json) { + var revisionRange = this.getRevisionRange(json); + if (revisionRange == null) return null; + + var failures = []; + var botName = json.builderName; + for (var i = 0; i < json.steps.length; ++i) { + var step = json.steps[i]; + var binaryName = step.name; + if (step.results[0] != 0) { // Failed. + for (var j = 0; j < step.logs.length; ++j) { + var log = step.logs[j]; + if (log[0] == 'stdio') + continue; + var testName = log[0]; + failures.push([botName, binaryName, testName, revisionRange]); + } + } + } + + return failures; +}; + +/** + * Get the revisions involved in a build. Sadly, this only works on Chromium's + * main builders because downstream trees provide git revision SHA1s through + * JSON instead of SVN numbers. + */ +BuildInfo.prototype.getRevisionRange = function(json) { + if (json.sourceStamp.changes.length == 0) { + return null; + } + + var lowest = parseInt(json.sourceStamp.changes[0].revision, 10); + var highest = parseInt(json.sourceStamp.changes[0].revision, 10); + for (var i = 1; i < json.sourceStamp.changes.length; ++i) { + var rev = parseInt(json.sourceStamp.changes[i].revision, 10); + if (rev < lowest) + lowest = rev; + if (rev > highest) + highest = rev; + } + return [lowest, highest]; +}; + +/** Creates HTML to display info about this build. */ +BuildInfo.prototype.createHtml = function(buildNumberCell, + botUrl, + showFullInfo) { + var fullStatusText = 'Build ' + this.number + ':\n' + this.statusText; + createBuildHtml(buildNumberCell, + botUrl + '/builds/' + this.number, + showFullInfo ? this.number : null, + fullStatusText, + showFullInfo ? this.truncatedStatusText : null, + this.state); +}; + +/** Creates a table cell for a particular build number. */ +function createBuildHtml(cellElement, + url, + buildNumber, + fullStatusText, + truncatedStatusText, + buildState) { + // Create a link to the build results. + var linkElement = document.createElement('a'); + linkElement.href = url; + + // Display either the build number (for the last completed build), or show the + // status of the step. + var buildIdentifierElement = document.createElement('span'); + if (buildNumber) { + buildIdentifierElement.className = 'build-identifier'; + buildIdentifierElement.innerHTML = buildNumber; + } else { + buildIdentifierElement.className = 'build-letter'; + buildIdentifierElement.innerHTML = buildState.toUpperCase()[0]; + } + linkElement.appendChild(buildIdentifierElement); + + // Show the status of the build in truncated form so it doesn't take up the + // whole screen. + if (truncatedStatusText) { + var statusElement = document.createElement('span'); + statusElement.className = 'build-status'; + statusElement.innerHTML = truncatedStatusText; + linkElement.appendChild(statusElement); + } + + // Tack the cell onto the end of the row. + cellElement.className = buildState; + cellElement.title = fullStatusText; + cellElement.appendChild(linkElement); +} diff --git a/tools/sheriffing/failureinfo.js b/tools/sheriffing/failureinfo.js new file mode 100644 index 0000000..ad349b7 --- /dev/null +++ b/tools/sheriffing/failureinfo.js @@ -0,0 +1,191 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * Return the range of intersection between the two lists. Ranges are sets of + * CLs, so it's inclusive on both ends. + */ +function rangeIntersection(a, b) { + if (a[0] > b[1]) + return null; + if (a[1] < b[0]) + return null; + // We know they intersect, result is the larger lower bound to the smaller + // upper bound. + return [Math.max(a[0], b[0]), Math.min(a[1], b[1])]; +} + +/** + * Finds the intersections between the blamelists. + * Input is a object mapping botname => [low, high]. + * + * Result: + * [ + * [ low0, high0 ], [ bot1, bot2, bot3 ], + * [ low1, high1 ], [ bot4, ... ], + * ..., + * ] + */ +function findIntersections(testData) { + var keys = Object.keys(testData); + var intersections = []; + var botName = keys[0]; + var range = testData[botName]; + intersections.push([range, [botName]]); + for (var i = 1; i < keys.length; ++i) { + botName = keys[i]; + range = testData[botName]; + var intersectedSome = false; + for (var j = 0; j < intersections.length; ++j) { + var intersect = rangeIntersection(intersections[j][0], range); + if (intersect) { + intersections[j][0] = intersect; + intersections[j][1].push(botName); + intersectedSome = true; + break; + } + } + if (!intersectedSome) { + intersections.push([range, [botName]]); + } + } + + return intersections; +} + +/** Flatten out the list of tests and sort them by the binary/test name. */ +function flattenAndSortTests(rangesByTest) { + var rangesByTestNames = Object.keys(rangesByTest); + var flat = []; + for (var i = 0; i < rangesByTestNames.length; ++i) { + var name = rangesByTestNames[i]; + flat.push([name, rangesByTest[name]]); + } + + flat.sort(function(a, b) { + if (a[0] < b[0]) return -1; + if (a[0] > b[0]) return 1; + return 0; + }); + + return flat; +} + +/** + * Build the HTML table row for a test failure. |test| is [ name, testData ]. + * |testData| contains ranges suitable for input to |findIntersections|. + */ +function buildTestFailureTableRowHTML(test) { + var row = document.createElement('tr'); + var binaryCell = row.insertCell(-1); + var nameParts = test[0].split('-'); + binaryCell.innerHTML = nameParts[0]; + binaryCell.className = 'category'; + var flakinessLink = document.createElement('a'); + flakinessLink.href = + 'http://test-results.appspot.com/dashboards/' + + 'flakiness_dashboard.html#testType=' + + nameParts[0] + '&tests=' + nameParts[1]; + flakinessLink.innerHTML = nameParts[1]; + row.appendChild(flakinessLink); + + var intersections = findIntersections(test[1]); + for (var j = 0; j < intersections.length; ++j) { + var intersection = intersections[j]; + var range = row.insertCell(-1); + range.className = 'failure-range'; + var low = intersection[0][0]; + var high = intersection[0][1]; + var url = + 'http://build.chromium.org/f/chromium/perf/dashboard/ui/' + + 'changelog.html?url=%2Ftrunk%2Fsrc&range=' + + low + '%3A' + high + '&mode=html'; + range.innerHTML = '<a href="' + url + '">' + low + ' - ' + high + '</a>: ' + + truncateStatusText(intersection[1].join(', ')); + } + return row; +} + + +/** Updates the correlations contents. */ +function updateCorrelationsHTML() { + // The logic here is to try to narrow blamelists by using information across + // bots. If a particular test has failed on multiple different + // configurations, there's a good chance that it has the same root cause, so + // calculate the intersection of blamelists of the first time it failed on + // each builder. + + var allFailures = []; + for (var i = 0; i < gWaterfallData.length; ++i) { + var waterfallInfo = gWaterfallData[i]; + var botInfo = waterfallInfo.botInfo; + var allBotNames = Object.keys(botInfo); + for (var j = 0; j < allBotNames.length; ++j) { + var botName = allBotNames[j]; + if (botInfo[botName].isSteadyGreen) + continue; + var builds = botInfo[botName].builds; + var buildNames = Object.keys(builds); + for (var k = 0; k < buildNames.length; ++k) { + var build = builds[buildNames[k]]; + if (build.failures) { + for (var l = 0; l < build.failures.length; ++l) { + allFailures.push(build.failures[l]); + } + } + } + } + } + + // allFailures is now a list of lists, each containing: + // [ botname, binaryname, testname, [low_rev, high_rev] ]. + + var rangesByTest = {}; + for (var i = 0; i < allFailures.length; ++i) { + var failure = allFailures[i]; + var botName = failure[0]; + var binaryName = failure[1]; + var testName = failure[2]; + var range = failure[3]; + if (binaryName.indexOf('steps') != -1) + continue; + if (testName.indexOf('preamble') != -1) + continue; + var key = binaryName + '-' + testName; + + if (!rangesByTest.hasOwnProperty(key)) + rangesByTest[key] = {}; + // If there's no range, that's all we know. + if (!rangesByTest[key].hasOwnProperty([botName])) { + rangesByTest[key][botName] = range; + } else { + // Otherwise, track only the lowest range for this bot (we only want + // when it turned red, not the whole range that it's been red for). + if (range[0] < rangesByTest[key][botName][0]) { + rangesByTest[key][botName] = range; + } + } + } + + var table = document.getElementById('failure-info'); + while (table.rows.length > 0) { + table.deleteRow(-1); + } + + var headerCell = document.createElement('td'); + headerCell.colSpan = 15; + headerCell.innerHTML = + 'test failures (ranges are blamelist intersections of first failure on ' + + 'each bot of retrieved data. so, if a test has been failing for a ' + + 'while, the range may be incorrect)'; + headerCell.className = 'section-header'; + var headerRow = table.insertRow(-1); + headerRow.appendChild(headerCell); + + var flat = flattenAndSortTests(rangesByTest); + + for (var i = 0; i < flat.length; ++i) { + table.appendChild(buildTestFailureTableRowHTML(flat[i])); + } +} diff --git a/tools/sheriffing/functions.js b/tools/sheriffing/functions.js index 9c16744..4e9ea7f 100644 --- a/tools/sheriffing/functions.js +++ b/tools/sheriffing/functions.js @@ -3,858 +3,150 @@ // found in the LICENSE file. /** Random constants. */ -var kMaxBuildStatusLength = 50; -var kTicksBetweenRefreshes = 60; -var kHistoricBuilds = 4; -var kMaxMillisecondsToWait = 5 * 60 * 1000; - -var g_ticks_until_refresh = kTicksBetweenRefreshes; +var MAX_BUILD_STATUS_LENGTH = 50; +var TICKS_BETWEEN_REFRESHES = 60; +var NUM_PREVIOUS_BUILDS_TO_SHOW = 3; +var MAX_MILLISECONDS_TO_WAIT = 5 * 60 * 1000; /** Parsed JSON data. */ -var g_waterfall_data = {}; -var g_status_data = {}; -var g_waterfall_data_dirty = true; -var g_css_loading_rule = null; +var gWaterfallData = []; +var gStatusData = []; +var gWaterfallDataIsDirty = true; + +/** Global state. */ +var gTicksUntilRefresh = TICKS_BETWEEN_REFRESHES; /** Statistics. */ -var g_requests_in_flight = 0; -var g_requests_ignored = 0; -var g_requests_retried = 0; -var g_start_time = 0; +var gNumRequestsInFlight = 0; +var gNumRequestsIgnored = 0; +var gNumRequestsRetried = 0; +var gStartTimestamp = 0; /** Cut the status message down so it doesn't hog the whole screen. */ function truncateStatusText(text) { - if (text.length > kMaxBuildStatusLength) { - return text.substr(0, kMaxBuildStatusLength) + '...'; + if (text.length > MAX_BUILD_STATUS_LENGTH) { + return text.substr(0, MAX_BUILD_STATUS_LENGTH) + '...'; } return text; } -/** Information about a particular status page. */ -function StatusPageInfo(status_page_url) { - this.url = status_page_url + 'current?format=json'; - this.in_flight = 0; - this.message = ''; - this.date = ''; - this.state = ''; -} - -/** Information about a particular waterfall. */ -function WaterfallInfo(waterfall_name, waterfall_url) { - var waterfall_link_element = document.createElement('a'); - waterfall_link_element.href = waterfall_url; - waterfall_link_element.target = '_blank'; - waterfall_link_element.innerHTML = waterfall_name; - - var td_element = document.createElement('td'); - td_element.colSpan = 15; - td_element.appendChild(waterfall_link_element); - - this.bot_info = {}; - this.url = waterfall_url; - this.in_flight = 0; - this.td_element = td_element; - this.last_update = 0; -} - -/** Information about a particular bot. */ -function BotInfo(category) { - // Sometimes the categories have digits at the beginning for ordering the - // category. Chop it off. - if (category && category.length > 0) { - var splitter_index = category.indexOf('|'); - if (splitter_index != -1) { - category = category.substr(0, splitter_index); - } - } - - this.in_flight = 0; - this.builds = {}; - this.category = category; - this.build_numbers_running = null; - this.is_steady_green = false; - this.state = ''; - this.num_updates_offline = 0; -} - -/** Get the revisions involved in a build. */ -function getRevisionRange(json) { - var lowest = parseInt(json.sourceStamp.changes[0].revision, 10); - var highest = parseInt(json.sourceStamp.changes[0].revision, 10); - for (var i = 1; i < json.sourceStamp.changes.length; ++i) { - var rev = parseInt(json.sourceStamp.changes[i].revision, 10); - if (rev < lowest) - lowest = rev; - if (rev > highest) - highest = rev; - } - return [lowest, highest]; -} - -/** Information about a particular build. */ -function BuildInfo(json) { - // Parse out the status message for the build. - var status_text; - if (json.currentStep) { - status_text = 'running ' + json.currentStep.name; - } else { - status_text = json.text.join(' '); - } - - var truncated_status_text = truncateStatusText(status_text); - - // Determine what state the build is in. - var state; - if (status_text.indexOf('exception') != -1) { - state = 'exception'; - } else if (status_text.indexOf('running') != -1) { - state = 'running'; - } else if (status_text.indexOf('successful') != -1) { - state = 'success'; - } else if (status_text.indexOf('failed') != -1) { - state = 'failed'; - } else if (status_text.indexOf('offline') != -1) { - state = 'offline'; - } else { - state = 'unknown'; - } - - if (state == 'failed') { - // Save data about failed tests and blamelist so we can do intersections. - var failures = []; - var revision_range = getRevisionRange(json); - var bot_name = json.builderName; - for (var i = 0; i < json.steps.length; ++i) { - var step = json.steps[i]; - var binary_name = step.name; - if (step.results[0] != 0) { // Failed. - for (var j = 0; j < step.logs.length; ++j) { - var log = step.logs[j]; - if (log[0] == 'stdio') - continue; - var test_name = log[0]; - failures.push([bot_name, binary_name, test_name, revision_range]); - } - } - } - this.failures = failures; - } else { - this.failures = null; - } - - this.status_text = status_text; - this.truncated_status_text = truncated_status_text; - this.state = state; -} - -/** Send and parse an asynchronous request to get a repo status JSON. */ -function requestStatusPageJSON(repo_name) { - if (g_status_data[repo_name].in_flight) return; - - var request = new XMLHttpRequest(); - request.open('GET', g_status_data[repo_name].url, true); - request.onreadystatechange = function() { - if (request.readyState == 4 && request.status == 200) { - g_status_data[repo_name].in_flight--; - g_requests_in_flight--; - - status_page_json = JSON.parse(request.responseText); - g_status_data[repo_name].message = status_page_json.message; - g_status_data[repo_name].state = status_page_json.general_state; - g_status_data[repo_name].date = status_page_json.date; - } - }; - g_status_data[repo_name].in_flight++; - g_requests_in_flight++; - request.send(null); -} - -/** Send an asynchronous request to get the main waterfall's JSON. */ -function requestWaterfallJSON(waterfall_info) { - if (waterfall_info.in_flight) { - var elapsed = new Date().getTime() - waterfall_info.last_update; - if (elapsed < kMaxMillisecondsToWait) return; - - waterfall_info.in_flight--; - g_requests_in_flight--; - g_requests_retried++; - } - - var waterfall_url = waterfall_info.url; - var url = waterfall_url + 'json/builders/'; - - var request = new XMLHttpRequest(); - request.open('GET', url, true); - request.onreadystatechange = function() { - if (request.readyState == 4 && request.status == 200) { - waterfall_info.in_flight--; - g_requests_in_flight--; - parseWaterfallJSON(JSON.parse(request.responseText), waterfall_info); - } - }; - waterfall_info.in_flight++; - g_requests_in_flight++; - waterfall_info.last_update = new Date().getTime(); - request.send(null); -} - -/** Update info about the bot, including info about the builder's builds. */ -function requestBuilderJSON(waterfall_info, - root_json_url, - builder_json, - builder_name) { - // Prepare the bot info. - if (!waterfall_info.bot_info[builder_name]) { - waterfall_info.bot_info[builder_name] = new BotInfo(builder_json.category); - } - var bot_info = waterfall_info.bot_info[builder_name]; - - // Update the builder's state. - bot_info.build_numbers_running = builder_json.currentBuilds; - bot_info.num_pending_builds = builder_json.pendingBuilds; - bot_info.state = builder_json.state; - if (bot_info.state == 'offline') { - bot_info.num_updates_offline++; - console.log(builder_name + ' has been offline for ' + - bot_info.num_updates_offline + ' update(s) in a row'); - } else { - bot_info.num_updates_offline = 0; - } - - // Send an asynchronous request to get info about the builder's last builds. - var last_completed_build_number = - guessLastCompletedBuildNumber(builder_json, bot_info); - if (last_completed_build_number) { - for (var build_number = last_completed_build_number - kHistoricBuilds; - build_number <= last_completed_build_number; - ++build_number) { - if (build_number < 0) continue; - requestBuildJSON(waterfall_info, - builder_name, - root_json_url, - builder_json, - build_number); - } - } -} - -/** Given a builder's JSON, guess the last known build that it completely - * finished. */ -function guessLastCompletedBuildNumber(builder_json, bot_info) { - // The cached builds line doesn't store every build so we can't just take the - // last number. - var build_numbers_running = builder_json.currentBuilds; - bot_info.build_numbers_running = build_numbers_running; - - var build_numbers_cached = builder_json.cachedBuilds; - if (build_numbers_running && build_numbers_running.length > 0) { - max_build_number = - Math.max(build_numbers_cached[build_numbers_cached.length - 1], - build_numbers_running[build_numbers_running.length - 1]); - - var completed_build_number = max_build_number; - while (build_numbers_running.indexOf(completed_build_number) != -1 && - completed_build_number >= 0) { - completed_build_number--; - } - return completed_build_number; - } else { - // Nothing's currently building. Just assume the last cached build is - // correct. - return build_numbers_cached[build_numbers_cached.length - 1]; - } -} - -/** Get the data for a particular build. */ -function requestBuildJSON(waterfall_info, - builder_name, - root_json_url, - builder_json, - build_number) { - var bot_info = waterfall_info.bot_info[builder_name]; - - // Check if we already have the data on this build. - if (bot_info.builds[build_number] && - bot_info.builds[build_number].state != 'running') { - g_requests_ignored++; - return; - } - - // Grab it. - var url = - root_json_url + 'builders/' + builder_name + '/builds/' + build_number; - - var request = new XMLHttpRequest(); - request.open('GET', url, true); - request.onreadystatechange = function() { - if (request.readyState == 4 && request.status == 200) { - bot_info.in_flight--; - g_requests_in_flight--; - - var json = JSON.parse(request.responseText); - bot_info.builds[build_number] = new BuildInfo(json); - - checkBotIsSteadyGreen(builder_name, bot_info); - g_waterfall_data_dirty = true; - } - }; - - bot_info.in_flight++; - g_requests_in_flight++; - request.send(null); -} - -function parseWaterfallJSON(builders_json, waterfall_info) { - var root_json_url = waterfall_info.url + 'json/'; - - // Go through each builder on the waterfall and get the latest status. - var builder_names = Object.keys(builders_json); - for (var i = 0; i < builder_names.length; ++i) { - var builder_name = builder_names[i]; - - // TODO: Any filtering here. - - var builder_json = builders_json[builder_name]; - requestBuilderJSON( - waterfall_info, root_json_url, builder_json, builder_name); - g_waterfall_data_dirty = true; - } -} - /** Queries all of the servers for their latest statuses. */ function queryServersForInfo() { - var waterfall_names = Object.keys(g_waterfall_data); - for (var index = 0; index < waterfall_names.length; ++index) { - var name = waterfall_names[index]; - var waterfall_info = g_waterfall_data[name]; - requestWaterfallJSON(waterfall_info); - } - - var status_page_names = Object.keys(g_status_data); - for (var index = 0; index < status_page_names.length; ++index) { - requestStatusPageJSON(status_page_names[index]); + for (var index = 0; index < gWaterfallData.length; ++index) { + gWaterfallData[index].requestJson(); } -} - -/** - * Return the range of intersection between the two lists. Ranges are sets of - * CLs, so it's inclusive on both ends. - */ -function rangeIntersection(a, b) { - if (a[0] > b[1]) - return null; - if (a[1] < b[0]) - return null; - // We know they intersect, result is the larger lower bound to the smaller - // upper bound. - return [Math.max(a[0], b[0]), Math.min(a[1], b[1])]; -} -function findIntersections(test_data) { - // Input is a object mapping botname => [low, high]. - // - // Result: - // [ - // [ low0, high0 ], [ bot1, bot2, bot3 ], - // [ low1, high1 ], [ bot4, ... ], - // ..., - // ] - - var keys = Object.keys(test_data); - var intersections = []; - var bot_name = keys[0]; - var range = test_data[bot_name]; - intersections.push([range, [bot_name]]); - for (var i = 1; i < keys.length; ++i) { - bot_name = keys[i]; - range = test_data[bot_name]; - var intersected_some = false; - for (var j = 0; j < intersections.length; ++j) { - var intersect = rangeIntersection(intersections[j][0], range); - if (intersect) { - intersections[j][0] = intersect; - intersections[j][1].push(bot_name); - intersected_some = true; - break; - } - } - if (!intersected_some) { - intersections.push([range, [bot_name]]); - } - } - - return intersections; -} - -/** Flatten out the list of tests and sort them by the binary/test name. */ -function flattenAndSortTests(ranges_by_test) { - var ranges_by_test_names = Object.keys(ranges_by_test); - var flat = []; - for (var i = 0; i < ranges_by_test_names.length; ++i) { - var name = ranges_by_test_names[i]; - flat.push([name, ranges_by_test[name]]); - } - - flat.sort(function(a, b) { - if (a[0] < b[0]) return -1; - if (a[0] > b[0]) return 1; - return 0; - }); - - return flat; -} - -/** - * Build the HTML table row for a test failure. |test| is [ name, test_data ]. - * |test_data| contains ranges suitable for input to |findIntersections|. - */ -function buildTestFailureTableRowHTML(test) { - var row = document.createElement('tr'); - var binary_cell = row.insertCell(-1); - var name_parts = test[0].split('-'); - binary_cell.innerHTML = name_parts[0]; - binary_cell.className = 'category'; - var flakiness_link = document.createElement('a'); - flakiness_link.href = - 'http://test-results.appspot.com/dashboards/' + - 'flakiness_dashboard.html#testType=' + - name_parts[0] + '&tests=' + name_parts[1]; - flakiness_link.target = '_blank'; - flakiness_link.innerHTML = name_parts[1]; - row.appendChild(flakiness_link); - - var intersections = findIntersections(test[1]); - for (var j = 0; j < intersections.length; ++j) { - var intersection = intersections[j]; - var range = row.insertCell(-1); - range.className = 'failure_range'; - var low = intersection[0][0]; - var high = intersection[0][1]; - var url = - 'http://build.chromium.org/f/chromium/perf/dashboard/ui/' + - 'changelog.html?url=%2Ftrunk%2Fsrc&range=' + - low + '%3A' + high + '&mode=html'; - range.innerHTML = '<a target="_blank" href="' + url + '">' + low + - ' - ' + high + '</a>: ' + - truncateStatusText(intersection[1].join(', ')); - } - return row; -} - - -/** Updates the correlations contents. */ -function updateCorrelationsHTML() { - // The logic here is to try to narrow blamelists by using information across - // bots. If a particular test has failed on multiple different - // configurations, there's a good chance that it has the same root cause, so - // calculate the intersection of blamelists of the first time it failed on - // each builder. - - var all_failures = []; - for (var i = 0; i < kBuilderPages.length; ++i) { - var waterfall_name = kBuilderPages[i][0]; - var waterfall_info = g_waterfall_data[waterfall_name]; - var bot_info = waterfall_info.bot_info; - var all_bot_names = Object.keys(bot_info); - for (var j = 0; j < all_bot_names.length; ++j) { - var bot_name = all_bot_names[j]; - if (bot_info[bot_name].is_steady_green) - continue; - var builds = bot_info[bot_name].builds; - var build_names = Object.keys(builds); - for (var k = 0; k < build_names.length; ++k) { - var build = builds[build_names[k]]; - if (build.failures) { - for (var l = 0; l < build.failures.length; ++l) { - all_failures.push(build.failures[l]); - } - } - } - } - } - - // all_failures is now a list of lists, each containing: - // [ botname, binaryname, testname, [low_rev, high_rev] ]. - - var ranges_by_test = {}; - for (var i = 0; i < all_failures.length; ++i) { - var failure = all_failures[i]; - var bot_name = failure[0]; - var binary_name = failure[1]; - var test_name = failure[2]; - var range = failure[3]; - if (binary_name.indexOf('steps') != -1) - continue; - if (test_name.indexOf('preamble') != -1) - continue; - var key = binary_name + '-' + test_name; - - if (!ranges_by_test.hasOwnProperty(key)) - ranges_by_test[key] = {}; - // If there's no range, that's all we know. - if (!ranges_by_test[key].hasOwnProperty([bot_name])) { - ranges_by_test[key][bot_name] = range; - } else { - // Otherwise, track only the lowest range for this bot (we only want - // when it turned red, not the whole range that it's been red for). - if (range[0] < ranges_by_test[key][bot_name][0]) { - ranges_by_test[key][bot_name] = range; - } - } - } - - var table = document.getElementById('failure_info'); - while (table.rows.length > 0) { - table.deleteRow(-1); - } - - var header_cell = document.createElement('td'); - header_cell.colSpan = 15; - header_cell.innerHTML = - 'test failures (ranges are blamelist intersections of first failure on ' + - 'each bot of retrieved data. so, if a test has been failing for a ' + - 'while, the range may be incorrect)'; - header_cell.className = 'waterfall_name'; - var header_row = table.insertRow(-1); - header_row.appendChild(header_cell); - - var flat = flattenAndSortTests(ranges_by_test); - - for (var i = 0; i < flat.length; ++i) { - table.appendChild(buildTestFailureTableRowHTML(flat[i])); + for (var index = 0; index < gStatusData.length; ++index) { + gStatusData[index].requestJson(); } } /** Updates the sidebar's contents. */ function updateSidebarHTML() { - var output = ''; - - // Buildbot info. - var status_names = Object.keys(g_status_data); - for (var i = 0; i < status_names.length; ++i) { - var status_name = status_names[i]; - var status_info = g_status_data[status_name]; - var status_url = kStatusPages[i][1]; - - output += '<ul class="box ' + status_info.state + '">'; - output += '<li><a target="_blank" href="' + status_url + '">' + - status_name + '</a></li>'; - output += '<li>' + status_info.date + '</li>'; - output += '<li>' + status_info.message + '</li>'; - output += '</ul>'; + // Update all of the project info. + var divElement = document.getElementById('sidebar-contents'); + while (divElement.firstChild) { + divElement.removeChild(divElement.firstChild); } - var div = document.getElementById('sidebar_contents'); - div.innerHTML = output; + for (var i = 0; i < gStatusData.length; ++i) { + divElement.appendChild(gStatusData[i].createHtml()); + } // Debugging stats. - document.getElementById('ticks_until_refresh').innerHTML = - g_ticks_until_refresh; - document.getElementById('requests_in_flight').innerHTML = - g_requests_in_flight; - document.getElementById('requests_ignored').innerHTML = g_requests_ignored; - document.getElementById('requests_retried').innerHTML = g_requests_retried; + document.getElementById('num-ticks-until-refresh').innerHTML = + gTicksUntilRefresh; + document.getElementById('num-requests-in-flight').innerHTML = + gNumRequestsInFlight; + document.getElementById('num-requests-ignored').innerHTML = + gNumRequestsIgnored; + document.getElementById('num-requests-retried').innerHTML = + gNumRequestsRetried; } -/** Organizes all of the bots by category, then alphabetically within their - * categories. */ -function sortBotNamesByCategory(bot_info) { +/** + * Organizes all of the bots by category, then alphabetically within their + * categories. + */ +function sortBotNamesByCategory(botInfo) { // Bucket all of the bots according to their category. - var all_bot_names = Object.keys(bot_info); - var bucketed_names = {}; - for (var i = 0; i < all_bot_names.length; ++i) { - var bot_name = all_bot_names[i]; - var category = bot_info[bot_name].category; + var allBotNames = Object.keys(botInfo); + var bucketedNames = {}; + for (var i = 0; i < allBotNames.length; ++i) { + var botName = allBotNames[i]; + var category = botInfo[botName].category; - if (!bucketed_names[category]) bucketed_names[category] = []; - bucketed_names[category].push(bot_name); + if (!bucketedNames[category]) bucketedNames[category] = []; + bucketedNames[category].push(botName); } // Alphabetically sort bots within their buckets, then append them to the // current list. - var sorted_bot_names = []; - var all_categories = Object.keys(bucketed_names); - all_categories.sort(); - for (var i = 0; i < all_categories.length; ++i) { - var category = all_categories[i]; - var bucket_bots = bucketed_names[category]; - bucket_bots.sort(); + var sortedBotNames = []; + var allCategories = Object.keys(bucketedNames); + allCategories.sort(); + for (var i = 0; i < allCategories.length; ++i) { + var category = allCategories[i]; + var bucketBots = bucketedNames[category]; + bucketBots.sort(); - for (var j = 0; j < bucket_bots.length; ++j) { - sorted_bot_names.push(bucket_bots[j]); + for (var j = 0; j < bucketBots.length; ++j) { + sortedBotNames.push(bucketBots[j]); } } - return sorted_bot_names; -} - -/** - * Returns true IFF the last few builds are all green. - * Also alerts the user if the last completed build goes red after being - * steadily green (if desired). - */ -function checkBotIsSteadyGreen(bot_name, bot_info) { - var ascending_build_numbers = Object.keys(bot_info.builds); - ascending_build_numbers.sort(); - - for (var j = ascending_build_numbers.length - 1; - j >= 0 && j >= ascending_build_numbers.length - 1 - kHistoricBuilds; - --j) { - var build_number = ascending_build_numbers[j]; - if (!build_number) continue; - - var build_info = bot_info.builds[build_number]; - if (!build_info) continue; - - // Running builds throw heuristics out of whack. Keep the bot visible. - if (build_info.state == 'running') return false; - - if (build_info.state != 'success') { - if (bot_info.is_steady_green && - document.getElementById('checkbox_alert_steady_red').checked) { - alert(bot_name + - ' has failed for the first time in a while. Consider looking.'); - } - bot_info.is_steady_green = false; - return false; - } - } - - bot_info.is_steady_green = true; - return true; + return sortedBotNames; } /** Update all the waterfall data. */ function updateStatusHTML() { - var table = document.getElementById('build_info'); + var table = document.getElementById('build-info'); while (table.rows.length > 0) { table.deleteRow(-1); } - for (var i = 0; i < kBuilderPages.length; ++i) { - var waterfall_name = kBuilderPages[i][0]; - var waterfall_info = g_waterfall_data[waterfall_name]; - updateWaterfallStatusHTML(waterfall_name, waterfall_info); + for (var i = 0; i < gWaterfallData.length; ++i) { + gWaterfallData[i].updateWaterfallStatusHTML(); } } /** Marks the waterfall data as dirty due to updated filter. */ function filterUpdated() { - g_waterfall_data_dirty = true; -} - -/** Creates a table cell for a particular build number. */ -function createBuildNumberCellHTML(build_url, - build_number, - full_status_text, - truncated_status_text, - build_state) { - // Create a link to the build results. - var link_element = document.createElement('a'); - link_element.href = build_url; - link_element.target = '_blank'; - - // Display either the build number (for the last completed build), or show the - // status of the step. - var build_identifier_element = document.createElement('span'); - if (build_number) { - build_identifier_element.className = 'build_identifier'; - build_identifier_element.innerHTML = build_number; - } else { - build_identifier_element.className = 'build_letter'; - build_identifier_element.innerHTML = build_state.toUpperCase()[0]; - } - link_element.appendChild(build_identifier_element); - - // Show the status of the build, in truncated form so it doesn't take up the - // whole screen. - if (truncated_status_text) { - var status_span_element = document.createElement('span'); - status_span_element.className = 'build_status'; - status_span_element.innerHTML = truncated_status_text; - link_element.appendChild(status_span_element); - } - - // Tack the cell onto the end of the row. - var build_number_cell = document.createElement('td'); - build_number_cell.className = build_state; - build_number_cell.title = full_status_text; - build_number_cell.appendChild(link_element); - - return build_number_cell; -} - -/** Updates the HTML for a particular waterfall. */ -function updateWaterfallStatusHTML(waterfall_name, waterfall_info) { - var table = document.getElementById('build_info'); - - // Point at the waterfall. - var header_cell = waterfall_info.td_element; - header_cell.className = - 'waterfall_name' + (waterfall_info.in_flight > 0 ? ' in_flight' : ''); - var header_row = table.insertRow(-1); - header_row.appendChild(header_cell); - - // Print out useful bits about the bots. - var bot_names = sortBotNamesByCategory(waterfall_info.bot_info); - for (var i = 0; i < bot_names.length; ++i) { - var bot_name = bot_names[i]; - var bot_info = waterfall_info.bot_info[bot_name]; - var bot_row = document.createElement('tr'); - - // Insert a cell for the bot category. Chop off any numbers used for - // sorting. - var category_cell = bot_row.insertCell(-1); - var category = bot_info.category; - if (category && category.length > 0 && category[0] >= '0' && - category[0] <= '9') { - category = category.substr(1, category.length); - } - category_cell.innerHTML = category; - category_cell.className = 'category'; - - // Insert a cell for the bot name. - var bot_cell = bot_row.insertCell(-1); - var builder_url = waterfall_info.url + 'builders/' + bot_name; - var bot_link = - '<a target="_blank" href="' + builder_url + '">' + bot_name + '</a>'; - bot_cell.innerHTML = bot_link; - bot_cell.className = - 'bot_name' + (bot_info.in_flight > 0 ? ' in_flight' : ''); - - // Display information on the builds we have. - // This assumes that the build number always increases, but this is a bad - // assumption since - // builds get parallelized. - var build_numbers = Object.keys(bot_info.builds); - build_numbers.sort(); - - if (bot_info.num_pending_builds) { - var pending_cell = document.createElement('span'); - pending_cell.className = 'pending_count'; - pending_cell.innerHTML = '+' + bot_info.num_pending_builds; - bot_row.appendChild(pending_cell); - } else { - bot_row.insertCell(-1); - } - - if (bot_info.build_numbers_running && - bot_info.build_numbers_running.length > 0) { - // Display the number of the highest numbered running build. - bot_info.build_numbers_running.sort(); - var length = bot_info.build_numbers_running.length; - var build_number = bot_info.build_numbers_running[length - 1]; - var build_url = builder_url + '/builds/' + build_number; - var build_number_cell = createBuildNumberCellHTML( - build_url, build_number, null, null, 'running'); - bot_row.appendChild(build_number_cell); - } else if (bot_info.state == 'offline' && - bot_info.num_updates_offline >= 3) { - // The bot's supposedly offline. Wait a few updates to see since a - // builder can be marked offline in between builds and during reboots. - var build_number_cell = document.createElement('td'); - build_number_cell.className = bot_info.state + ' build_identifier'; - build_number_cell.innerHTML = 'offline'; - bot_row.appendChild(build_number_cell); - } else { - bot_row.insertCell(-1); - } - - // Display the last few builds. - for (var j = build_numbers.length - 1; - j >= 0 && j >= build_numbers.length - 1 - kHistoricBuilds; - --j) { - var build_number = build_numbers[j]; - if (!build_number) continue; - - var build_info = bot_info.builds[build_number]; - if (!build_info) continue; - - // Create and append the cell. - var is_last_build = (j == build_numbers.length - 1); - var build_url = builder_url + '/builds/' + build_number; - var status_text_full = - 'Build ' + build_number + ':\n' + build_info.status_text; - var build_number_cell = createBuildNumberCellHTML( - build_url, - is_last_build ? build_number : null, - status_text_full, - is_last_build ? build_info.truncated_status_text : null, - build_info.state); - bot_row.appendChild(build_number_cell); - } - - // Determine whether we should apply keyword filter. - var filter = document.getElementById('text_filter').value.trim(); - if (filter.length > 0) { - var keywords = filter.split(' '); - var build_numbers = Object.keys(bot_info.builds); - var matches_filter = false; - console.log(bot_info); - - for (var x = 0; x < build_numbers.length && !matches_filter; ++x) { - var build_status = bot_info.builds[build_numbers[x]].status_text; - console.log(build_status); - - for (var y = 0; y < keywords.length && !matches_filter; ++y) { - if (build_status.indexOf(keywords[y]) >= 0) - matches_filter = true; - } - } - - if (!matches_filter) - continue; - } - - // If the user doesn't want to see completely green bots, hide it. - var should_hide_stable = - document.getElementById('checkbox_hide_stable').checked; - if (should_hide_stable && bot_info.is_steady_green) - continue; - - table.appendChild(bot_row); - } + gWaterfallDataIsDirty = true; } /** Update the page content. */ function updateContent() { - if (--g_ticks_until_refresh <= 0) { - g_ticks_until_refresh = kTicksBetweenRefreshes; + if (--gTicksUntilRefresh <= 0) { + gTicksUntilRefresh = TICKS_BETWEEN_REFRESHES; queryServersForInfo(); } // Redraw the page content. - if (g_waterfall_data_dirty) { - g_waterfall_data_dirty = false; + if (gWaterfallDataIsDirty) { + gWaterfallDataIsDirty = false; updateStatusHTML(); - updateCorrelationsHTML(); + + if (document.getElementById('failure-info')) { + updateCorrelationsHTML(); + } } updateSidebarHTML(); } /** Initialize all the things. */ function initialize() { - var g_start_time = new Date().getTime(); + var gStartTimestamp = new Date().getTime(); // Initialize the waterfall pages. for (var i = 0; i < kBuilderPages.length; ++i) { - g_waterfall_data[kBuilderPages[i][0]] = - new WaterfallInfo(kBuilderPages[i][0], kBuilderPages[i][1]); + gWaterfallData.push(new WaterfallInfo(kBuilderPages[i])); } // Initialize the status pages. for (var i = 0; i < kStatusPages.length; ++i) { - g_status_data[kStatusPages[i][0]] = new StatusPageInfo(kStatusPages[i][1]); - } - - // Set up the useful links HTML in the sidebar. - var useful_links_ul = document.getElementById('useful_links'); - for (var i = 0; i < kLinks.length; ++i) { - var link_html = '<a target="_blank" href="' + kLinks[i][1] + '">' + - kLinks[i][0] + '</a>'; - var li_element = document.createElement('li'); - li_element.innerHTML = link_html; - useful_links_ul.appendChild(li_element); + gStatusData.push(new StatusPageInfo(kStatusPages[i][0], + kStatusPages[i][1])); } // Kick off the main loops. diff --git a/tools/sheriffing/index.html b/tools/sheriffing/index.html index acaac1b..5758d67 100644 --- a/tools/sheriffing/index.html +++ b/tools/sheriffing/index.html @@ -9,61 +9,70 @@ found in the LICENSE file. <meta http-equiv="cache-control" content="no-cache"> <title>Sheriffing dashboard</title> + <base target="_blank" /> <link href='http://fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'> <link rel="stylesheet" href="style.css"> <script type="text/javascript"> /** Watefall pages, ordered by importance. */ var kBuilderPages = new Array(); - kBuilderPages.push(["clobber builders", "http://build.chromium.org/p/chromium/"]); - kBuilderPages.push(["win", "http://build.chromium.org/p/chromium.win/"]); - kBuilderPages.push(["mac", "http://build.chromium.org/p/chromium.mac/"]); - kBuilderPages.push(["linux", "http://build.chromium.org/p/chromium.linux/"]); - kBuilderPages.push(["chromeos", "http://build.chromium.org/p/chromium.chromiumos/"]); - kBuilderPages.push(["chrome branded", "http://build.chromium.org/p/chromium.chrome/"]); - kBuilderPages.push(["memory", "http://build.chromium.org/p/chromium.memory/"]); + kBuilderPages.push(['clobber builders', 'http://build.chromium.org/p/chromium/', true]); + kBuilderPages.push(['win', 'http://build.chromium.org/p/chromium.win/', true]); + kBuilderPages.push(['mac', 'http://build.chromium.org/p/chromium.mac/', true]); + kBuilderPages.push(['linux', 'http://build.chromium.org/p/chromium.linux/', true]); + kBuilderPages.push(['chromeos', 'http://build.chromium.org/p/chromium.chromiumos/', true]); + kBuilderPages.push(['chrome branded', 'http://build.chromium.org/p/chromium.chrome/', true]); + kBuilderPages.push(['memory', 'http://build.chromium.org/p/chromium.memory/', true]); /** Waterfall open/closed status pages. */ var kStatusPages = new Array(); - kStatusPages.push(["chromium", "https://chromium-status.appspot.com/"]); - kStatusPages.push(["blink", "http://blink-status.appspot.com/"]); - - /** Links of interest. */ - var kLinks = new Array(); - kLinks.push(["Useful Chromium URLs", "https://code.google.com/p/chromium/wiki/UsefulURLs"]); + kStatusPages.push(['chromium', 'https://chromium-status.appspot.com/']); + kStatusPages.push(['blink', 'http://blink-status.appspot.com/']); </script> <script type="text/javascript" src="functions.js"></script> + <script type="text/javascript" src="statuspageinfo.js"></script> + <script type="text/javascript" src="waterfallinfo.js"></script> + <script type="text/javascript" src="botinfo.js"></script> + <script type="text/javascript" src="buildinfo.js"></script> + <script type="text/javascript" src="failureinfo.js"></script> </head> <body onload="javascript:initialize();"> - <div id="sidebar"> - <div id="sidebar_contents"></div> + <div id="sidebar"> + <div id="sidebar-contents"></div> - <ul class="box"> - <li>Email <a href="mailto:chrome-troopers@google.com" target="_blank">chrome-troopers@google.com</a> about bot issues</li> - </ul> + <div class="box"> + Bot issues? E-mail:<br><a href="mailto:chrome-troopers@google.com">chrome-troopers@google.com</a> + </div> - <!-- Useful links. --> - <ul id="useful_links" class="box"> - </ul> + <!-- Useful links. --> + <ul class="box"> + <li> + <a href="https://code.google.com/p/chromium/wiki/UsefulURLs"> + Useful Chromium URLs + </a> + </li> + </ul> - <!-- Options. --> - <ul id="options" class="box"> - <li><input type="checkbox" checked id="checkbox_hide_stable" onclick="updateStatusHTML();">Hide steady green bots</input></li> - <li><input type="checkbox" id="checkbox_alert_steady_red" onclick="updateStatusHTML();">Alert if steady green bots go red</input></li> - <li>Filter: <input type="text" id="text_filter" onkeypress="filterUpdated();"></input></li> - </ul> + <!-- Options. --> + <ul id="options" class="box"> + <li><input type="checkbox" checked id="checkbox-hide-stable" onclick="updateStatusHTML();">Hide steady green bots</input></li> + <li><input type="checkbox" id="checkbox-alert-steady-red" onclick="updateStatusHTML();">Alert if steady green bots go red</input></li> + <li> + <input type="text" placeholder="Status message filter" id="text-filter" onkeypress="filterUpdated();"> + </input> + </li> + </ul> - <!-- Debugging stats. --> - <ul id="debugging" class="box"> - <li>Ticks until refresh: <span id="ticks_until_refresh"></span></li> - <li>Requests in flight: <span id="requests_in_flight"></span></li> - <li>Requests cached: <span id="requests_ignored"></span></li> - <li>Requests retried: <span id="requests_retried"></span></li> - </ul> - </div> - <table id="build_info"></table> - <table id="failure_info"></table> + <!-- Debugging stats. --> + <ul id="debugging" class="box"> + <li>Ticks until refresh: <span id="num-ticks-until-refresh"></span></li> + <li>Requests in flight: <span id="num-requests-in-flight"></span></li> + <li>Requests cached: <span id="num-requests-ignored"></span></li> + <li>Requests retried: <span id="num-requests-retried"></span></li> + </ul> + </div> + <table id="build-info"></table> + <table id="failure-info"></table> </body> </html> - diff --git a/tools/sheriffing/index_android.html b/tools/sheriffing/index_android.html new file mode 100644 index 0000000..b042da1 --- /dev/null +++ b/tools/sheriffing/index_android.html @@ -0,0 +1,128 @@ +<!DOCTYPE html> +<!-- +Copyright 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> +<html> + <head> + <meta http-equiv="cache-control" content="no-cache"> + <title>Android sheriffing dashboard</title> + + <base target="_blank" /> + <link href='http://fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'> + <link rel="stylesheet" href="style.css"> + + <script type="text/javascript"> + /** Watefall pages, ordered by importance. */ + var kBuilderPages = new Array(); + kBuilderPages.push(['Blink (blocks rolls into Chromium)', + 'http://build.chromium.org/p/chromium.webkit/', + false]); + kBuilderPages.push(['Chromium', + 'http://build.chromium.org/p/chromium/', + false]); + kBuilderPages.push(['Chromium Linux', + 'http://build.chromium.org/p/chromium.linux/', + false]); + kBuilderPages.push(['Chrome on Android ToT (blocks rolls)', + 'https://chromegw.corp.google.com/i/clank.tot/', + true]); + kBuilderPages.push(['Chrome on Android', + 'https://chromegw.corp.google.com/i/clank/', + true]); + kBuilderPages.push(['Chromium FYI', + 'http://build.chromium.org/p/chromium.fyi/', + false]); + kBuilderPages.push(['WebView ToT', + 'http://chromegw.corp.google.com/i/clank.webview.tot/', + true]); + kBuilderPages.push(['Chrome on Android: offical release', + 'http://master.chrome.corp.google.com:8000/', + true]); + kBuilderPages.push(['Chrome on Android: official release-tests', + 'https://chromegw.corp.google.com/i/clank.qa/', + true]); + kBuilderPages.push(['Chromium GPU', + 'http://build.chromium.org/p/chromium.gpu/', + false]); + + /** Waterfall open/closed status pages. */ + var kStatusPages = new Array(); + kStatusPages.push(['blink', 'http://blink-status.appspot.com/']); + kStatusPages.push(['chromium', 'https://chromium-status.appspot.com/']); + kStatusPages.push(['clank', 'https://c-status.appspot.com/']); + </script> + + <script type="text/javascript" src="functions.js"></script> + <script type="text/javascript" src="statuspageinfo.js"></script> + <script type="text/javascript" src="waterfallinfo.js"></script> + <script type="text/javascript" src="botinfo.js"></script> + <script type="text/javascript" src="buildinfo.js"></script> + <script type="text/javascript" src="failureinfo.js"></script> + + <script type="text/javascript"> + /** Override the original function to show all Android bots. */ + WaterfallInfo.prototype.shouldShowBot = function(builderName) { + return builderName.toLowerCase().indexOf('android') != -1; + } + </script> + </head> + <body onload="javascript:initialize();"> + <div id="sidebar"> + <div id="sidebar-contents"></div> + + <div class="box"> + Bot issues? E-mail:<br><a href="mailto:chrome-troopers@google.com">chrome-troopers@google.com</a> + </div> + + <!-- Useful links. --> + <ul class="box"> + <li> + <a href="https://docs.google.com/a/google.com/document/d/1NTUhIbC-86hHK-IkznPuBlko0XG5o0r3eghDYs0g7no/edit#"> + Sheriff status updates + </a> + </li> + <li> + <a href="https://sites.google.com/a/google.com/clank/engineering/sheriffs"> + Sheriffing Chrome on Android + </a> + </li> + <li> + <a href="https://sites.google.com/a/google.com/clank/engineering/gardening-chromium"> + Gardening Chrome on Android + </a> + </li> + <li> + <a href="http://build.chromium.org/p/tryserver.chromium/waterfall"> + Try bots + </a> + </li> + <li> + <a href="https://code.google.com/p/chromium/wiki/UsefulURLs"> + Useful Chromium URLs + </a> + </li> + </ul> + + <!-- Options. --> + <ul id="options" class="box"> + <li><input type="checkbox" id="checkbox-hide-stable" onclick="updateStatusHTML();">Hide steady green bots</input></li> + <li><input type="checkbox" id="checkbox-alert-steady-red" onclick="updateStatusHTML();">Alert if steady green bots break</input></li> + <li> + <input type="text" placeholder="Status message filter" id="text-filter" onkeypress="filterUpdated();"> + </input> + </li> + </ul> + + <!-- Debugging stats. --> + <ul id="debugging" class="box"> + <li>Ticks until refresh: <span id="num-ticks-until-refresh"></span></li> + <li>Requests in flight: <span id="num-requests-in-flight"></span></li> + <li>Requests cached: <span id="num-requests-ignored"></span></li> + <li>Requests retried: <span id="num-requests-retried"></span></li> + </ul> + </div> + <table id="build-info"></table> + </body> +</html> diff --git a/tools/sheriffing/statuspageinfo.js b/tools/sheriffing/statuspageinfo.js new file mode 100644 index 0000000..2f11682 --- /dev/null +++ b/tools/sheriffing/statuspageinfo.js @@ -0,0 +1,61 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** Information about a particular status page. */ +function StatusPageInfo(statusPageName, statusPageUrl) { + this.date = ''; + this.inFlight = 0; + this.jsonUrl = statusPageUrl + 'current?format=json'; + this.message = ''; + this.name = statusPageName; + this.state = ''; + this.url = statusPageUrl; +} + +/** Send and parse an asynchronous request to get a repo status JSON. */ +StatusPageInfo.prototype.requestJson = function() { + if (this.inFlight) return; + + this.inFlight++; + gNumRequestsInFlight++; + + var statusPageInfo = this; + var request = new XMLHttpRequest(); + request.open('GET', this.jsonUrl, true); + request.onreadystatechange = function() { + if (request.readyState == 4 && request.status == 200) { + statusPageInfo.inFlight--; + gNumRequestsInFlight--; + + var statusPageJson = JSON.parse(request.responseText); + statusPageInfo.date = statusPageJson.date; + statusPageInfo.message = statusPageJson.message; + statusPageInfo.state = statusPageJson.general_state; + } + }; + request.send(null); +}; + +/** Creates HTML displaying the status. */ +StatusPageInfo.prototype.createHtml = function() { + var linkElement = document.createElement('a'); + linkElement.href = this.url; + linkElement.innerHTML = this.name; + + var statusElement = document.createElement('li'); + statusElement.appendChild(linkElement); + + var dateElement = document.createElement('li'); + dateElement.innerHTML = this.date; + + var messageElement = document.createElement('li'); + messageElement.innerHTML = this.message; + + var boxElement = document.createElement('ul'); + boxElement.className = 'box ' + this.state; + boxElement.appendChild(statusElement); + boxElement.appendChild(dateElement); + boxElement.appendChild(messageElement); + return boxElement; +}; diff --git a/tools/sheriffing/style.css b/tools/sheriffing/style.css index 041cd4d..f2ba657 100644 --- a/tools/sheriffing/style.css +++ b/tools/sheriffing/style.css @@ -21,47 +21,54 @@ a:hover { text-decoration: underline; } +input#text-filter { + width: 100%; +} + /* Waterfall styles */ -#build_info { +#build-info { background-color: white; margin-left: 250px; - border-spacing: 1px; + border-spacing: 0px; } -#build_info td { +#build-info td { padding: 0.2em 0.5em 0.2em 0.5em; } -#failure_info { +#failure-info { background-color: white; margin-left: 250px; - border-spacing: 1px; + border-spacing: 0px; } -#failure_info td { +#failure-info td { padding: 0.2em 0.5em 0.2em 0.5em; } -.failure_range { +.failure-range { white-space: nowrap; } -.waterfall_name { +.section-header { font-weight: bold; background: #eeeeee; border-top: 1px dotted #999999; + text-align: left; + padding: 0.2em 0.5em; } -.pending_count { +.pending-count { width: 2.5em; min-width: 2.5em; max-width: 3.5em; display: inline-block; text-align: right; color: #cccccc; + background: transparent; } -.build_identifier { +.build-identifier { width: 3.5em; min-width: 3.5em; max-width: 3.5em; @@ -70,16 +77,16 @@ a:hover { color: blue; } -.build_letter { +.build-letter { color: blue; } -.build_status { +.build-status { padding-left: 0.5em; color: black; } -.in_flight { +.in-flight { background-color: #99ffff; } @@ -107,7 +114,8 @@ a:hover { background: white; margin: 0.4em; padding: 1em; - border-bottom: 1px solid #999999; + box-shadow: 0px 2px 2px #888888; + border-radius: 0.25em; } .box li { @@ -148,6 +156,10 @@ a:hover { background: #cccccc; } +.warnings { + background: #ffc343; +} + /* Tree statuses. */ .open { background: #ccff99; diff --git a/tools/sheriffing/waterfallinfo.js b/tools/sheriffing/waterfallinfo.js new file mode 100644 index 0000000..2dccd58 --- /dev/null +++ b/tools/sheriffing/waterfallinfo.js @@ -0,0 +1,134 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** Information about a particular waterfall. */ +function WaterfallInfo(waterfallData) { + var waterfallName = waterfallData[0]; + var waterfallUrl = waterfallData[1]; + var waterfallShowsAllBots = waterfallData[2]; + + // Create a table cell that acts as a header for its bot section. + var linkElement = document.createElement('a'); + linkElement.href = waterfallUrl; + linkElement.innerHTML = waterfallName; + var thElement = document.createElement('th'); + thElement.colSpan = 15; + thElement.className = 'section-header'; + thElement.appendChild(linkElement); + + this.botInfo = {}; + this.inFlight = 0; + this.name = waterfallName; + this.showsAllBots = waterfallShowsAllBots; + this.thElement = thElement; + this.timeLastRequested = 0; + this.rootJsonUrl = waterfallUrl + 'json/'; + this.url = waterfallUrl; +} + +/** Send an asynchronous request to get the main waterfall's JSON. */ +WaterfallInfo.prototype.requestJson = function() { + if (this.inFlight) { + var elapsed = new Date().getTime() - this.timeLastRequested; + if (elapsed < MAX_MILLISECONDS_TO_WAIT) return; + + // A response was not received in a reasonable timeframe. Try again. + this.inFlight--; + gNumRequestsInFlight--; + gNumRequestsRetried++; + } + + this.inFlight++; + this.timeLastRequested = new Date().getTime(); + gNumRequestsInFlight++; + + // Create the request and send it off. + var waterfallInfo = this; + var url = this.url + 'json/builders/'; + var request = new XMLHttpRequest(); + request.open('GET', url, true); + request.onreadystatechange = function() { + if (request.readyState == 4 && request.status == 200) { + waterfallInfo.parseJSON(JSON.parse(request.responseText)); + } + }; + request.send(null); +}; + +/** Parse out the data received about the waterfall. */ +WaterfallInfo.prototype.parseJSON = function(buildersJson) { + this.inFlight--; + gNumRequestsInFlight--; + + // Go through each builder on the waterfall and get the latest status. + var builderNames = Object.keys(buildersJson); + for (var i = 0; i < builderNames.length; ++i) { + var builderName = builderNames[i]; + + if (!this.showsAllBots && !this.shouldShowBot(builderName)) continue; + + // Prepare the bot info. + var builderJson = buildersJson[builderName]; + if (!this.botInfo[builderName]) { + this.botInfo[builderName] = new BotInfo(builderName, + builderJson.category); + } + this.botInfo[builderName].update(this.rootJsonUrl, builderJson); + gWaterfallDataIsDirty = true; + } +}; + +/** Override this function to filter out particular bots. */ +WaterfallInfo.prototype.shouldShowBot = function(builderName) { + return true; +}; + +/** Updates the HTML. */ +WaterfallInfo.prototype.updateWaterfallStatusHTML = function() { + var table = document.getElementById('build-info'); + + // Point at the waterfall. + var headerCell = this.thElement; + headerCell.className = + 'section-header' + (this.inFlight > 0 ? ' in-flight' : ''); + var headerRow = table.insertRow(-1); + headerRow.appendChild(headerCell); + + // Print out useful bits about the bots. + var botNames = sortBotNamesByCategory(this.botInfo); + for (var i = 0; i < botNames.length; ++i) { + var botName = botNames[i]; + var botInfo = this.botInfo[botName]; + var waterfallBaseUrl = this.url + 'builders/'; + + var botRowElement = botInfo.createHtml(waterfallBaseUrl); + + // Determine whether we should apply keyword filter. + var filter = document.getElementById('text-filter').value.trim(); + if (filter.length > 0) { + var keywords = filter.split(' '); + var buildNumbers = Object.keys(botInfo.builds); + var matchesFilter = false; + + for (var x = 0; x < buildNumbers.length && !matchesFilter; ++x) { + var buildStatus = botInfo.builds[buildNumbers[x]].statusText; + for (var y = 0; y < keywords.length && !matchesFilter; ++y) { + if (buildStatus.indexOf(keywords[y]) >= 0) + matchesFilter = true; + } + } + + if (!matchesFilter) + continue; + } + + // If the user doesn't want to see completely green bots, hide it. + var shouldHideStable = + document.getElementById('checkbox-hide-stable').checked; + if (shouldHideStable && botInfo.isSteadyGreen) + continue; + + table.appendChild(botRowElement); + } +}; |