| <!-- |
| 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); |
| }, |
| |
| _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 textOffsetX = textMarginX; |
| var textOffsetY = fontSize + textMarginY; |
| var textHeight = fontSize + 2 * textMarginY; |
| var botLinkOffsetY = textOffsetY + botLinkFontSize + botLinkMarginY; |
| var taskSpecHeight = textHeight + botLinkHeight + 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 = 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 = botLinkTextWidth; |
| 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; |
| 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 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 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(); |
| |
| // 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(); |
| }, |
| }); |
| })(); |
| </script> |
| </dom-module> |