OLD | NEW |
| (Empty) |
1 <html> | |
2 <head> | |
3 <title>Skia Buildstep Dashboard</title> | |
4 <link rel="icon" href="favicon.ico"> | |
5 <script language="JavaScript"> | |
6 "use strict"; | |
7 | |
8 var DEFAULT_MASTER_FILTER = "*"; | |
9 var DEFAULT_BUILDER_FILTER = "*"; | |
10 var DEFAULT_BUILDSTEP_FILTER = "*"; | |
11 var DEFAULT_TIME_PERIOD = 24*60*60; // 24 hours in seconds. | |
12 | |
13 // Column indices for grouping Graphite data. | |
14 var GROUP_NODE_BUILDER = 2; | |
15 var GROUP_NODE_BUILDSTEP = 3; | |
16 | |
17 // We hard-code this giant interval to be passed to Graphite's summarize | |
18 // function to ensure that the data ends up grouped into one big bucket, no | |
19 // matter how we set the time period. | |
20 var SUMMARIZE_INTERVAL = "10year"; | |
21 | |
22 // Timestamp of when we started loading data. Used to compute load time. | |
23 var loadStart = null; | |
24 | |
25 // Used to store information about what data is currently being loaded. | |
26 var currentlyLoadingData = null; | |
27 | |
28 // Metrics to load from Graphite. | |
29 // Takes the form: [[metric_name, merge_function], ...] | |
30 var metrics = [["success", "sum"], | |
31 ["failure", "sum"], | |
32 ["duration", "avg"]]; | |
33 | |
34 // Metrics to derive from the loaded metrics above. We display these in the | |
35 // table. Takes the form: [[metric_name, derivation_fn], ...], where | |
36 // derivation_fn is a function which takes as a parameter a dictionary | |
37 // containing values for all of the above metrics for a given buildStep and | |
38 // returns a value, which may be null. | |
39 var derivedMetrics = [ | |
40 ["Duration", function(stepData) { | |
41 if (stepData["duration"] == undefined) { | |
42 return null; | |
43 } | |
44 return stepData["duration"]; | |
45 }], | |
46 ["Failure Rate", function(stepData) { | |
47 if (stepData["success"] == undefined || | |
48 stepData["failure"] == undefined) { | |
49 return null; | |
50 } | |
51 var success = parseFloat(stepData["success"]); | |
52 var failure = parseFloat(stepData["failure"]); | |
53 var totalRuns = success + failure; | |
54 var failureRate = failure / totalRuns; | |
55 if (totalRuns <= 0) { | |
56 return null; | |
57 } | |
58 return failureRate; | |
59 }], | |
60 ["Total Runs", function(stepData) { | |
61 if (stepData["success"] == undefined || | |
62 stepData["failure"] == undefined) { | |
63 return null; | |
64 } | |
65 var success = parseFloat(stepData["success"]); | |
66 var failure = parseFloat(stepData["failure"]); | |
67 return success + failure; | |
68 }], | |
69 ]; | |
70 | |
71 // Use the second data column as the default sort index, since that typically | |
72 // indicates the most important column which isn't a label. | |
73 var defaultSortIndex = 1; | |
74 | |
75 /** | |
76 * Display text or HTML in the logging div. | |
77 * | |
78 * @param {string} msg The HTML or text to display. | |
79 */ | |
80 function setMessage(msg) { | |
81 console.log(msg); | |
82 document.getElementById("logging_div").innerHTML = msg; | |
83 } | |
84 | |
85 /** | |
86 * Function to call when starting to load data. Prints the given URL and | |
87 * stores some data for retrieval on loadingDone(). | |
88 * | |
89 * @param {string} url The URL that we're loading. | |
90 * @param {Object} data Arbitrary data to store.s | |
91 */ | |
92 function loadingStart(url, data) { | |
93 document.body.style.cursor = "wait"; | |
94 document.getElementById("buildstep_div").style.display = "none"; | |
95 document.getElementById("builder_div").style.display = "none"; | |
96 document.getElementById("load_data_button").disabled = true; | |
97 loadStart = new Date().getTime(); | |
98 setMessage("Loading data from " + url); | |
99 currentlyLoadingData = data; | |
100 } | |
101 | |
102 /** | |
103 * Function to call when done loading data. Returns any data stored on | |
104 * loadingStart(). | |
105 */ | |
106 function loadingDone() { | |
107 document.body.style.cursor = "auto"; | |
108 document.getElementById("buildstep_div").style.display = "block"; | |
109 document.getElementById("builder_div").style.display = "block"; | |
110 document.getElementById("load_data_button").disabled = false; | |
111 var message = ""; | |
112 if (loadStart) { | |
113 var elapsedSeconds = (new Date().getTime() - loadStart) / 1000; | |
114 message = "Loaded data in " + elapsedSeconds + " seconds."; | |
115 } | |
116 setMessage(message); | |
117 var retData = currentlyLoadingData; | |
118 currentlyLoadingData = null; | |
119 return retData; | |
120 } | |
121 | |
122 /** | |
123 * Load data from the given URL using JSONP. | |
124 * | |
125 * @param {string} url The URL from which to load data. | |
126 * @param {string} callbackName The name of the function to use as a callback. | |
127 */ | |
128 function loadJSONP(url, callbackName) { | |
129 var script = document.createElement("script"); | |
130 var join = "&"; | |
131 if (url.indexOf("?") < 0) { | |
132 join = "?"; | |
133 } | |
134 script.src = url + join + "jsonp=" + callbackName; | |
135 document.head.appendChild(script); | |
136 } | |
137 | |
138 /** | |
139 * Re-organize Graphite data into a dictionary. | |
140 * | |
141 * @param {Array<Object>} data Data to organize. | |
142 */ | |
143 function graphiteDataDict(data) { | |
144 var dataDict = {}; | |
145 for (var i = 0; i < data.length; ++i) { | |
146 var splitName = data[i]["target"].split("."); | |
147 var name = splitName.slice(0, splitName.length - 1).join("."); | |
148 var resultType = splitName[1]; | |
149 var datapoints = data[i]["datapoints"]; | |
150 var result = 0; | |
151 if (datapoints.length > 0) { | |
152 result = datapoints[datapoints.length - 1][0]; | |
153 } | |
154 if (result == null) { | |
155 result = 0; | |
156 } | |
157 if (!dataDict[name]) { | |
158 dataDict[name] = {}; | |
159 } | |
160 dataDict[name][resultType] = result; | |
161 dataDict[name]["name"] = name; | |
162 } | |
163 return dataDict; | |
164 } | |
165 | |
166 /** | |
167 * Create lists of column names and rows given a data dictionary. | |
168 * | |
169 * @param {Object} dataDict data to organize into columns and rows. | |
170 */ | |
171 function getColsAndRows(dataDict) { | |
172 // Create the set of columns | |
173 var cols = ["Name"]; | |
174 for (var i = 0; i < derivedMetrics.length; ++i) { | |
175 cols.push(derivedMetrics[i][0]); | |
176 } | |
177 | |
178 // Create a row of data for each buildStep. | |
179 var rows = []; | |
180 for (var name in dataDict) { | |
181 var row = [name]; | |
182 // Fill in the data row. | |
183 for (var i = 0; i < derivedMetrics.length; ++i) { | |
184 var value = derivedMetrics[i][1](dataDict[name]); | |
185 row.push(value); | |
186 } | |
187 rows.push(row); | |
188 } | |
189 return [cols, rows] | |
190 } | |
191 | |
192 /** | |
193 * Build the data table given a set of data. | |
194 * | |
195 * @param {Array<Object>} data The data to put into the table. | |
196 * @param {string} title The title of the table. | |
197 * @param {string} containerId ID of the container to hold the table. | |
198 */ | |
199 function makeTableFromData(data, title, containerId) { | |
200 var buildStepData = graphiteDataDict(data); | |
201 var colsAndRows = getColsAndRows(buildStepData); | |
202 rebuildTable(title, colsAndRows[0], colsAndRows[1], containerId, | |
203 null, true); | |
204 } | |
205 | |
206 /** | |
207 * Sort the rows on the given column index. | |
208 * | |
209 * @param {Array<Array>} rows The rows to sort. | |
210 * @param {number} sortIndex The index of the column by which to sort. | |
211 * @param {number} sortOrder 1 or -1; determines whether to sort ascending or | |
212 * descending. | |
213 */ | |
214 function sortRows(rows, sortIndex, sortOrder) { | |
215 // Sort the table rows by sortIndex. | |
216 if (null == sortIndex || undefined == sortIndex) { | |
217 sortIndex = defaultSortIndex; | |
218 } | |
219 rows.sort(function(a, b) { | |
220 if (null == a) { return -sortOrder; } | |
221 if (null == b) { return sortOrder; } | |
222 if (null == a[sortIndex]) { return -sortOrder; } | |
223 if (null == b[sortIndex]) { return sortOrder; } | |
224 if (a[sortIndex] > b[sortIndex]) { return sortOrder; } | |
225 if (a[sortIndex] < b[sortIndex]) { return -sortOrder; } | |
226 return 0; | |
227 }); | |
228 } | |
229 | |
230 /** | |
231 * Rebuild the data table. | |
232 * | |
233 * @param {string} title The title of the table. | |
234 * @param {Array<string>} cols Array of column names. | |
235 * @param {Array<Array>} rows Array of row data. | |
236 * @param {string} containerId ID of the container where the table goes. | |
237 * @param {number} sortIndex The index of the column by which to sort. | |
238 * @param {boolean} reload Whether or not we reloaded data. This affects the | |
239 * sorting of rows. | |
240 */ | |
241 function rebuildTable(title, cols, rows, containerId, sortIndex, reload) { | |
242 var tableId = containerId + "_table"; | |
243 var oldTable = document.getElementById(tableId); | |
244 var lastSortIndex = null; | |
245 var sortOrder = -1; | |
246 if (oldTable) { | |
247 lastSortIndex = oldTable.sortIndex; | |
248 if (reload) { | |
249 sortIndex = lastSortIndex; | |
250 } else if (sortIndex == lastSortIndex) { | |
251 sortOrder = -oldTable.sortOrder; | |
252 } | |
253 } | |
254 sortRows(rows, sortIndex, sortOrder); | |
255 | |
256 var table = document.createElement("table"); | |
257 table.sortIndex = sortIndex; | |
258 table.sortOrder = sortOrder; | |
259 var thead = document.createElement("thead"); | |
260 | |
261 for (var i = 0; i < cols.length; ++i) { | |
262 var th = document.createElement("th"); | |
263 th.style.padding = "5px"; | |
264 th.style.textAlign = "right"; | |
265 var sortLink = document.createElement("a"); | |
266 sortLink.href = "#"; | |
267 sortLink.innerHTML = cols[i]; | |
268 sortLink.tableTitle = title; | |
269 sortLink.containerId = containerId; | |
270 sortLink.sortIndex = i; | |
271 sortLink.rowsObj = rows; | |
272 sortLink.colsObj = cols; | |
273 sortLink.addEventListener("click", function(event) { | |
274 var cols = event.target.colsObj; | |
275 var rows = event.target.rowsObj; | |
276 var containerId = event.target.containerId; | |
277 var sortIndex = event.target.sortIndex; | |
278 var tableTitle = event.target.tableTitle; | |
279 rebuildTable(tableTitle, cols, rows, containerId, sortIndex, false); | |
280 }); | |
281 th.appendChild(sortLink); | |
282 thead.appendChild(th); | |
283 } | |
284 | |
285 table.appendChild(thead); | |
286 | |
287 for (var i = 0; i < rows.length; ++i) { | |
288 var tr = document.createElement("tr"); | |
289 for (var j = 0; j < rows[i].length; ++j) { | |
290 var td = document.createElement("td"); | |
291 td.style.padding = "5px"; | |
292 td.style.textAlign = "right"; | |
293 var value = rows[i][j]; | |
294 if (typeof value == "number") { | |
295 value = parseFloat(value).toFixed(2); | |
296 } | |
297 // Add links for builder breakdowns to the first column. | |
298 if (tableId == "buildstep_div_table" && j == 0) { | |
299 var a = document.createElement("a"); | |
300 a.href = "#"; | |
301 a.buildStepName = value; | |
302 a.addEventListener("click", function(event) { | |
303 var name = event.target.buildStepName; | |
304 loadGraphiteData(null, null, null, null, name, | |
305 name + " on ...", "builder_div", | |
306 GROUP_NODE_BUILDER); | |
307 }); | |
308 a.innerHTML = value; | |
309 td.appendChild(a); | |
310 } else { | |
311 td.innerHTML = value; | |
312 } | |
313 tr.appendChild(td); | |
314 } | |
315 table.appendChild(tr); | |
316 } | |
317 var container = document.getElementById(containerId); | |
318 container.innerHTML = ""; | |
319 table.id = tableId; | |
320 var h2 = document.createElement("h2"); | |
321 h2.style.textAlign = "center"; | |
322 h2.innerHTML = title; | |
323 container.appendChild(h2); | |
324 container.appendChild(table); | |
325 } | |
326 | |
327 /** | |
328 * Callback function for receiving data from Graphite. Organizes the data and | |
329 * rebuilds the data table. | |
330 * | |
331 * @param {Array<Object>} data Data obtained from Graphite. | |
332 */ | |
333 function gotGraphiteData(data) { | |
334 var loadingData = loadingDone(); | |
335 makeTableFromData(data, loadingData["tableTitle"], | |
336 loadingData["containerId"]); | |
337 } | |
338 | |
339 /** | |
340 * Helper function for building Graphite data URLs. If any of the parameters | |
341 * are not provided, it obtains defaults from the text boxes on the page. | |
342 * | |
343 * @param {string} timeStart Unix timestamp; time period beginning. | |
344 * @param {string} timeEnd Unix timestamp; time period end. | |
345 * @param {string} masterFilter Filter the data by build master. | |
346 * @param {string} builderFilter Filter the data by builder. | |
347 * @param {string} buildStepFilter Filter the data by build step. | |
348 * @param {number} groupNode Column index for grouping Graphite data. | |
349 */ | |
350 function makeGraphiteURL(timeStart, timeEnd, masterFilter, builderFilter, | |
351 buildStepFilter, groupNode) { | |
352 if (!timeStart) { | |
353 timeStart = | |
354 dateTimeLocalToUnixUTC(document.getElementById("time_start").value); | |
355 } | |
356 if (!timeEnd) { | |
357 timeEnd = | |
358 dateTimeLocalToUnixUTC(document.getElementById("time_end").value); | |
359 } | |
360 if (!masterFilter) { | |
361 masterFilter = document.getElementById("master_filter").value; | |
362 } | |
363 if (!builderFilter) { | |
364 builderFilter = document.getElementById("builder_filter").value; | |
365 } | |
366 if (!buildStepFilter) { | |
367 buildStepFilter = document.getElementById("buildstep_filter").value; | |
368 } | |
369 var url = "http://skiamonitor.com/render?format=json&from=" + | |
370 timeStart + "&until=" + timeEnd; | |
371 var metricPrefixParts = ["buildbot", masterFilter, builderFilter, | |
372 buildStepFilter] | |
373 var metricPrefix = metricPrefixParts.join("."); | |
374 | |
375 for (var i = 0; i < metrics.length; ++i) { | |
376 var metric = metrics[i][0]; | |
377 var summaryMode = metrics[i][1]; | |
378 var fullMetricName = metricPrefix + "." + metric; | |
379 var groupByNode = "groupByNode(" + fullMetricName + "," + groupNode + | |
380 ",%22" + summaryMode + "%22)"; | |
381 var summarize = "summarize(" + groupByNode + ",%22" + SUMMARIZE_INTERVAL + | |
382 "%22,%22" + summaryMode + "%22,true)"; | |
383 var aliasByNode = "aliasByNode(" + summarize + ",0)"; | |
384 var aliasSub = "aliasSub(" + aliasByNode + ",%22$%22,%22." + metric + | |
385 "%22)"; | |
386 var fullTarget = "&target=" + aliasSub; | |
387 url += fullTarget; | |
388 } | |
389 return url; | |
390 } | |
391 | |
392 /** | |
393 * Load data from Graphite. | |
394 * | |
395 * @param {string} timeStart Unix timestamp; time period beginning. | |
396 * @param {string} timeEnd Unix timestamp; time period end. | |
397 * @param {string} masterFilter Filter the data by build master. | |
398 * @param {string} builderFilter Filter the data by builder. | |
399 * @param {string} buildStepFilter Filter the data by build step. | |
400 * @param {string} tableTitle Title for the table. | |
401 * @param {string} containerId ID of the element which will hold the table. | |
402 * @param {number} groupNode Column index for grouping Graphite data. | |
403 */ | |
404 function loadGraphiteData(timeStart, timeEnd, masterFilter, builderFilter, | |
405 buildStepFilter, tableTitle, containerId, groupNode)
{ | |
406 var url = makeGraphiteURL(timeStart, timeEnd, masterFilter, builderFilter, | |
407 buildStepFilter, groupNode); | |
408 var data = {"tableTitle": tableTitle, "containerId": containerId}; | |
409 loadingStart(url, data); | |
410 loadJSONP(url, "gotGraphiteData"); | |
411 } | |
412 | |
413 /** | |
414 * Function called when the "Load data" button is clicked. | |
415 */ | |
416 function reloadWithParams() { | |
417 var newQuery = | |
418 "from=" + dateTimeLocalToUnixUTC(document.getElementById("time_start").v
alue) + | |
419 "&until=" + dateTimeLocalToUnixUTC(document.getElementById("time_end").v
alue) + | |
420 "&master=" + document.getElementById("master_filter").value + | |
421 "&builder=" + document.getElementById("builder_filter").value + | |
422 "&buildstep=" + document.getElementById("buildstep_filter").value; | |
423 window.location.search = newQuery; | |
424 } | |
425 | |
426 /** | |
427 * Get the local timezone offset in seconds. | |
428 */ | |
429 function getTimezoneOffset() { | |
430 return new Date().getTimezoneOffset() * 60; | |
431 } | |
432 | |
433 /** | |
434 * Convert UTC to Local time. | |
435 */ | |
436 function UTCToLocal(unixDate) { | |
437 return unixDate - getTimezoneOffset(); | |
438 } | |
439 | |
440 /** | |
441 * Convert Local to UTC time. | |
442 */ | |
443 function localToUTC(unixDate) { | |
444 return unixDate + getTimezoneOffset(); | |
445 } | |
446 | |
447 /** | |
448 * Get the date in Unix format. | |
449 */ | |
450 function unixDate(date) { | |
451 var dateObj = null; | |
452 if (!date) { | |
453 dateObj = new Date() | |
454 } else { | |
455 dateObj = new Date(date); | |
456 } | |
457 return dateObj.getTime() / 1000; | |
458 } | |
459 | |
460 /** | |
461 * Convert from RFC 3339 to Unix format. | |
462 */ | |
463 function RFC3339ToUnix(date) { | |
464 return unixDate(date); | |
465 } | |
466 | |
467 /** | |
468 * Convert from Unix to RFC 3339 format, with no time zone information. | |
469 */ | |
470 function unixToRFC3339(timestamp) { | |
471 var date = new Date(parseInt(timestamp) * 1000); | |
472 function zfill(num) { | |
473 if (num < 10) { | |
474 return "0" + num; | |
475 } else { | |
476 return num; | |
477 } | |
478 } | |
479 return (date.getUTCFullYear() + "-" + | |
480 zfill(date.getUTCMonth() + 1) + "-" + | |
481 zfill(date.getUTCDate()) + "T" + | |
482 zfill(date.getUTCHours()) + ":" + | |
483 zfill(date.getUTCMinutes()) + ":" + | |
484 zfill(date.getUTCSeconds())); | |
485 } | |
486 | |
487 /** | |
488 * Convert from Unix timestamp to datetime-local RFC 3339 format. | |
489 */ | |
490 function unixUTCToDateTimeLocal(unix) { | |
491 return unixToRFC3339(UTCToLocal(unix)); | |
492 } | |
493 | |
494 /** | |
495 * Convert from datetime-local RFC 3339 format to Unix timestamp. | |
496 */ | |
497 function dateTimeLocalToUnixUTC(rfcLocal) { | |
498 return localToUTC(RFC3339ToUnix(rfcLocal)); | |
499 } | |
500 | |
501 /** | |
502 * Function called on page load. | |
503 */ | |
504 function init() { | |
505 // Load default query values. | |
506 var currentTime = unixDate(); | |
507 var timeEnd = currentTime; | |
508 var timeStart = currentTime - DEFAULT_TIME_PERIOD; | |
509 var masterFilter = DEFAULT_MASTER_FILTER; | |
510 var builderFilter = DEFAULT_BUILDER_FILTER; | |
511 var buildstepFilter = DEFAULT_BUILDSTEP_FILTER; | |
512 | |
513 // Retrieve any parameters from the URL. | |
514 var params = window.location.search.substring(1).split("&"); | |
515 // Remove empty string from parameters. | |
516 while (params.indexOf("") != -1) { | |
517 params.splice(params.indexOf("")); | |
518 } | |
519 for (var i = 0; i < params.length; ++i) { | |
520 var splitParam = params[i].split("="); | |
521 if (splitParam[0] == "from") { | |
522 timeStart = splitParam[1]; | |
523 } else if (splitParam[0] == "until") { | |
524 timeEnd = splitParam[1]; | |
525 } else if (splitParam[0] == "master") { | |
526 masterFilter = splitParam[1]; | |
527 } else if (splitParam[0] == "builder") { | |
528 builderFilter = splitParam[1]; | |
529 } else if (splitParam[0] == "buildstep") { | |
530 buildstepFilter = splitParam[1]; | |
531 } else { | |
532 console.error("Unknown parameter: " + splitParam[0]); | |
533 } | |
534 } | |
535 | |
536 // Fill the form with the appropriate values. | |
537 document.getElementById("time_start").value = | |
538 unixUTCToDateTimeLocal(timeStart); | |
539 document.getElementById("time_end").value = | |
540 unixUTCToDateTimeLocal(timeEnd); | |
541 document.getElementById("master_filter").value = masterFilter; | |
542 document.getElementById("builder_filter").value = builderFilter; | |
543 document.getElementById("buildstep_filter").value = buildstepFilter; | |
544 | |
545 // Decide what to do, based on the number of provided parameters. | |
546 if (params.length == 0) { | |
547 // Don't load data; we assume that the user might want to tweak the | |
548 // filters first. | |
549 } else if (params.length != 5) { | |
550 // Reload the page, filling in the rest of the parameters with their | |
551 // defaults. | |
552 reloadWithParams(); | |
553 } else { | |
554 // All parameters were provided. Load data. | |
555 loadGraphiteData(timeStart, timeEnd, masterFilter, builderFilter, | |
556 buildstepFilter, "Build Steps", "buildstep_div", | |
557 GROUP_NODE_BUILDSTEP); | |
558 } | |
559 } | |
560 | |
561 </script> | |
562 </head> | |
563 <body> | |
564 <div id="heading" style="font-size:2.5em; text-align:center; height:7%;"> | |
565 Skia Buildstep Dashboard | |
566 </div> | |
567 <div style="text-align:center;"> | |
568 Click a column header to sort that column. Click a buildstep to see its | |
569 results broken down into builders. | |
570 </div> | |
571 <div id="main_content_area" style="width:100%; height:90%; padding:0px; | |
572 margin:0px;"> | |
573 <div id="menu_div" | |
574 style="float:left; width:18%; height:100%; padding:0px; margin:0px;"> | |
575 <div style="width:100%;"> | |
576 <nobr>Time period:</nobr><br/> | |
577 From:<br/> | |
578 <input type="datetime-local" id="time_start" /><br/> | |
579 To:<br/> | |
580 <input type="datetime-local" id="time_end" /><br/> | |
581 <nobr>Build master filter:</nobr><br/> | |
582 <input type="text" id="master_filter" /><br/> | |
583 <nobr>Builder filter:</nobr><br/> | |
584 <input type="text" id="builder_filter" /><br/> | |
585 <nobr>Build step filter:</nobr><br/> | |
586 <input type="text" id="buildstep_filter" /><br/> | |
587 <input type="button" id="load_data_button" onClick="reloadWithParams()
;" value="Load Data"/> | |
588 </div> | |
589 <div id="logging_div" style="width:100%; padding:0px; margin:0px"></div> | |
590 </div> | |
591 <div id="buildstep_div" | |
592 style="float:left; width:41%; padding:0px; margin:0px"> | |
593 </div> | |
594 <div id="builder_div" | |
595 style="float:left; width:41%; padding:0px; margin:0px"> | |
596 </div> | |
597 </div> | |
598 <script type="text/javascript">init();</script> | |
599 </body> | |
600 </html> | |
601 | |
OLD | NEW |