blob: 3a8781ab6ca1a1920fbfbc9eafbd351c76d32653 [file] [log] [blame]
<!--
This in an HTML Import-able file that contains the definition
of the following elements:
<task-graph-sk>
Status information about the task scheduler.
To use this file import it:
<link href="/res/imp/task-graph-sk.html" rel="import" />
Usage:
<task-graph-sk tasks="[[tasks]]" jobs="[[jobs]]" swarming-server="..."></task-graph-sk>
Properties:
jobs: Array of Jobs, as provided by the Task Scheduler server.
task: Optional, a Task object, as provided by the Task Scheduler server. If
provided, the matching Task in the graph will be highlighted.
swarmingServer: String; hostname of the Swarming server.
Methods:
None.
Events:
None.
-->
<link rel="import" href="/res/imp/bower_components/polymer/polymer.html">
<dom-module id="task-graph-sk">
<template>
<svg id="tasks_svg"></svg>
</template>
<script src="/res/imp/bower_components/d3/d3.min.js"></script>
<script>
(function(){
var taskStatusToTextColor = {
"": ["pending", "rgb(255, 255, 255)"],
"RUNNING": ["running", "rgb(248, 230, 180)"],
"SUCCESS": ["succeeded", "rgb(209, 228, 188)"],
"FAILURE": ["failed", "rgb(217, 95, 2)"],
"MISHAP": ["mishap", "rgb(117, 112, 179)"],
};
Polymer({
is: "task-graph-sk",
properties: {
jobs: {
type: Array,
},
task: {
type: Object,
value: function() {
return {
name: "",
};
},
},
swarmingServer: {
type: String,
},
},
observers: [
"_draw(jobs.*, task.*)",
],
_computeBotsLink: function(dims) {
var link = "https://" + this.swarmingServer + "/botlist";
if (dims) {
for (var i = 0; i < dims.length; i++) {
if (i == 0) {
link += "?";
} else {
link += "&";
}
link += "f=" + encodeURIComponent(dims[i]);
}
}
return link;
},
_computeTaskLink: function(task) {
var swarmingLink = "https://" + this.swarmingServer + "/task?id=" + task.swarmingTaskId;
return "https://task-driver.skia.org/td/" + task.id + "?ifNotFound=" + encodeURIComponent(swarmingLink);
},
_computeTasksLink: function(task) {
return "https://" + this.swarmingServer + "/tasklist?f=sk_name-tag:" + task.name;
},
_draw: function() {
var graph = {};
var taskData = {};
var taskDims = {};
for (var i = 0; i < this.jobs.length; i++) {
var job = this.jobs[i];
for (var taskName in job.dependencies) {
var deps = graph[taskName] || [];
if (job.dependencies[taskName]) {
for (var j = 0; j < job.dependencies[taskName].length; j++) {
deps.push(job.dependencies[taskName][j]);
}
}
graph[taskName] = deps;
}
for (var taskName in job.taskDimensions) {
if (!taskDims[taskName]) {
taskDims[taskName] = job.taskDimensions[taskName];
}
}
if (job.tasks) {
for (var taskName in job.tasks) {
var tasks = taskData[taskName] || [];
if (job.tasks[taskName]) {
for (var j = 0; j < job.tasks[taskName].length; j++) {
var task = job.tasks[taskName][j];
var found = tasks.find(function(item) {
return item.id === task.id;
});
if (!found) {
tasks.push(task);
}
}
}
taskData[taskName] = tasks;
}
}
}
// Sort the tasks and task specs for consistency.
var graphKeys = [];
for (var taskName in graph) {
graph[taskName].sort();
graphKeys.push(taskName);
}
graphKeys.sort();
for (var taskName in taskData) {
taskData[taskName].sort(function(a, b) {
return a.attempt < b.attempt;
});
}
// Skip drawing the graph if taskData or graph are missing or empty. This
// is mainly to prevent errors on the demo page.
if (!taskData || !graph || Object.keys(taskData).length == 0 || Object.keys(graph).length == 0) {
console.log("Not drawing graph; taskData or graph not ready.");
return;
}
console.log("Drawing tasks graph.");
// Compute the "depth" of each task spec.
var depth = {};
var cols = [];
var visited = {};
var visit = function(current) {
visited[current] = true
var myDepth = 0;
var deps = graph[current] || [];
for (var i = 0; i < deps.length; i++) {
var dep = deps[i];
// Visit the dep if we haven't yet. Its depth may be zero, so we have
// to explicitly use "depth[dep] == undefined" instead of "!depth[dep]"
if (depth[dep] == undefined) {
visit(dep);
}
if (depth[dep] >= myDepth) {
myDepth = depth[dep] + 1;
}
}
depth[current] = myDepth;
if (cols.length == myDepth) {
cols.push([]);
} else if (myDepth > cols.length) {
console.log("_computeTasksGraph skipped a column!");
return;
}
cols[myDepth].push({
name: current,
tasks: taskData[current] || [],
});
};
// Visit all of the nodes.
for (var i = 0; i < graphKeys.length; i++) {
var key = graphKeys[i];
if (!visited[key]) {
visit(key);
}
}
var botLinkFontSize = 11;
var botLinkMarginX = 10;
var botLinkMarginY = 4;
var botLinkHeight = botLinkFontSize + 2*botLinkMarginY;
var botLinkText = "view swarming bots";
var fontFamily = "Arial";
var fontSize = 12;
var taskSpecMarginX = 20;
var taskSpecMarginY = 20;
var taskMarginX = 10;
var taskMarginY = 10;
var textMarginX = 10;
var textMarginY = 10;
var taskWidth = 30;
var taskHeight = 30;
var taskLinkFontSize = botLinkFontSize;
var taskLinkMarginX = botLinkMarginX;
var taskLinkMarginY = botLinkMarginY;
var taskLinkHeight = taskLinkFontSize + 2*taskLinkMarginY;
var taskLinkText = "view swarming tasks";
var textOffsetX = textMarginX;
var textOffsetY = fontSize + textMarginY;
var textHeight = fontSize + 2 * textMarginY;
var botLinkOffsetY = textOffsetY + botLinkFontSize + botLinkMarginY;
var taskLinkOffsetY = botLinkOffsetY + taskLinkFontSize + taskLinkMarginY;
var taskSpecHeight = textHeight + botLinkHeight + taskLinkHeight + taskHeight + taskMarginY;
// Compute the task spec block width for each column.
var maxTextWidth = 0;
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
ctx.font = botLinkFontSize + "px " + fontFamily;
var botLinkTextWidth = ctx.measureText(botLinkText).width + 2 * botLinkMarginX;
ctx.font = taskLinkFontSize + "px " + fontFamily;
var taskLinkTextWidth = ctx.measureText(taskLinkText).width + 2 * taskLinkMarginX;
ctx.font = fontSize + "px " + fontFamily;
var taskSpecWidth = [];
for (var col = 0; col < cols.length; col++) {
// Get the minimum width of a task spec block needed to fit the entire
// task spec name.
var maxWidth = Math.max(botLinkTextWidth, taskLinkTextWidth);
for (var i = 0; i < cols[col].length; i++) {
var oldFont = ctx.font;
var text = cols[col][i].name;
if (text == this.task.name) {
ctx.font = "bold " + ctx.font;
}
var textWidth = ctx.measureText(text).width + 2 * textMarginX;
ctx.font = oldFont;
if (textWidth > maxWidth) {
maxWidth = textWidth;
}
var numTasks = cols[col][i].tasks.length || 1;
var tasksWidth = taskMarginX + numTasks * (taskWidth + taskMarginX);
if (tasksWidth > maxWidth) {
maxWidth = tasksWidth;
}
}
taskSpecWidth.push(maxWidth);
}
// Lay out the task specs and tasks.
var totalWidth = 0;
var totalHeight = 0;
var taskSpecs = [];
var tasks = [];
var byName = {};
var curX = taskMarginX;
for (var col = 0; col < cols.length; col++) {
var curY = taskMarginY;
// Add an entry for each task.
for (var i = 0; i < cols[col].length; i++) {
var taskSpec = cols[col][i];
var entry = {
x: curX,
y: curY,
width: taskSpecWidth[col],
height: taskSpecHeight,
name: taskSpec.name,
numTasks: taskSpec.tasks.length,
};
taskSpecs.push(entry);
byName[taskSpec.name] = entry;
var taskX = curX + taskMarginX;
var taskY = curY + textHeight + botLinkHeight + taskLinkHeight;
for (var j = 0; j < taskSpec.tasks.length; j++) {
var task = taskSpec.tasks[j];
tasks.push({
x: taskX + j * (taskWidth + taskMarginX),
y: taskY,
width: taskWidth,
height: taskHeight,
task: task,
});
}
curY += taskSpecHeight + taskSpecMarginY;
}
if (curY > totalHeight) {
totalHeight = curY;
}
curX += taskSpecWidth[col] + taskSpecMarginX;
}
totalWidth = curX;
// Compute the arrows.
var arrows = []
for (var name in graph) {
var dst = byName[name];
var deps = graph[name];
if (deps) {
for (var j = 0; j < deps.length; j++) {
var src = byName[deps[j]]
if (!src) {
console.log("Error: task " + dst.name + " has unknown parent " + deps[j]);
} else {
arrows.push([src, dst]);
}
}
}
}
// Draw the graph.
svg = d3.select(this.$.tasks_svg)
.attr("width", totalWidth)
.attr("height", totalHeight);
// Helper for styling.
var styleToString = function(style) {
var rv = "";
for (var key in style) {
if (rv.length > 0) {
rv += "; "
}
rv += key + ":" + style[key];
}
return rv;
}
// Draw arrows.
var arrowWidth = 4;
var arrowHeight = 4;
var arrowHeadPath = svg.selectAll("marker.arrowhead").data([0]);
arrowHeadPath.enter()
.append("svg:marker")
.attr("id", "arrowhead")
.attr("class", "arrowhead")
.attr("viewBox", "0 0 10 10")
.attr("refX", "0")
.attr("refY", "5")
.attr("markerUnits", "strokeWidth")
.attr("markerWidth", arrowWidth)
.attr("markerHeight", arrowHeight)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M 0 0 L 10 5 L 0 10 Z"); // Filled triangle path.
arrowHeadPath.exit().remove();
var arrowId = function(data) { return `(${data[0].name}),(${data[1].name})`; };
var arrowPaths = svg.selectAll("path.arrow").data(arrows, arrowId);
arrowPaths
.enter()
.append("svg:path")
.attr("class", "arrow")
.attr("stroke", "black")
.attr("stroke-width", "1")
.attr("fill", "transparent")
.attr("marker-end", "url(#arrowhead)");
arrowPaths
.attr("d", function(data) {
// Start and end points.
var x1 = data[0].x + data[0].width;
var y1 = data[0].y + data[0].height / 2;
var x2 = data[1].x - arrowWidth;
var y2 = data[1].y + data[1].height / 2;
// Control points.
var cx1 = x1 + taskSpecMarginX - arrowWidth/2;
var cy1 = y1;
var cx2 = x2 - taskSpecMarginX + arrowWidth/2;
var cy2 = y2;
return ("M" + x1 + " " + y1
+ " C" + cx1 + " " + cy1
+ " " + cx2 + " " + cy2
+ " " + x2 + " " + y2);
});
arrowPaths.exit().remove();
// Draw task specs.
var taskSpecRects = svg.selectAll("rect.taskSpec").data(taskSpecs, function(data) { return data.name; });
taskSpecRects.enter()
.append("svg:rect")
.attr("class", "taskSpec")
.attr("rx", "4")
.attr("ry", "4");
taskSpecRects
.attr("x", function(data) { return data.x; })
.attr("y", function(data) { return data.y; })
.attr("width", function(data) { return data.width; })
.attr("height", function(data) { return data.height; })
.attr("style", function(taskspec) {
var style = {
"stroke": "black",
"fill": "white",
};
if (taskspec.name == this.task.name) {
style["stroke-width"] = "3px";
}
return styleToString(style);
}.bind(this));
taskSpecRects.exit().remove();
// Draw text.
var taskSpecTexts = svg.selectAll("text.taskSpec").data(taskSpecs, function(data) { return data.name; });
taskSpecTexts.enter()
.append("svg:text")
.attr("class", "taskSpec")
.attr("font-family", fontFamily)
.attr("font-size", fontSize);
taskSpecTexts
.attr("x", function(data) { return data.x + textOffsetX; })
.attr("y", function(data) { return data.y + textOffsetY; })
.attr("font-weight", function(data) { return data.name == this.task.name ? "bold" : "normal"; }.bind(this))
.text(function(data) { return data.name; });
taskSpecTexts.exit().remove();
// Draw links to Swarming bot lists.
var botLinks = svg.selectAll("a.bots")
.data(taskSpecs, function(data) { return data.name; });
botLinks.enter()
.append("a")
.attr("class", "bots")
.attr("target", "_blank")
.append("svg:text")
.attr("class", "bots")
.attr("font-family", fontFamily)
.attr("font-size", botLinkFontSize)
.attr("style", styleToString({"text-decoration": "underline"}));
botLinks
.attr("href", function(data) {
var dims = taskDims[data.name];
return this._computeBotsLink(dims);
}.bind(this));
botLinks.exit().remove()
var botTexts = svg.selectAll("text.bots")
.data(taskSpecs, function(data) { return data.name; });
botTexts
.attr("x", function(data) { return data.x + textOffsetX; })
.attr("y", function(data) { return data.y + botLinkOffsetY; })
.text(botLinkText)
.attr("href", function(data) {
var dims = taskDims[data.name];
return this._computeBotsLink(dims);
}.bind(this));
botTexts.exit().remove();
// Draw links to Swarming task lists.
var taskLinks = svg.selectAll("a.taskLinks")
.data(taskSpecs, function(data) { return data.name; });
taskLinks.enter()
.append("a")
.attr("class", "taskLinks")
.attr("target", "_blank")
.append("svg:text")
.attr("class", "taskLinks")
.attr("font-family", fontFamily)
.attr("font-size", taskLinkFontSize)
.attr("style", styleToString({"text-decoration": "underline"}));
taskLinks
.attr("href", function(data) {
return this._computeTasksLink(data);
}.bind(this));
taskLinks.exit().remove()
var taskTexts = svg.selectAll("text.taskLinks")
.data(taskSpecs, function(data) { return data.name; });
taskTexts
.attr("x", function(data) { return data.x + textOffsetX; })
.attr("y", function(data) { return data.y + taskLinkOffsetY; })
.text(taskLinkText)
.attr("href", function(data) {
return this._computeTasksLink(data);
}.bind(this));
taskTexts.exit().remove();
// Draw tasks.
var taskLinks = svg.selectAll("a.task").data(tasks, function(data) { return data.task.id; });
taskLinks.enter()
.append("a")
.attr("class", "task")
.attr("target", "_blank")
.append("svg:rect")
.attr("rx", "4")
.attr("ry", "4")
.attr("class", "task");
taskLinks.attr("href", function(data) {
return this._computeTaskLink(data.task);
}.bind(this));
taskLinks.exit().remove();
var taskRects = svg.selectAll("rect.task").data(tasks, function(data) { return data.task.id; });
taskRects
.attr("x", function(data) { return data.x; })
.attr("y", function(data) { return data.y; })
.attr("width", function(data) { return data.width; })
.attr("height", function(data) { return data.height; })
.attr("style", function(data) {
var color = taskStatusToTextColor[data.task.status][1];
var style = {
"stroke": "black",
"fill": color,
};
if (data.task.id == this.task.id) {
style["stroke-width"] = "3px";
}
return styleToString(style);
}.bind(this));
taskRects.exit().remove();
},
});
})();
</script>
</dom-module>