Release the Settings API.
[chromium-blink-merge.git] / tools / sheriffing / functions.js
blob9c16744fa7a3d602fc2598c897d139dfdc7ecbcf
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /** Random constants. */
6 var kMaxBuildStatusLength = 50;
7 var kTicksBetweenRefreshes = 60;
8 var kHistoricBuilds = 4;
9 var kMaxMillisecondsToWait = 5 * 60 * 1000;
11 var g_ticks_until_refresh = kTicksBetweenRefreshes;
13 /** Parsed JSON data. */
14 var g_waterfall_data = {};
15 var g_status_data = {};
16 var g_waterfall_data_dirty = true;
17 var g_css_loading_rule = null;
19 /** Statistics. */
20 var g_requests_in_flight = 0;
21 var g_requests_ignored = 0;
22 var g_requests_retried = 0;
23 var g_start_time = 0;
25 /** Cut the status message down so it doesn't hog the whole screen. */
26 function truncateStatusText(text) {
27 if (text.length > kMaxBuildStatusLength) {
28 return text.substr(0, kMaxBuildStatusLength) + '...';
30 return text;
33 /** Information about a particular status page. */
34 function StatusPageInfo(status_page_url) {
35 this.url = status_page_url + 'current?format=json';
36 this.in_flight = 0;
37 this.message = '';
38 this.date = '';
39 this.state = '';
42 /** Information about a particular waterfall. */
43 function WaterfallInfo(waterfall_name, waterfall_url) {
44 var waterfall_link_element = document.createElement('a');
45 waterfall_link_element.href = waterfall_url;
46 waterfall_link_element.target = '_blank';
47 waterfall_link_element.innerHTML = waterfall_name;
49 var td_element = document.createElement('td');
50 td_element.colSpan = 15;
51 td_element.appendChild(waterfall_link_element);
53 this.bot_info = {};
54 this.url = waterfall_url;
55 this.in_flight = 0;
56 this.td_element = td_element;
57 this.last_update = 0;
60 /** Information about a particular bot. */
61 function BotInfo(category) {
62 // Sometimes the categories have digits at the beginning for ordering the
63 // category. Chop it off.
64 if (category && category.length > 0) {
65 var splitter_index = category.indexOf('|');
66 if (splitter_index != -1) {
67 category = category.substr(0, splitter_index);
71 this.in_flight = 0;
72 this.builds = {};
73 this.category = category;
74 this.build_numbers_running = null;
75 this.is_steady_green = false;
76 this.state = '';
77 this.num_updates_offline = 0;
80 /** Get the revisions involved in a build. */
81 function getRevisionRange(json) {
82 var lowest = parseInt(json.sourceStamp.changes[0].revision, 10);
83 var highest = parseInt(json.sourceStamp.changes[0].revision, 10);
84 for (var i = 1; i < json.sourceStamp.changes.length; ++i) {
85 var rev = parseInt(json.sourceStamp.changes[i].revision, 10);
86 if (rev < lowest)
87 lowest = rev;
88 if (rev > highest)
89 highest = rev;
91 return [lowest, highest];
94 /** Information about a particular build. */
95 function BuildInfo(json) {
96 // Parse out the status message for the build.
97 var status_text;
98 if (json.currentStep) {
99 status_text = 'running ' + json.currentStep.name;
100 } else {
101 status_text = json.text.join(' ');
104 var truncated_status_text = truncateStatusText(status_text);
106 // Determine what state the build is in.
107 var state;
108 if (status_text.indexOf('exception') != -1) {
109 state = 'exception';
110 } else if (status_text.indexOf('running') != -1) {
111 state = 'running';
112 } else if (status_text.indexOf('successful') != -1) {
113 state = 'success';
114 } else if (status_text.indexOf('failed') != -1) {
115 state = 'failed';
116 } else if (status_text.indexOf('offline') != -1) {
117 state = 'offline';
118 } else {
119 state = 'unknown';
122 if (state == 'failed') {
123 // Save data about failed tests and blamelist so we can do intersections.
124 var failures = [];
125 var revision_range = getRevisionRange(json);
126 var bot_name = json.builderName;
127 for (var i = 0; i < json.steps.length; ++i) {
128 var step = json.steps[i];
129 var binary_name = step.name;
130 if (step.results[0] != 0) { // Failed.
131 for (var j = 0; j < step.logs.length; ++j) {
132 var log = step.logs[j];
133 if (log[0] == 'stdio')
134 continue;
135 var test_name = log[0];
136 failures.push([bot_name, binary_name, test_name, revision_range]);
140 this.failures = failures;
141 } else {
142 this.failures = null;
145 this.status_text = status_text;
146 this.truncated_status_text = truncated_status_text;
147 this.state = state;
150 /** Send and parse an asynchronous request to get a repo status JSON. */
151 function requestStatusPageJSON(repo_name) {
152 if (g_status_data[repo_name].in_flight) return;
154 var request = new XMLHttpRequest();
155 request.open('GET', g_status_data[repo_name].url, true);
156 request.onreadystatechange = function() {
157 if (request.readyState == 4 && request.status == 200) {
158 g_status_data[repo_name].in_flight--;
159 g_requests_in_flight--;
161 status_page_json = JSON.parse(request.responseText);
162 g_status_data[repo_name].message = status_page_json.message;
163 g_status_data[repo_name].state = status_page_json.general_state;
164 g_status_data[repo_name].date = status_page_json.date;
167 g_status_data[repo_name].in_flight++;
168 g_requests_in_flight++;
169 request.send(null);
172 /** Send an asynchronous request to get the main waterfall's JSON. */
173 function requestWaterfallJSON(waterfall_info) {
174 if (waterfall_info.in_flight) {
175 var elapsed = new Date().getTime() - waterfall_info.last_update;
176 if (elapsed < kMaxMillisecondsToWait) return;
178 waterfall_info.in_flight--;
179 g_requests_in_flight--;
180 g_requests_retried++;
183 var waterfall_url = waterfall_info.url;
184 var url = waterfall_url + 'json/builders/';
186 var request = new XMLHttpRequest();
187 request.open('GET', url, true);
188 request.onreadystatechange = function() {
189 if (request.readyState == 4 && request.status == 200) {
190 waterfall_info.in_flight--;
191 g_requests_in_flight--;
192 parseWaterfallJSON(JSON.parse(request.responseText), waterfall_info);
195 waterfall_info.in_flight++;
196 g_requests_in_flight++;
197 waterfall_info.last_update = new Date().getTime();
198 request.send(null);
201 /** Update info about the bot, including info about the builder's builds. */
202 function requestBuilderJSON(waterfall_info,
203 root_json_url,
204 builder_json,
205 builder_name) {
206 // Prepare the bot info.
207 if (!waterfall_info.bot_info[builder_name]) {
208 waterfall_info.bot_info[builder_name] = new BotInfo(builder_json.category);
210 var bot_info = waterfall_info.bot_info[builder_name];
212 // Update the builder's state.
213 bot_info.build_numbers_running = builder_json.currentBuilds;
214 bot_info.num_pending_builds = builder_json.pendingBuilds;
215 bot_info.state = builder_json.state;
216 if (bot_info.state == 'offline') {
217 bot_info.num_updates_offline++;
218 console.log(builder_name + ' has been offline for ' +
219 bot_info.num_updates_offline + ' update(s) in a row');
220 } else {
221 bot_info.num_updates_offline = 0;
224 // Send an asynchronous request to get info about the builder's last builds.
225 var last_completed_build_number =
226 guessLastCompletedBuildNumber(builder_json, bot_info);
227 if (last_completed_build_number) {
228 for (var build_number = last_completed_build_number - kHistoricBuilds;
229 build_number <= last_completed_build_number;
230 ++build_number) {
231 if (build_number < 0) continue;
232 requestBuildJSON(waterfall_info,
233 builder_name,
234 root_json_url,
235 builder_json,
236 build_number);
241 /** Given a builder's JSON, guess the last known build that it completely
242 * finished. */
243 function guessLastCompletedBuildNumber(builder_json, bot_info) {
244 // The cached builds line doesn't store every build so we can't just take the
245 // last number.
246 var build_numbers_running = builder_json.currentBuilds;
247 bot_info.build_numbers_running = build_numbers_running;
249 var build_numbers_cached = builder_json.cachedBuilds;
250 if (build_numbers_running && build_numbers_running.length > 0) {
251 max_build_number =
252 Math.max(build_numbers_cached[build_numbers_cached.length - 1],
253 build_numbers_running[build_numbers_running.length - 1]);
255 var completed_build_number = max_build_number;
256 while (build_numbers_running.indexOf(completed_build_number) != -1 &&
257 completed_build_number >= 0) {
258 completed_build_number--;
260 return completed_build_number;
261 } else {
262 // Nothing's currently building. Just assume the last cached build is
263 // correct.
264 return build_numbers_cached[build_numbers_cached.length - 1];
268 /** Get the data for a particular build. */
269 function requestBuildJSON(waterfall_info,
270 builder_name,
271 root_json_url,
272 builder_json,
273 build_number) {
274 var bot_info = waterfall_info.bot_info[builder_name];
276 // Check if we already have the data on this build.
277 if (bot_info.builds[build_number] &&
278 bot_info.builds[build_number].state != 'running') {
279 g_requests_ignored++;
280 return;
283 // Grab it.
284 var url =
285 root_json_url + 'builders/' + builder_name + '/builds/' + build_number;
287 var request = new XMLHttpRequest();
288 request.open('GET', url, true);
289 request.onreadystatechange = function() {
290 if (request.readyState == 4 && request.status == 200) {
291 bot_info.in_flight--;
292 g_requests_in_flight--;
294 var json = JSON.parse(request.responseText);
295 bot_info.builds[build_number] = new BuildInfo(json);
297 checkBotIsSteadyGreen(builder_name, bot_info);
298 g_waterfall_data_dirty = true;
302 bot_info.in_flight++;
303 g_requests_in_flight++;
304 request.send(null);
307 function parseWaterfallJSON(builders_json, waterfall_info) {
308 var root_json_url = waterfall_info.url + 'json/';
310 // Go through each builder on the waterfall and get the latest status.
311 var builder_names = Object.keys(builders_json);
312 for (var i = 0; i < builder_names.length; ++i) {
313 var builder_name = builder_names[i];
315 // TODO: Any filtering here.
317 var builder_json = builders_json[builder_name];
318 requestBuilderJSON(
319 waterfall_info, root_json_url, builder_json, builder_name);
320 g_waterfall_data_dirty = true;
324 /** Queries all of the servers for their latest statuses. */
325 function queryServersForInfo() {
326 var waterfall_names = Object.keys(g_waterfall_data);
327 for (var index = 0; index < waterfall_names.length; ++index) {
328 var name = waterfall_names[index];
329 var waterfall_info = g_waterfall_data[name];
330 requestWaterfallJSON(waterfall_info);
333 var status_page_names = Object.keys(g_status_data);
334 for (var index = 0; index < status_page_names.length; ++index) {
335 requestStatusPageJSON(status_page_names[index]);
340 * Return the range of intersection between the two lists. Ranges are sets of
341 * CLs, so it's inclusive on both ends.
343 function rangeIntersection(a, b) {
344 if (a[0] > b[1])
345 return null;
346 if (a[1] < b[0])
347 return null;
348 // We know they intersect, result is the larger lower bound to the smaller
349 // upper bound.
350 return [Math.max(a[0], b[0]), Math.min(a[1], b[1])];
353 function findIntersections(test_data) {
354 // Input is a object mapping botname => [low, high].
356 // Result:
357 // [
358 // [ low0, high0 ], [ bot1, bot2, bot3 ],
359 // [ low1, high1 ], [ bot4, ... ],
360 // ...,
361 // ]
363 var keys = Object.keys(test_data);
364 var intersections = [];
365 var bot_name = keys[0];
366 var range = test_data[bot_name];
367 intersections.push([range, [bot_name]]);
368 for (var i = 1; i < keys.length; ++i) {
369 bot_name = keys[i];
370 range = test_data[bot_name];
371 var intersected_some = false;
372 for (var j = 0; j < intersections.length; ++j) {
373 var intersect = rangeIntersection(intersections[j][0], range);
374 if (intersect) {
375 intersections[j][0] = intersect;
376 intersections[j][1].push(bot_name);
377 intersected_some = true;
378 break;
381 if (!intersected_some) {
382 intersections.push([range, [bot_name]]);
386 return intersections;
389 /** Flatten out the list of tests and sort them by the binary/test name. */
390 function flattenAndSortTests(ranges_by_test) {
391 var ranges_by_test_names = Object.keys(ranges_by_test);
392 var flat = [];
393 for (var i = 0; i < ranges_by_test_names.length; ++i) {
394 var name = ranges_by_test_names[i];
395 flat.push([name, ranges_by_test[name]]);
398 flat.sort(function(a, b) {
399 if (a[0] < b[0]) return -1;
400 if (a[0] > b[0]) return 1;
401 return 0;
404 return flat;
408 * Build the HTML table row for a test failure. |test| is [ name, test_data ].
409 * |test_data| contains ranges suitable for input to |findIntersections|.
411 function buildTestFailureTableRowHTML(test) {
412 var row = document.createElement('tr');
413 var binary_cell = row.insertCell(-1);
414 var name_parts = test[0].split('-');
415 binary_cell.innerHTML = name_parts[0];
416 binary_cell.className = 'category';
417 var flakiness_link = document.createElement('a');
418 flakiness_link.href =
419 'http://test-results.appspot.com/dashboards/' +
420 'flakiness_dashboard.html#testType=' +
421 name_parts[0] + '&tests=' + name_parts[1];
422 flakiness_link.target = '_blank';
423 flakiness_link.innerHTML = name_parts[1];
424 row.appendChild(flakiness_link);
426 var intersections = findIntersections(test[1]);
427 for (var j = 0; j < intersections.length; ++j) {
428 var intersection = intersections[j];
429 var range = row.insertCell(-1);
430 range.className = 'failure_range';
431 var low = intersection[0][0];
432 var high = intersection[0][1];
433 var url =
434 'http://build.chromium.org/f/chromium/perf/dashboard/ui/' +
435 'changelog.html?url=%2Ftrunk%2Fsrc&range=' +
436 low + '%3A' + high + '&mode=html';
437 range.innerHTML = '<a target="_blank" href="' + url + '">' + low +
438 ' - ' + high + '</a>: ' +
439 truncateStatusText(intersection[1].join(', '));
441 return row;
445 /** Updates the correlations contents. */
446 function updateCorrelationsHTML() {
447 // The logic here is to try to narrow blamelists by using information across
448 // bots. If a particular test has failed on multiple different
449 // configurations, there's a good chance that it has the same root cause, so
450 // calculate the intersection of blamelists of the first time it failed on
451 // each builder.
453 var all_failures = [];
454 for (var i = 0; i < kBuilderPages.length; ++i) {
455 var waterfall_name = kBuilderPages[i][0];
456 var waterfall_info = g_waterfall_data[waterfall_name];
457 var bot_info = waterfall_info.bot_info;
458 var all_bot_names = Object.keys(bot_info);
459 for (var j = 0; j < all_bot_names.length; ++j) {
460 var bot_name = all_bot_names[j];
461 if (bot_info[bot_name].is_steady_green)
462 continue;
463 var builds = bot_info[bot_name].builds;
464 var build_names = Object.keys(builds);
465 for (var k = 0; k < build_names.length; ++k) {
466 var build = builds[build_names[k]];
467 if (build.failures) {
468 for (var l = 0; l < build.failures.length; ++l) {
469 all_failures.push(build.failures[l]);
476 // all_failures is now a list of lists, each containing:
477 // [ botname, binaryname, testname, [low_rev, high_rev] ].
479 var ranges_by_test = {};
480 for (var i = 0; i < all_failures.length; ++i) {
481 var failure = all_failures[i];
482 var bot_name = failure[0];
483 var binary_name = failure[1];
484 var test_name = failure[2];
485 var range = failure[3];
486 if (binary_name.indexOf('steps') != -1)
487 continue;
488 if (test_name.indexOf('preamble') != -1)
489 continue;
490 var key = binary_name + '-' + test_name;
492 if (!ranges_by_test.hasOwnProperty(key))
493 ranges_by_test[key] = {};
494 // If there's no range, that's all we know.
495 if (!ranges_by_test[key].hasOwnProperty([bot_name])) {
496 ranges_by_test[key][bot_name] = range;
497 } else {
498 // Otherwise, track only the lowest range for this bot (we only want
499 // when it turned red, not the whole range that it's been red for).
500 if (range[0] < ranges_by_test[key][bot_name][0]) {
501 ranges_by_test[key][bot_name] = range;
506 var table = document.getElementById('failure_info');
507 while (table.rows.length > 0) {
508 table.deleteRow(-1);
511 var header_cell = document.createElement('td');
512 header_cell.colSpan = 15;
513 header_cell.innerHTML =
514 'test failures (ranges are blamelist intersections of first failure on ' +
515 'each bot of retrieved data. so, if a test has been failing for a ' +
516 'while, the range may be incorrect)';
517 header_cell.className = 'waterfall_name';
518 var header_row = table.insertRow(-1);
519 header_row.appendChild(header_cell);
521 var flat = flattenAndSortTests(ranges_by_test);
523 for (var i = 0; i < flat.length; ++i) {
524 table.appendChild(buildTestFailureTableRowHTML(flat[i]));
528 /** Updates the sidebar's contents. */
529 function updateSidebarHTML() {
530 var output = '';
532 // Buildbot info.
533 var status_names = Object.keys(g_status_data);
534 for (var i = 0; i < status_names.length; ++i) {
535 var status_name = status_names[i];
536 var status_info = g_status_data[status_name];
537 var status_url = kStatusPages[i][1];
539 output += '<ul class="box ' + status_info.state + '">';
540 output += '<li><a target="_blank" href="' + status_url + '">' +
541 status_name + '</a></li>';
542 output += '<li>' + status_info.date + '</li>';
543 output += '<li>' + status_info.message + '</li>';
544 output += '</ul>';
547 var div = document.getElementById('sidebar_contents');
548 div.innerHTML = output;
550 // Debugging stats.
551 document.getElementById('ticks_until_refresh').innerHTML =
552 g_ticks_until_refresh;
553 document.getElementById('requests_in_flight').innerHTML =
554 g_requests_in_flight;
555 document.getElementById('requests_ignored').innerHTML = g_requests_ignored;
556 document.getElementById('requests_retried').innerHTML = g_requests_retried;
559 /** Organizes all of the bots by category, then alphabetically within their
560 * categories. */
561 function sortBotNamesByCategory(bot_info) {
562 // Bucket all of the bots according to their category.
563 var all_bot_names = Object.keys(bot_info);
564 var bucketed_names = {};
565 for (var i = 0; i < all_bot_names.length; ++i) {
566 var bot_name = all_bot_names[i];
567 var category = bot_info[bot_name].category;
569 if (!bucketed_names[category]) bucketed_names[category] = [];
570 bucketed_names[category].push(bot_name);
573 // Alphabetically sort bots within their buckets, then append them to the
574 // current list.
575 var sorted_bot_names = [];
576 var all_categories = Object.keys(bucketed_names);
577 all_categories.sort();
578 for (var i = 0; i < all_categories.length; ++i) {
579 var category = all_categories[i];
580 var bucket_bots = bucketed_names[category];
581 bucket_bots.sort();
583 for (var j = 0; j < bucket_bots.length; ++j) {
584 sorted_bot_names.push(bucket_bots[j]);
588 return sorted_bot_names;
592 * Returns true IFF the last few builds are all green.
593 * Also alerts the user if the last completed build goes red after being
594 * steadily green (if desired).
596 function checkBotIsSteadyGreen(bot_name, bot_info) {
597 var ascending_build_numbers = Object.keys(bot_info.builds);
598 ascending_build_numbers.sort();
600 for (var j = ascending_build_numbers.length - 1;
601 j >= 0 && j >= ascending_build_numbers.length - 1 - kHistoricBuilds;
602 --j) {
603 var build_number = ascending_build_numbers[j];
604 if (!build_number) continue;
606 var build_info = bot_info.builds[build_number];
607 if (!build_info) continue;
609 // Running builds throw heuristics out of whack. Keep the bot visible.
610 if (build_info.state == 'running') return false;
612 if (build_info.state != 'success') {
613 if (bot_info.is_steady_green &&
614 document.getElementById('checkbox_alert_steady_red').checked) {
615 alert(bot_name +
616 ' has failed for the first time in a while. Consider looking.');
618 bot_info.is_steady_green = false;
619 return false;
623 bot_info.is_steady_green = true;
624 return true;
627 /** Update all the waterfall data. */
628 function updateStatusHTML() {
629 var table = document.getElementById('build_info');
630 while (table.rows.length > 0) {
631 table.deleteRow(-1);
634 for (var i = 0; i < kBuilderPages.length; ++i) {
635 var waterfall_name = kBuilderPages[i][0];
636 var waterfall_info = g_waterfall_data[waterfall_name];
637 updateWaterfallStatusHTML(waterfall_name, waterfall_info);
641 /** Marks the waterfall data as dirty due to updated filter. */
642 function filterUpdated() {
643 g_waterfall_data_dirty = true;
646 /** Creates a table cell for a particular build number. */
647 function createBuildNumberCellHTML(build_url,
648 build_number,
649 full_status_text,
650 truncated_status_text,
651 build_state) {
652 // Create a link to the build results.
653 var link_element = document.createElement('a');
654 link_element.href = build_url;
655 link_element.target = '_blank';
657 // Display either the build number (for the last completed build), or show the
658 // status of the step.
659 var build_identifier_element = document.createElement('span');
660 if (build_number) {
661 build_identifier_element.className = 'build_identifier';
662 build_identifier_element.innerHTML = build_number;
663 } else {
664 build_identifier_element.className = 'build_letter';
665 build_identifier_element.innerHTML = build_state.toUpperCase()[0];
667 link_element.appendChild(build_identifier_element);
669 // Show the status of the build, in truncated form so it doesn't take up the
670 // whole screen.
671 if (truncated_status_text) {
672 var status_span_element = document.createElement('span');
673 status_span_element.className = 'build_status';
674 status_span_element.innerHTML = truncated_status_text;
675 link_element.appendChild(status_span_element);
678 // Tack the cell onto the end of the row.
679 var build_number_cell = document.createElement('td');
680 build_number_cell.className = build_state;
681 build_number_cell.title = full_status_text;
682 build_number_cell.appendChild(link_element);
684 return build_number_cell;
687 /** Updates the HTML for a particular waterfall. */
688 function updateWaterfallStatusHTML(waterfall_name, waterfall_info) {
689 var table = document.getElementById('build_info');
691 // Point at the waterfall.
692 var header_cell = waterfall_info.td_element;
693 header_cell.className =
694 'waterfall_name' + (waterfall_info.in_flight > 0 ? ' in_flight' : '');
695 var header_row = table.insertRow(-1);
696 header_row.appendChild(header_cell);
698 // Print out useful bits about the bots.
699 var bot_names = sortBotNamesByCategory(waterfall_info.bot_info);
700 for (var i = 0; i < bot_names.length; ++i) {
701 var bot_name = bot_names[i];
702 var bot_info = waterfall_info.bot_info[bot_name];
703 var bot_row = document.createElement('tr');
705 // Insert a cell for the bot category. Chop off any numbers used for
706 // sorting.
707 var category_cell = bot_row.insertCell(-1);
708 var category = bot_info.category;
709 if (category && category.length > 0 && category[0] >= '0' &&
710 category[0] <= '9') {
711 category = category.substr(1, category.length);
713 category_cell.innerHTML = category;
714 category_cell.className = 'category';
716 // Insert a cell for the bot name.
717 var bot_cell = bot_row.insertCell(-1);
718 var builder_url = waterfall_info.url + 'builders/' + bot_name;
719 var bot_link =
720 '<a target="_blank" href="' + builder_url + '">' + bot_name + '</a>';
721 bot_cell.innerHTML = bot_link;
722 bot_cell.className =
723 'bot_name' + (bot_info.in_flight > 0 ? ' in_flight' : '');
725 // Display information on the builds we have.
726 // This assumes that the build number always increases, but this is a bad
727 // assumption since
728 // builds get parallelized.
729 var build_numbers = Object.keys(bot_info.builds);
730 build_numbers.sort();
732 if (bot_info.num_pending_builds) {
733 var pending_cell = document.createElement('span');
734 pending_cell.className = 'pending_count';
735 pending_cell.innerHTML = '+' + bot_info.num_pending_builds;
736 bot_row.appendChild(pending_cell);
737 } else {
738 bot_row.insertCell(-1);
741 if (bot_info.build_numbers_running &&
742 bot_info.build_numbers_running.length > 0) {
743 // Display the number of the highest numbered running build.
744 bot_info.build_numbers_running.sort();
745 var length = bot_info.build_numbers_running.length;
746 var build_number = bot_info.build_numbers_running[length - 1];
747 var build_url = builder_url + '/builds/' + build_number;
748 var build_number_cell = createBuildNumberCellHTML(
749 build_url, build_number, null, null, 'running');
750 bot_row.appendChild(build_number_cell);
751 } else if (bot_info.state == 'offline' &&
752 bot_info.num_updates_offline >= 3) {
753 // The bot's supposedly offline. Wait a few updates to see since a
754 // builder can be marked offline in between builds and during reboots.
755 var build_number_cell = document.createElement('td');
756 build_number_cell.className = bot_info.state + ' build_identifier';
757 build_number_cell.innerHTML = 'offline';
758 bot_row.appendChild(build_number_cell);
759 } else {
760 bot_row.insertCell(-1);
763 // Display the last few builds.
764 for (var j = build_numbers.length - 1;
765 j >= 0 && j >= build_numbers.length - 1 - kHistoricBuilds;
766 --j) {
767 var build_number = build_numbers[j];
768 if (!build_number) continue;
770 var build_info = bot_info.builds[build_number];
771 if (!build_info) continue;
773 // Create and append the cell.
774 var is_last_build = (j == build_numbers.length - 1);
775 var build_url = builder_url + '/builds/' + build_number;
776 var status_text_full =
777 'Build ' + build_number + ':\n' + build_info.status_text;
778 var build_number_cell = createBuildNumberCellHTML(
779 build_url,
780 is_last_build ? build_number : null,
781 status_text_full,
782 is_last_build ? build_info.truncated_status_text : null,
783 build_info.state);
784 bot_row.appendChild(build_number_cell);
787 // Determine whether we should apply keyword filter.
788 var filter = document.getElementById('text_filter').value.trim();
789 if (filter.length > 0) {
790 var keywords = filter.split(' ');
791 var build_numbers = Object.keys(bot_info.builds);
792 var matches_filter = false;
793 console.log(bot_info);
795 for (var x = 0; x < build_numbers.length && !matches_filter; ++x) {
796 var build_status = bot_info.builds[build_numbers[x]].status_text;
797 console.log(build_status);
799 for (var y = 0; y < keywords.length && !matches_filter; ++y) {
800 if (build_status.indexOf(keywords[y]) >= 0)
801 matches_filter = true;
805 if (!matches_filter)
806 continue;
809 // If the user doesn't want to see completely green bots, hide it.
810 var should_hide_stable =
811 document.getElementById('checkbox_hide_stable').checked;
812 if (should_hide_stable && bot_info.is_steady_green)
813 continue;
815 table.appendChild(bot_row);
819 /** Update the page content. */
820 function updateContent() {
821 if (--g_ticks_until_refresh <= 0) {
822 g_ticks_until_refresh = kTicksBetweenRefreshes;
823 queryServersForInfo();
826 // Redraw the page content.
827 if (g_waterfall_data_dirty) {
828 g_waterfall_data_dirty = false;
829 updateStatusHTML();
830 updateCorrelationsHTML();
832 updateSidebarHTML();
835 /** Initialize all the things. */
836 function initialize() {
837 var g_start_time = new Date().getTime();
839 // Initialize the waterfall pages.
840 for (var i = 0; i < kBuilderPages.length; ++i) {
841 g_waterfall_data[kBuilderPages[i][0]] =
842 new WaterfallInfo(kBuilderPages[i][0], kBuilderPages[i][1]);
845 // Initialize the status pages.
846 for (var i = 0; i < kStatusPages.length; ++i) {
847 g_status_data[kStatusPages[i][0]] = new StatusPageInfo(kStatusPages[i][1]);
850 // Set up the useful links HTML in the sidebar.
851 var useful_links_ul = document.getElementById('useful_links');
852 for (var i = 0; i < kLinks.length; ++i) {
853 var link_html = '<a target="_blank" href="' + kLinks[i][1] + '">' +
854 kLinks[i][0] + '</a>';
855 var li_element = document.createElement('li');
856 li_element.innerHTML = link_html;
857 useful_links_ul.appendChild(li_element);
860 // Kick off the main loops.
861 queryServersForInfo();
862 updateStatusHTML();
863 setInterval('updateContent()', 1000);