summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--tools/sheriffing/botinfo.js227
-rw-r--r--tools/sheriffing/buildinfo.js137
-rw-r--r--tools/sheriffing/failureinfo.js191
-rw-r--r--tools/sheriffing/functions.js854
-rw-r--r--tools/sheriffing/index.html85
-rw-r--r--tools/sheriffing/index_android.html128
-rw-r--r--tools/sheriffing/statuspageinfo.js61
-rw-r--r--tools/sheriffing/style.css40
-rw-r--r--tools/sheriffing/waterfallinfo.js134
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);
+ }
+};