diff options
-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); + } +}; |