Skip to content

Commit 205f838

Browse files
author
Andrew Or
committed
Reimplement rendering with dagre-d3 instead of viz.js
Before this commit, this patch relies on a JavaScript version of GraphViz that was compiled from C. Even the minified version of this resource was ~2.5M. The main motivation for switching away from this library, however, is that this is a complete black box of which we have absolutely no control. It is not at all extensible, and if something breaks we will have a hard time understanding why. The new library, dagre-d3, is not perfect either. It does not officially support clustering of nodes; for certain large graphs, the clusters will have a lot of unnecessary whitespace. A few in the dagre-d3 community are looking into a solution, but until then we will have to live with this (minor) inconvenience.
1 parent 5e22946 commit 205f838

File tree

8 files changed

+158
-1336
lines changed

8 files changed

+158
-1336
lines changed

core/src/main/resources/org/apache/spark/ui/static/d3.min.js

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/src/main/resources/org/apache/spark/ui/static/dagre-d3.min.js

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/src/main/resources/org/apache/spark/ui/static/graphlib-dot.min.js

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/*
19+
* Render a DAG that describes the RDDs for a given stage.
20+
*
21+
* Input: The content of a dot file, stored in the text of the "#viz-dot-file" element
22+
* Output: An SVG that visualizes the DAG, stored in the "#viz-graph" element
23+
*
24+
* This relies on a custom implementation of dagre-d3, which can be found under
25+
* http://github.com/andrewor14/dagre-d3/dist/dagre-d3.js. For more detail, please
26+
* track the changes in that project after it was forked.
27+
*/
28+
function renderStageViz() {
29+
30+
// Parse the dot file and render it in an SVG
31+
var dot = d3.select("#viz-dot-file").text();
32+
var escaped_dot = dot
33+
.replace(/&lt;/g, "<")
34+
.replace(/&gt;/g, ">")
35+
.replace(/&quot;/g, "\"");
36+
var g = graphlibDot.read(escaped_dot);
37+
var render = new dagreD3.render();
38+
var svg = d3.select("#viz-graph");
39+
svg.call(render, g);
40+
41+
// Set the appropriate SVG dimensions to ensure that all elements are displayed
42+
var svgMargin = 20;
43+
var boundingBox = svg.node().getBBox();
44+
svg.style("width", (boundingBox.width + svgMargin) + "px");
45+
svg.style("height", (boundingBox.height + svgMargin) + "px");
46+
47+
// Add style to clusters, nodes and edges
48+
d3.selectAll("svg g.cluster rect")
49+
.style("fill", "none")
50+
.style("stroke", "#AADFFF")
51+
.style("stroke-width", "4px")
52+
.style("stroke-opacity", "0.5");
53+
d3.selectAll("svg g.node rect")
54+
.style("fill", "white")
55+
.style("stroke", "black")
56+
.style("stroke-width", "2px")
57+
.style("fill-opacity", "0.8")
58+
.style("stroke-opacity", "0.9");
59+
d3.selectAll("svg g.edgePath path")
60+
.style("stroke", "black")
61+
.style("stroke-width", "2px");
62+
63+
// Add labels to clusters
64+
d3.selectAll("svg g.cluster").each(function(cluster_data) {
65+
var cluster = d3.select(this);
66+
cluster.selectAll("rect").each(function(rect_data) {
67+
var rect = d3.select(this);
68+
// Shift the boxes up a little
69+
rect.attr("y", toFloat(rect.attr("y")) - 10);
70+
rect.attr("height", toFloat(rect.attr("height")) + 10);
71+
var labelX = toFloat(rect.attr("x")) + toFloat(rect.attr("width")) - 5;
72+
var labelY = toFloat(rect.attr("y")) + 15;
73+
var labelText = cluster.attr("id").replace(/^cluster/, "").replace(/_.*$/, "");
74+
cluster.append("text")
75+
.attr("x", labelX)
76+
.attr("y", labelY)
77+
.attr("fill", "#AAAAAA")
78+
.attr("font-size", "11px")
79+
.attr("text-anchor", "end")
80+
.text(labelText);
81+
});
82+
});
83+
84+
// We have shifted a few elements upwards, so we should fix the SVG views
85+
var startX = -svgMargin;
86+
var startY = -svgMargin;
87+
var endX = toFloat(svg.style("width")) + svgMargin;
88+
var endY = toFloat(svg.style("height")) + svgMargin;
89+
var newViewBox = startX + " " + startY + " " + endX + " " + endY;
90+
svg.attr("viewBox", newViewBox);
91+
92+
}
93+
94+
/* Helper method to convert attributes to numeric values. */
95+
function toFloat(f) {
96+
return parseFloat(f.replace(/px$/, ""))
97+
}
98+

core/src/main/resources/org/apache/spark/ui/static/viz.js

Lines changed: 0 additions & 1302 deletions
This file was deleted.

core/src/main/scala/org/apache/spark/ui/UIUtils.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,10 @@ private[spark] object UIUtils extends Logging {
168168
}
169169

