blob: 0d8dc6236e49634056dc95098cbf58a1b084fa98 [file] [log] [blame]
<html>
<head>
<title>Skia Buildstep Dashboard</title>
<link rel="icon" href="favicon.ico">
<script language="JavaScript">
"use strict";
// Timestamp of when we started loading data. Used to compute load time.
var loadStart = null;
// Used to store information about what data is currently being loaded.
var currentlyLoadingData = null;
// Metrics to load from Graphite.
// Takes the form: [[metric_name, merge_function], ...]
var metrics = [["success", "sum"],
["failure", "sum"],
["duration", "avg"]];
// Metrics to derive from the loaded metrics above. We display these in the
// table. Takes the form: [[metric_name, derivation_fn], ...], where
// derivation_fn is a function which takes as a parameter a dictionary
// containing values for all of the above metrics for a given buildStep and
// returns a value, which may be null.
var derivedMetrics = [
["Duration", function(stepData) {
if (stepData["duration"] == undefined) {
return null;
}
return stepData["duration"];
}],
["Failure Rate", function(stepData) {
if (stepData["success"] == undefined ||
stepData["failure"] == undefined) {
return null;
}
var success = parseFloat(stepData["success"]);
var failure = parseFloat(stepData["failure"]);
var totalRuns = success + failure;
var failureRate = failure / totalRuns;
if (totalRuns <= 0) {
return null;
}
return failureRate;
}],
["Total Runs", function(stepData) {
if (stepData["success"] == undefined ||
stepData["failure"] == undefined) {
return null;
}
var success = parseFloat(stepData["success"]);
var failure = parseFloat(stepData["failure"]);
return success + failure;
}],
];
// Use the second data column as the default sort index, since that typically
// indicates the most important column which isn't a label.
var defaultSortIndex = 1;
/**
* Display text or HTML in the logging div.
*
* @param {string} msg The HTML or text to display.
*/
function setMessage(msg) {
console.log(msg);
document.getElementById("logging_div").innerHTML = msg;
}
/**
* Function to call when starting to load data. Prints the given URL and
* stores some data for retrieval on loadingDone().
*
* @param {string} url The URL that we're loading.
* @param {Object} data Arbitrary data to store.s
*/
function loadingStart(url, data) {
document.body.style.cursor = "wait";
document.getElementById("buildstep_div").style.display = "none";
document.getElementById("builder_div").style.display = "none";
loadStart = new Date().getTime();
setMessage("Loading data from " + url);
currentlyLoadingData = data;
}
/**
* Function to call when done loading data. Returns any data stored on
* loadingStart().
*/
function loadingDone() {
document.body.style.cursor = "auto";
document.getElementById("buildstep_div").style.display = "block";
document.getElementById("builder_div").style.display = "block";
var message = "";
if (loadStart) {
var elapsedSeconds = (new Date().getTime() - loadStart) / 1000;
message = "Loaded data in " + elapsedSeconds + " seconds.";
}
setMessage(message);
var retData = currentlyLoadingData;
currentlyLoadingData = null;
return retData;
}
/**
* Load data from the given URL using JSONP.
*
* @param {string} url The URL from which to load data.
* @param {string} callbackName The name of the function to use as a callback.
*/
function loadJSONP(url, callbackName) {
var script = document.createElement("script");
var join = "&";
if (url.indexOf("?") < 0) {
join = "?";
}
script.src = url + join + "jsonp=" + callbackName;
document.head.appendChild(script);
}
/**
* Re-organize Graphite data into a dictionary.
*
* @param {Array<Object>} data Data to organize.
*/
function graphiteDataDict(data) {
var dataDict = {};
for (var i = 0; i < data.length; ++i) {
var splitName = data[i]["target"].split(".");
var name = splitName.slice(0, splitName.length - 1).join(".");
var resultType = splitName[1];
var datapoints = data[i]["datapoints"];
var result = 0;
if (datapoints.length > 0) {
result = datapoints[datapoints.length - 1][0];
}
if (result == null) {
result = 0;
}
if (!dataDict[name]) {
dataDict[name] = {};
}
dataDict[name][resultType] = result;
dataDict[name]["name"] = name;
}
return dataDict;
}
/**
* Create lists of column names and rows given a data dictionary.
*
* @param {Object} dataDict data to organize into columns and rows.
*/
function getColsAndRows(dataDict) {
// Create the set of columns
var cols = ["Name"];
for (var i = 0; i < derivedMetrics.length; ++i) {
cols.push(derivedMetrics[i][0]);
}
// Create a row of data for each buildStep.
var rows = [];
for (var name in dataDict) {
var row = [name];
// Fill in the data row.
for (var i = 0; i < derivedMetrics.length; ++i) {
var value = derivedMetrics[i][1](dataDict[name]);
row.push(value);
}
rows.push(row);
}
return [cols, rows]
}
/**
* Build the data table given a set of data.
*
* @param {Array<Object>} data The data to put into the table.
* @param {string} title The title of the table.
* @param {string} containerId ID of the container to hold the table.
*/
function makeTableFromData(data, title, containerId) {
var buildStepData = graphiteDataDict(data);
var colsAndRows = getColsAndRows(buildStepData);
rebuildTable(title, colsAndRows[0], colsAndRows[1], containerId,
null, true);
}
/**
* Sort the rows on the given column index.
*
* @param {Array<Array>} rows The rows to sort.
* @param {number} sortIndex The index of the column by which to sort.
* @param {number} sortOrder 1 or -1; determines whether to sort ascending or
* descending.
*/
function sortRows(rows, sortIndex, sortOrder) {
// Sort the table rows by sortIndex.
if (null == sortIndex || undefined == sortIndex) {
sortIndex = defaultSortIndex;
}
rows.sort(function(a, b) {
if (null == a) { return -sortOrder; }
if (null == b) { return sortOrder; }
if (null == a[sortIndex]) { return -sortOrder; }
if (null == b[sortIndex]) { return sortOrder; }
if (a[sortIndex] > b[sortIndex]) { return sortOrder; }
if (a[sortIndex] < b[sortIndex]) { return -sortOrder; }
return 0;
});
}
/**
* Rebuild the data table.
*
* @param {string} title The title of the table.
* @param {Array<string>} cols Array of column names.
* @param {Array<Array>} rows Array of row data.
* @param {string} containerId ID of the container where the table goes.
* @param {number} sortIndex The index of the column by which to sort.
* @param {boolean} reload Whether or not we reloaded data. This affects the
* sorting of rows.
*/
function rebuildTable(title, cols, rows, containerId, sortIndex, reload) {
var tableId = containerId + "_table";
var oldTable = document.getElementById(tableId);
var lastSortIndex = null;
var sortOrder = -1;
if (oldTable) {
lastSortIndex = oldTable.sortIndex;
if (reload) {
sortIndex = lastSortIndex;
} else if (sortIndex == lastSortIndex) {
sortOrder = -oldTable.sortOrder;
}
}
sortRows(rows, sortIndex, sortOrder);
var table = document.createElement("table");
table.sortIndex = sortIndex;
table.sortOrder = sortOrder;
var thead = document.createElement("thead");
for (var i = 0; i < cols.length; ++i) {
var th = document.createElement("th");
th.style.padding = "5px";
th.style.textAlign = "right";
var sortLink = document.createElement("a");
sortLink.href = "#";
sortLink.innerHTML = cols[i];
sortLink.tableTitle = title;
sortLink.containerId = containerId;
sortLink.sortIndex = i;
sortLink.rowsObj = rows;
sortLink.colsObj = cols;
sortLink.addEventListener("click", function(event) {
var cols = event.target.colsObj;
var rows = event.target.rowsObj;
var containerId = event.target.containerId;
var sortIndex = event.target.sortIndex;
var tableTitle = event.target.tableTitle;
rebuildTable(tableTitle, cols, rows, containerId, sortIndex, false);
});
th.appendChild(sortLink);
thead.appendChild(th);
}
table.appendChild(thead);
for (var i = 0; i < rows.length; ++i) {
var tr = document.createElement("tr");
for (var j = 0; j < rows[i].length; ++j) {
var td = document.createElement("td");
td.style.padding = "5px";
td.style.textAlign = "right";
var value = rows[i][j];
if (typeof value == "number") {
value = parseFloat(value).toFixed(2);
}
// Add links for builder breakdowns to the first column.
if (tableId == "buildstep_div_table" && j == 0) {
var a = document.createElement("a");
a.href = "#";
a.buildStepName = value;
a.addEventListener("click", function(event) {
var name = event.target.buildStepName;
loadGraphiteData(null, null, null, name,
name + " on ...", "builder_div");
});
a.innerHTML = value;
td.appendChild(a);
} else {
td.innerHTML = value;
}
tr.appendChild(td);
}
table.appendChild(tr);
}
var container = document.getElementById(containerId);
container.innerHTML = "";
table.id = tableId;
var h2 = document.createElement("h2");
h2.style.textAlign = "center";
h2.innerHTML = title;
container.appendChild(h2);
container.appendChild(table);
}
/**
* Callback function for receiving data from Graphite. Organizes the data and
* rebuilds the data table.
*
* @param {Array<Object>} data Data obtained from Graphite.
*/
function gotGraphiteData(data) {
var loadingData = loadingDone();
makeTableFromData(data, loadingData["tableTitle"],
loadingData["containerId"]);
}
/**
* Helper function for building Graphite data URLs. If any of the parameters
* are not provided, it obtains defaults from the text boxes on the page.
*
* @param {string} timePeriod Time period over which to retrieve data.
* @param {string} masterFilter Filter the data by build master.
* @param {string} builderFilter Filter the data by builder.
* @param {string} buildStepFilter Filter the data by build step.
*/
function makeGraphiteURL(timePeriod, masterFilter, builderFilter,
buildStepFilter) {
if (!timePeriod) {
timePeriod = document.getElementById("time_period").value;
}
if (!masterFilter) {
masterFilter = document.getElementById("build_master_filter").value;
}
if (!builderFilter) {
builderFilter = document.getElementById("builder_filter").value;
}
if (!buildStepFilter) {
buildStepFilter = document.getElementById("build_step_filter").value;
}
var url = "http://skiamonitor.com/render?format=json&from=-" + timePeriod;
var metricPrefixParts = ["buildbot", masterFilter, builderFilter,
buildStepFilter]
var metricPrefix = metricPrefixParts.join(".");
// Group by the last non-wildcarded metric part.
var groupNode = 0;
for (var i = 1; i < metricPrefixParts.length; ++i) {
if (metricPrefixParts[i].indexOf("*") >= 0) {
groupNode++;
}
}
for (var i = 0; i < metrics.length; ++i) {
var metric = metrics[i][0];
var summaryMode = metrics[i][1];
var fullMetricName = metricPrefix + "." + metric;
var groupByNode = "groupByNode(" + fullMetricName + "," + groupNode +
",%22" + summaryMode + "%22)";
var summarize = "summarize(" + groupByNode + ",%22" + timePeriod +
"%22,%22" + summaryMode + "%22,true)";
var aliasByNode = "aliasByNode(" + summarize + ",0)";
var aliasSub = "aliasSub(" + aliasByNode + ",%22$%22,%22." + metric +
"%22)";
var fullTarget = "&target=" + aliasSub;
url += fullTarget;
}
return url;
}
/**
* Load data from Graphite.
*/
function loadGraphiteData(timePeriod, masterFilter, builderFilter,
buildStepFilter, tableTitle, containerId) {
var url = makeGraphiteURL(timePeriod, masterFilter, builderFilter,
buildStepFilter);
var data = {"tableTitle": tableTitle, "containerId": containerId};
loadingStart(url, data);
loadJSONP(url, "gotGraphiteData");
}
/**
* Function called when the "Load data" button is clicked.
*/
function loadDataClick() {
document.getElementById('builder_div').innerHTML = '';
loadGraphiteData(null, null, null, null, "Build Steps", "buildstep_div")
}
</script>
</head>
<body>
<div id="heading" style="font-size:2.5em; text-align:center; height:7%;">
Skia Buildstep Dashboard
</div>
<div style="text-align:center;">
Click a column header to sort that column. Click a buildstep to see its
results broken down into builders.
</div>
<div id="main_content_area" style="width:100%; height:90%; padding:0px;
margin:0px;">
<div id="menu_div"
style="float:left; width:18%; height:100%; padding:0px; margin:0px;">
<div style="width:100%;">
<nobr>Time period:</nobr><br/>
<input type="text" id="time_period" value="24h" /><br/>
<nobr>Build master filter:</nobr><br/>
<input type="text" id="build_master_filter" value="*" /><br/>
<nobr>Builder filter:</nobr><br/>
<input type="text" id="builder_filter" value="*" /><br/>
<nobr>Build step filter:</nobr><br/>
<input type="text" id="build_step_filter" value="*" /><br/>
<input type="button" onClick="loadDataClick();" value="Load Data"/>
</div>
<div id="logging_div" style="width:100%; padding:0px; margin:0px"></div>
</div>
<div id="buildstep_div"
style="float:left; width:41%; padding:0px; margin:0px">
</div>
<div id="builder_div"
style="float:left; width:41%; padding:0px; margin:0px">
</div>
</div>
</body>
</html>