0001
0002
0003
0004
0005
0006
0007
0008
0009
0010
0011
0012
0013
0014
0015
0016
0017
0018
0019
0020
0021
0022
0023
0024
0025
0026
0027
0028
0029
0030
0031
0032
0033
0034
0035
0036
0037
0038
0039
0040
0041
0042
0043
0044
0045
0046
0047
0048
0049
0050
0051
0052
0053
0054 var VizConstants = {
0055 svgMarginX: 16,
0056 svgMarginY: 16,
0057 stageSep: 40,
0058 graphPrefix: "graph_",
0059 nodePrefix: "node_",
0060 clusterPrefix: "cluster_"
0061 };
0062
0063 var JobPageVizConstants = {
0064 clusterLabelSize: 12,
0065 stageClusterLabelSize: 14,
0066 rankSep: 40
0067 };
0068
0069 var StagePageVizConstants = {
0070 clusterLabelSize: 14,
0071 stageClusterLabelSize: 14,
0072 rankSep: 40
0073 };
0074
0075
0076
0077
0078
0079 function expandDagVizArrowKey(forJob) {
0080 return forJob ? "expand-dag-viz-arrow-job" : "expand-dag-viz-arrow-stage";
0081 }
0082
0083
0084
0085
0086
0087
0088
0089 function toggleDagViz(forJob) {
0090 var status = window.localStorage.getItem(expandDagVizArrowKey(forJob)) == "true";
0091 status = !status;
0092
0093 var arrowSelector = ".expand-dag-viz-arrow";
0094 $(arrowSelector).toggleClass('arrow-closed');
0095 $(arrowSelector).toggleClass('arrow-open');
0096 var shouldShow = $(arrowSelector).hasClass("arrow-open");
0097 if (shouldShow) {
0098 var shouldRender = graphContainer().select("*").empty();
0099 if (shouldRender) {
0100 renderDagViz(forJob);
0101 }
0102 graphContainer().style("display", "block");
0103 } else {
0104
0105 graphContainer().style("display", "none");
0106 }
0107
0108 window.localStorage.setItem(expandDagVizArrowKey(forJob), "" + status);
0109 }
0110
0111 $(function (){
0112 if ($("#stage-dag-viz").length &&
0113 window.localStorage.getItem(expandDagVizArrowKey(false)) == "true") {
0114
0115 window.localStorage.setItem(expandDagVizArrowKey(false), "false");
0116 toggleDagViz(false);
0117 } else if ($("#job-dag-viz").length &&
0118 window.localStorage.getItem(expandDagVizArrowKey(true)) == "true") {
0119
0120 window.localStorage.setItem(expandDagVizArrowKey(true), "false");
0121 toggleDagViz(true);
0122 }
0123 });
0124
0125
0126
0127
0128
0129
0130
0131
0132
0133
0134
0135
0136
0137
0138
0139
0140
0141 function renderDagViz(forJob) {
0142
0143
0144 var jobOrStage = forJob ? "job" : "stage";
0145 if (metadataContainer().empty() ||
0146 metadataContainer().selectAll("div").empty()) {
0147 var message =
0148 "<b>No visualization information available for this " + jobOrStage + "!</b><br/>" +
0149 "If this is an old " + jobOrStage + ", its visualization metadata may have been " +
0150 "cleaned up over time.<br/> You may consider increasing the value of ";
0151 if (forJob) {
0152 message += "<i>spark.ui.retainedJobs</i> and <i>spark.ui.retainedStages</i>.";
0153 } else {
0154 message += "<i>spark.ui.retainedStages</i>";
0155 }
0156 graphContainer().append("div").attr("id", "empty-dag-viz-message").html(message);
0157 return;
0158 }
0159
0160
0161 var svg = graphContainer().append("svg").attr("class", jobOrStage);
0162 if (forJob) {
0163 renderDagVizForJob(svg);
0164 } else {
0165 renderDagVizForStage(svg);
0166 }
0167
0168
0169 metadataContainer().selectAll(".cached-rdd").each(function(v) {
0170 var rddId = d3.select(this).text().trim();
0171 var nodeId = VizConstants.nodePrefix + rddId;
0172 svg.selectAll("g." + nodeId).classed("cached", true);
0173 });
0174
0175 metadataContainer().selectAll(".barrier-rdd").each(function() {
0176 var opId = d3.select(this).text().trim();
0177 var opClusterId = VizConstants.clusterPrefix + opId;
0178 var stageId = $(this).parents(".stage-metadata").attr("stage-id");
0179 var stageClusterId = VizConstants.graphPrefix + stageId;
0180 svg.selectAll("g[id=" + stageClusterId + "] g." + opClusterId).classed("barrier", true)
0181 });
0182
0183 resizeSvg(svg);
0184 interpretLineBreak(svg);
0185 }
0186
0187
0188 function renderDagVizForStage(svgContainer) {
0189 var metadata = metadataContainer().select(".stage-metadata");
0190 var dot = metadata.select(".dot-file").text().trim();
0191 var containerId = VizConstants.graphPrefix + metadata.attr("stage-id");
0192 var container = svgContainer.append("g").attr("id", containerId);
0193 renderDot(dot, container, false);
0194
0195
0196 svgContainer
0197 .selectAll("rect")
0198 .attr("rx", "5")
0199 .attr("ry", "5");
0200 }
0201
0202
0203
0204
0205
0206
0207
0208
0209
0210 function renderDagVizForJob(svgContainer) {
0211 var crossStageEdges = [];
0212
0213
0214
0215
0216 metadataContainer().selectAll(".stage-metadata").each(function(d, i) {
0217 var metadata = d3.select(this);
0218 var dot = metadata.select(".dot-file").text();
0219 var stageId = metadata.attr("stage-id");
0220 var containerId = VizConstants.graphPrefix + stageId;
0221 var isSkipped = metadata.attr("skipped") === "true";
0222 var container;
0223 if (isSkipped) {
0224 container = svgContainer
0225 .append("g")
0226 .attr("id", containerId)
0227 .attr("skipped", "true");
0228 } else {
0229
0230 var attemptId = 0;
0231 var stageLink = uiRoot + appBasePath + "/stages/stage/?id=" + stageId + "&attempt=" + attemptId;
0232 container = svgContainer
0233 .append("a")
0234 .attr("xlink:href", stageLink)
0235 .attr("onclick", "window.localStorage.setItem(expandDagVizArrowKey(false), true)")
0236 .append("g")
0237 .attr("id", containerId);
0238 }
0239
0240
0241
0242
0243 if (i > 0) {
0244 var existingStages = svgContainer.selectAll("g.cluster.stage");
0245 if (!existingStages.empty()) {
0246 var lastStage = d3.select(existingStages[0].pop());
0247 var lastStageWidth = toFloat(lastStage.select("rect").attr("width"));
0248 var lastStagePosition = getAbsolutePosition(lastStage);
0249 var offset = lastStagePosition.x + lastStageWidth + VizConstants.stageSep;
0250 container.attr("transform", "translate(" + offset + ", 0)");
0251 }
0252 }
0253
0254
0255 renderDot(dot, container, true);
0256
0257
0258
0259 if (isSkipped) {
0260 container.selectAll("g").classed("skipped", true);
0261 }
0262
0263
0264 container
0265 .selectAll("rect")
0266 .attr("rx", "4")
0267 .attr("ry", "4");
0268
0269
0270
0271
0272 metadata.selectAll(".incoming-edge").each(function(v) {
0273 var edge = d3.select(this).text().trim().split(",");
0274 crossStageEdges.push(edge);
0275 });
0276 });
0277
0278 addTooltipsForRDDs(svgContainer);
0279 drawCrossStageEdges(crossStageEdges, svgContainer);
0280 }
0281
0282
0283 function renderDot(dot, container, forJob) {
0284 var g = graphlibDot.read(dot);
0285 var renderer = new dagreD3.render();
0286 preprocessGraphLayout(g, forJob);
0287 renderer(container, g);
0288
0289
0290 container.selectAll("g.cluster[name^=\"Stage \"]").classed("stage", true);
0291 }
0292
0293
0294
0295
0296
0297
0298 function graphContainer() { return d3.select("#dag-viz-graph"); }
0299 function metadataContainer() { return d3.select("#dag-viz-metadata"); }
0300
0301
0302
0303
0304
0305
0306 function preprocessGraphLayout(g, forJob) {
0307 var nodes = g.nodes();
0308 for (var i = 0; i < nodes.length; i++) {
0309 var isCluster = g.children(nodes[i]).length > 0;
0310 if (!isCluster) {
0311 var node = g.node(nodes[i]);
0312 if (forJob) {
0313
0314 node.shape = "circle";
0315 node.labelStyle = "font-size: 0px";
0316 } else {
0317 node.labelStyle = "font-size: 12px";
0318 }
0319 node.padding = "5";
0320 }
0321 }
0322
0323 var edges = g.edges();
0324 for (var j = 0; j < edges.length; j++) {
0325 var edge = g.edge(edges[j]);
0326 edge.lineInterpolate = "basis";
0327 }
0328
0329 if (forJob) {
0330 g.graph().rankSep = JobPageVizConstants.rankSep;
0331 } else {
0332 g.graph().rankSep = StagePageVizConstants.rankSep;
0333 }
0334 }
0335
0336
0337
0338
0339
0340 function resizeSvg(svg) {
0341 var allClusters = svg.selectAll("g.cluster rect")[0];
0342 var startX = -VizConstants.svgMarginX +
0343 toFloat(d3.min(allClusters, function(e) {
0344 return getAbsolutePosition(d3.select(e)).x;
0345 }));
0346 var startY = -VizConstants.svgMarginY +
0347 toFloat(d3.min(allClusters, function(e) {
0348 return getAbsolutePosition(d3.select(e)).y;
0349 }));
0350 var endX = VizConstants.svgMarginX +
0351 toFloat(d3.max(allClusters, function(e) {
0352 var t = d3.select(e);
0353 return getAbsolutePosition(t).x + toFloat(t.attr("width"));
0354 }));
0355 var endY = VizConstants.svgMarginY +
0356 toFloat(d3.max(allClusters, function(e) {
0357 var t = d3.select(e);
0358 return getAbsolutePosition(t).y + toFloat(t.attr("height"));
0359 }));
0360 var width = endX - startX;
0361 var height = endY - startY;
0362 svg.attr("viewBox", startX + " " + startY + " " + width + " " + height)
0363 .attr("width", width)
0364 .attr("height", height);
0365 }
0366
0367
0368
0369
0370
0371
0372 function interpretLineBreak(svg) {
0373 svg.selectAll("tspan").each(function() {
0374 var node = d3.select(this);
0375 var original = node[0][0].innerHTML;
0376 if (original.indexOf("\\n") != -1) {
0377 var arr = original.split("\\n");
0378 var newNode = this.cloneNode(this);
0379
0380 node[0][0].innerHTML = arr[0];
0381 newNode.innerHTML = arr[1];
0382
0383 this.parentNode.appendChild(newNode);
0384 }
0385 });
0386 }
0387
0388
0389
0390
0391
0392 function drawCrossStageEdges(edges, svgContainer) {
0393 if (edges.length == 0) {
0394 return;
0395 }
0396
0397 var edgesContainer = svgContainer.append("g").attr("id", "cross-stage-edges");
0398 for (var i = 0; i < edges.length; i++) {
0399 var fromRDDId = edges[i][0];
0400 var toRDDId = edges[i][1];
0401 connectRDDs(fromRDDId, toRDDId, edgesContainer, svgContainer);
0402 }
0403
0404 var dagreD3Marker = svgContainer.select("g.edgePaths marker");
0405 if (!dagreD3Marker.empty()) {
0406 svgContainer
0407 .append(function() { return dagreD3Marker.node().cloneNode(true); })
0408 .attr("id", "marker-arrow");
0409 svgContainer.selectAll("g > path").attr("marker-end", "url(#marker-arrow)");
0410 svgContainer.selectAll("g.edgePaths def").remove();
0411 }
0412 }
0413
0414
0415
0416
0417
0418 function getAbsolutePosition(d3selection) {
0419 if (d3selection.empty()) {
0420 throw "Attempted to get absolute position of an empty selection.";
0421 }
0422 var obj = d3selection;
0423 var _x = toFloat(obj.attr("x")) || 0;
0424 var _y = toFloat(obj.attr("y")) || 0;
0425 while (!obj.empty()) {
0426 var transformText = obj.attr("transform");
0427 if (transformText) {
0428 var translate = d3.transform(transformText).translate;
0429 _x += toFloat(translate[0]);
0430 _y += toFloat(translate[1]);
0431 }
0432
0433 obj = d3.select(obj.node().parentNode);
0434
0435 if (obj.node() == graphContainer().node()) {
0436 break;
0437 }
0438 }
0439 return { x: _x, y: _y };
0440 }
0441
0442
0443 function connectRDDs(fromRDDId, toRDDId, edgesContainer, svgContainer) {
0444 var fromNodeId = VizConstants.nodePrefix + fromRDDId;
0445 var toNodeId = VizConstants.nodePrefix + toRDDId;
0446 var fromPos = getAbsolutePosition(svgContainer.select("g." + fromNodeId));
0447 var toPos = getAbsolutePosition(svgContainer.select("g." + toNodeId));
0448
0449
0450
0451
0452 var delta = toFloat(svgContainer
0453 .select("g.node." + toNodeId)
0454 .select("circle")
0455 .attr("r"));
0456 if (fromPos.x < toPos.x) {
0457 fromPos.x += delta;
0458 toPos.x -= delta;
0459 } else if (fromPos.x > toPos.x) {
0460 fromPos.x -= delta;
0461 toPos.x += delta;
0462 }
0463
0464 var points;
0465 if (fromPos.y == toPos.y) {
0466
0467
0468
0469
0470 points = [
0471 [fromPos.x, fromPos.y],
0472 [fromPos.x + (toPos.x - fromPos.x) * 0.2, fromPos.y],
0473 [fromPos.x + (toPos.x - fromPos.x) * 0.3, fromPos.y - 20],
0474 [fromPos.x + (toPos.x - fromPos.x) * 0.7, fromPos.y - 20],
0475 [fromPos.x + (toPos.x - fromPos.x) * 0.8, toPos.y],
0476 [toPos.x, toPos.y]
0477 ];
0478 } else {
0479
0480
0481
0482
0483
0484 points = [
0485 [fromPos.x, fromPos.y],
0486 [fromPos.x + (toPos.x - fromPos.x) * 0.4, fromPos.y],
0487 [fromPos.x + (toPos.x - fromPos.x) * 0.6, toPos.y],
0488 [toPos.x, toPos.y]
0489 ];
0490 }
0491
0492 var line = d3.svg.line().interpolate("basis");
0493 edgesContainer.append("path").datum(points).attr("d", line);
0494 }
0495
0496
0497 function addTooltipsForRDDs(svgContainer) {
0498 svgContainer.selectAll("g.node").each(function() {
0499 var node = d3.select(this);
0500 var tooltipText = node.attr("name");
0501 if (tooltipText) {
0502 node.select("circle")
0503 .attr("data-toggle", "tooltip")
0504 .attr("data-placement", "top")
0505 .attr("data-html", "true")
0506 .attr("title", tooltipText);
0507 }
0508
0509 node.on("mouseenter", function() { triggerTooltipForRDD(node, true); });
0510 node.on("mouseleave", function() { triggerTooltipForRDD(node, false); });
0511 });
0512
0513 $("[data-toggle=tooltip]")
0514 .filter("g.node circle")
0515 .tooltip({ container: "body", trigger: "manual" });
0516 }
0517
0518
0519
0520
0521
0522 function triggerTooltipForRDD(d3node, show) {
0523 var classes = d3node.node().classList;
0524 for (var i = 0; i < classes.length; i++) {
0525 var clazz = classes[i];
0526 var isRDDClass = clazz.indexOf(VizConstants.nodePrefix) == 0;
0527 if (isRDDClass) {
0528 graphContainer().selectAll("g." + clazz).each(function() {
0529 var circle = d3.select(this).select("circle").node();
0530 var showOrHide = show ? "show" : "hide";
0531 $(circle).tooltip(showOrHide);
0532 });
0533 }
0534 }
0535 }
0536
0537
0538 function toFloat(f) {
0539 if (f) {
0540 return parseFloat(f.toString().replace(/px$/, ""));
0541 } else {
0542 return f;
0543 }
0544 }
0545