170170
def vizHeaderNodes: Seq[Node] = {
171-
<script src={prependBaseUri("/static/viz.js")}></script>
171+
<script src={prependBaseUri("/static/d3.min.js")}></script>
172+
<script src={prependBaseUri("/static/dagre-d3.min.js")}></script>
173+
<script src={prependBaseUri("/static/graphlib-dot.min.js")}></script>
174+
<script src={prependBaseUri("/static/spark-stage-viz.js")}></script>
172175
}
173176

174177
/** Returns a spark page with correctly formatted headers */

core/src/main/scala/org/apache/spark/ui/jobs/StagePage.scala

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,13 @@ private[ui] class StagePage(parent: StagesTab) extends WebUIPage("stage") {
4141
if (graph.isEmpty) {
4242
return Seq.empty
4343
}
44-
val viz = <div id="stage-viz">{VizGraph.makeDotFile(graph.get)}</div>
45-
val script = {
46-
<script type="text/javascript">
47-
<xml:unparsed>
48-
var dot = document.getElementById("stage-viz").innerHTML;
49-
var dot = dot.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"");
50-
console.log(dot);
51-
var viz = Viz(dot, "svg", "dot");
52-
document.getElementById("stage-viz").innerHTML = viz;
53-
</xml:unparsed>
54-
</script>
44+
{
45+
<div id="viz-dot-file" style="display:none">
46+
{VizGraph.makeDotFile(graph.get)}
47+
</div>
48+
<svg id="viz-graph"></svg>
49+
<script type="text/javascript">renderStageViz()</script>
5550
}
56-
viz ++ script
5751
}
5852

5953
def render(request: HttpServletRequest): Seq[Node] = {

core/src/main/scala/org/apache/spark/ui/viz/VizGraph.scala

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ private[ui] object VizGraph {
8181
while (scopeIt.hasNext) {
8282
val scopeId = scopeIt.next()
8383
val scope = scopes.getOrElseUpdate(scopeId, new VizScope(scopeId))
84-
scope.attachChildNode(node)
84+
// Only attach this node to the innermost scope
85+
if (!scopeIt.hasNext) {
86+
scope.attachChildNode(node)
87+
}
8588
// RDD scopes are hierarchical, with the outermost scopes ordered first
8689
// If there is not a previous scope, then this must be a root scope
8790
if (previousScope == null) {
@@ -106,48 +109,35 @@ private[ui] object VizGraph {
106109
*/
107110
def makeDotFile(graph: VizGraph): String = {
108111
val dotFile = new StringBuilder
109-
dotFile.append(
110-
"""
111-
|digraph G {
112-
| node[fontsize="12", style="rounded, bold", shape="box"]
113-
| graph[labeljust="r", style="bold", color="#DDDDDD", fontsize="10"]
114-
""".stripMargin.trim)
112+
dotFile.append("digraph G {\n")
115113
//
116114
graph.rootScopes.foreach { scope =>
117115
dotFile.append(makeDotSubgraph(scope, " "))
118116
}
119117
//
120118
graph.rootNodes.foreach { node =>
121-
dotFile.append(" " + makeDotNode(node) + "\n")
119+
dotFile.append(s" ${makeDotNode(node)};\n")
122120
}
123121
//
124122
graph.edges.foreach { edge =>
125-
dotFile.append(" " + edge.fromId + "->" + edge.toId + "\n")
123+
dotFile.append(s" ${edge.fromId}->${edge.toId};\n")
126124
}
127125
dotFile.append("}")
126+
println(dotFile.toString())
128127
dotFile.toString()
129128
}
130129

131130
/** */
132131
private def makeDotNode(node: VizNode): String = {
133-
val dnode = new StringBuilder
134-
dnode.append(node.id)
135-
dnode.append(s""" [label="${node.name}"""")
136-
if (node.isCached) {
137-
dnode.append(s""", URL="/storage/rdd/?id=${node.id}", color="red"""")
138-
}
139-
dnode.append("]")
140-
dnode.toString()
132+
s"""${node.id} [label="${node.name}"]"""
141133
}
142134

143135
/** */
144136
private def makeDotSubgraph(scope: VizScope, indent: String): String = {
145137
val subgraph = new StringBuilder
146-
subgraph.append(indent + "subgraph cluster" + scope.id + " {\n")
147-
subgraph.append(indent + " label=\"" + scope.name + "\"\n")
148-
subgraph.append(indent + " fontcolor=\"#AAAAAA\"\n")
138+
subgraph.append(indent + s"subgraph cluster${scope.id} {\n")
149139
scope.childrenNodes.foreach { node =>
150-
subgraph.append(indent + " " + makeDotNode(node) + "\n")
140+
subgraph.append(indent + s" ${makeDotNode(node)};\n")
151141
}
152142
scope.childrenScopes.foreach { cscope =>
153143
subgraph.append(makeDotSubgraph(cscope, indent + " "))

0 commit comments

Comments
 (0)