From a1ec08f7edc8d956afcfbb92d10b26b7619486e8 Mon Sep 17 00:00:00 2001 From: Matei Zaharia <matei@databricks.com> Date: Fri, 8 May 2015 14:41:42 -0400 Subject: [PATCH] [SPARK-7298] Harmonize style of new visualizations - Colors on the timeline now match the rest of the UI - The expandable buttons to show timeline view, DAG, etc are now more visible - Timeline text is smaller - DAG visualization text and colors are more consistent throughout - Fix some JavaScript style issues - Various small fixes throughout (e.g. inconsistent capitalization, some confusing names, HTML escaping, etc) Author: Matei Zaharia <matei@databricks.com> Closes #5942 from mateiz/ui and squashes the following commits: def38d0 [Matei Zaharia] Add some tooltips 4c5a364 [Matei Zaharia] Reduce stage and rank separation slightly 43dcbe3 [Matei Zaharia] Some updates to DAG fac734a [Matei Zaharia] tweaks 6a6705d [Matei Zaharia] More fixes 67629f5 [Matei Zaharia] Various small tweaks --- .../apache/spark/ui/static/spark-dag-viz.css | 60 +++++--- .../apache/spark/ui/static/spark-dag-viz.js | 57 ++++---- .../apache/spark/ui/static/timeline-view.css | 128 +++++++++++------- .../apache/spark/ui/static/timeline-view.js | 4 +- .../org/apache/spark/ui/static/webui.css | 36 +++-- .../scala/org/apache/spark/ui/ToolTips.scala | 19 +++ .../scala/org/apache/spark/ui/UIUtils.scala | 15 +- .../apache/spark/ui/jobs/AllJobsPage.scala | 31 +++-- .../apache/spark/ui/jobs/AllStagesPage.scala | 15 +- .../org/apache/spark/ui/jobs/JobPage.scala | 23 ++-- .../org/apache/spark/ui/jobs/StagePage.scala | 16 +-- .../spark/ui/scope/RDDOperationGraph.scala | 2 +- 12 files changed, 255 insertions(+), 151 deletions(-) diff --git a/core/src/main/resources/org/apache/spark/ui/static/spark-dag-viz.css b/core/src/main/resources/org/apache/spark/ui/static/spark-dag-viz.css index 8481710828..18c72694f3 100644 --- a/core/src/main/resources/org/apache/spark/ui/static/spark-dag-viz.css +++ b/core/src/main/resources/org/apache/spark/ui/static/spark-dag-viz.css @@ -16,40 +16,51 @@ */ #dag-viz-graph svg path { - stroke: #444444; + stroke: #444; stroke-width: 1.5px; } #dag-viz-graph svg g.cluster rect { - stroke-width: 4px; - stroke-opacity: 0.5; + stroke-width: 1px; +} + +#dag-viz-graph svg g.node circle { + fill: #444; } -#dag-viz-graph svg g.node circle, #dag-viz-graph svg g.node rect { - fill: #444444; + fill: #C3EBFF; + stroke: #3EC0FF; + stroke-width: 1px; +} + +#dag-viz-graph svg g.node.cached circle { + fill: #444; } -#dag-viz-graph svg g.node.cached circle, #dag-viz-graph svg g.node.cached rect { - fill: #FF0000; + fill: #B3F5C5; + stroke: #56F578; + stroke-width: 1px; } /* Job page specific styles */ #dag-viz-graph svg.job marker#marker-arrow path { - fill: #444444; + fill: #333; stroke-width: 0px; } #dag-viz-graph svg.job g.cluster rect { - fill: #FFFFFF; - stroke: #AADFFF; + fill: #A0DFFF; + stroke: #3EC0FF; + stroke-width: 1px; } #dag-viz-graph svg.job g.cluster[id*="stage"] rect { - stroke: #FFDDEE; - stroke-width: 6px; + fill: #FFFFFF; + stroke: #FF99AC; + stroke-width: 1px; } #dag-viz-graph svg.job g#cross-stage-edges path { @@ -57,27 +68,36 @@ } #dag-viz-graph svg.job g.cluster text { - fill: #AAAAAA; + fill: #333; } /* Stage page specific styles */ #dag-viz-graph svg.stage g.cluster rect { - fill: #F0F8FF; - stroke: #AADFFF; + fill: #A0DFFF; + stroke: #3EC0FF; + stroke-width: 1px; } #dag-viz-graph svg.stage g.cluster[id*="stage"] rect { fill: #FFFFFF; - stroke: #FFDDEE; - stroke-width: 6px; + stroke: #FFA6B6; + stroke-width: 1px; } #dag-viz-graph svg.stage g.node g.label text tspan { - fill: #FFFFFF; + fill: #333; } #dag-viz-graph svg.stage g.cluster text { - fill: #444444; - font-weight: bold; + fill: #333; +} + +#dag-viz-graph a, #dag-viz-graph a:hover { + text-decoration: none; +} + +#dag-viz-graph .label { + font-weight: normal; + text-shadow: none; } diff --git a/core/src/main/resources/org/apache/spark/ui/static/spark-dag-viz.js b/core/src/main/resources/org/apache/spark/ui/static/spark-dag-viz.js index a0e3e914c2..764dd2cfcd 100644 --- a/core/src/main/resources/org/apache/spark/ui/static/spark-dag-viz.js +++ b/core/src/main/resources/org/apache/spark/ui/static/spark-dag-viz.js @@ -52,9 +52,9 @@ */ var VizConstants = { - svgMarginX: 20, - svgMarginY: 20, - stageSep: 50, + svgMarginX: 16, + svgMarginY: 16, + stageSep: 40, graphPrefix: "graph_", nodePrefix: "node_", stagePrefix: "stage_", @@ -63,14 +63,16 @@ var VizConstants = { }; var JobPageVizConstants = { - clusterLabelSize: 11, - stageClusterLabelSize: 14 -} + clusterLabelSize: 12, + stageClusterLabelSize: 14, + rankSep: 40 +}; var StagePageVizConstants = { clusterLabelSize: 14, - stageClusterLabelSize: 18 -} + stageClusterLabelSize: 14, + rankSep: 40 +}; /* * Show or hide the RDD DAG visualization. @@ -149,11 +151,11 @@ function renderDagVizForStage(svgContainer) { var dot = metadata.select(".dot-file").text(); var containerId = VizConstants.graphPrefix + metadata.attr("stage-id"); var container = svgContainer.append("g").attr("id", containerId); - renderDot(dot, container); + renderDot(dot, container, StagePageVizConstants.rankSep); - // Round corners on RDDs + // Round corners on rectangles svgContainer - .selectAll("g.node rect") + .selectAll("rect") .attr("rx", "5") .attr("ry", "5"); } @@ -207,7 +209,13 @@ function renderDagVizForJob(svgContainer) { } // Actually render the stage - renderDot(dot, container); + renderDot(dot, container, JobPageVizConstants.rankSep); + + // Round corners on rectangles + container + .selectAll("rect") + .attr("rx", "4") + .attr("ry", "4"); // If there are any incoming edges into this graph, keep track of them to render // them separately later. Note that we cannot draw them now because we need to @@ -223,12 +231,13 @@ function renderDagVizForJob(svgContainer) { } /* Render the dot file as an SVG in the given container. */ -function renderDot(dot, container) { +function renderDot(dot, container, rankSep) { var escaped_dot = dot .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, "\""); var g = graphlibDot.read(escaped_dot); + g.graph().rankSep = rankSep; var renderer = new dagreD3.render(); renderer(container, g); } @@ -248,12 +257,13 @@ function metadataContainer() { return d3.select("#dag-viz-metadata"); } * In general, the clustering support for dagre-d3 is quite limited at this point. */ function drawClusterLabels(svgContainer, forJob) { + var clusterLabelSize, stageClusterLabelSize; if (forJob) { - var clusterLabelSize = JobPageVizConstants.clusterLabelSize; - var stageClusterLabelSize = JobPageVizConstants.stageClusterLabelSize; + clusterLabelSize = JobPageVizConstants.clusterLabelSize; + stageClusterLabelSize = JobPageVizConstants.stageClusterLabelSize; } else { - var clusterLabelSize = StagePageVizConstants.clusterLabelSize; - var stageClusterLabelSize = StagePageVizConstants.stageClusterLabelSize; + clusterLabelSize = StagePageVizConstants.clusterLabelSize; + stageClusterLabelSize = StagePageVizConstants.stageClusterLabelSize; } svgContainer.selectAll("g.cluster").each(function() { var cluster = d3.select(this); @@ -283,7 +293,7 @@ function drawClusterLabel(d3cluster, fontSize) { .attr("x", labelX) .attr("y", labelY) .attr("text-anchor", "end") - .style("font-size", fontSize) + .style("font-size", fontSize + "px") .text(labelText); } @@ -303,12 +313,12 @@ function resizeSvg(svg) { })); var endX = VizConstants.svgMarginX + toFloat(d3.max(allClusters, function(e) { - var t = d3.select(e) + var t = d3.select(e); return getAbsolutePosition(t).x + toFloat(t.attr("width")); })); var endY = VizConstants.svgMarginY + toFloat(d3.max(allClusters, function(e) { - var t = d3.select(e) + var t = d3.select(e); return getAbsolutePosition(t).y + toFloat(t.attr("height")); })); var width = endX - startX; @@ -338,7 +348,7 @@ function drawCrossStageEdges(edges, svgContainer) { if (!dagreD3Marker.empty()) { svgContainer .append(function() { return dagreD3Marker.node().cloneNode(true); }) - .attr("id", "marker-arrow") + .attr("id", "marker-arrow"); svgContainer.selectAll("g > path").attr("marker-end", "url(#marker-arrow)"); svgContainer.selectAll("g.edgePaths def").remove(); // We no longer need these } @@ -394,12 +404,13 @@ function connectRDDs(fromRDDId, toRDDId, edgesContainer, svgContainer) { toPos.x += delta; } + var points; if (fromPos.y == toPos.y) { // If they are on the same rank, curve the middle part of the edge // upward a little to avoid interference with things in between // e.g. _______ // _____/ \_____ - var points = [ + points = [ [fromPos.x, fromPos.y], [fromPos.x + (toPos.x - fromPos.x) * 0.2, fromPos.y], [fromPos.x + (toPos.x - fromPos.x) * 0.3, fromPos.y - 20], @@ -413,7 +424,7 @@ function connectRDDs(fromRDDId, toRDDId, edgesContainer, svgContainer) { // / // | // _____/ - var points = [ + points = [ [fromPos.x, fromPos.y], [fromPos.x + (toPos.x - fromPos.x) * 0.4, fromPos.y], [fromPos.x + (toPos.x - fromPos.x) * 0.6, toPos.y], diff --git a/core/src/main/resources/org/apache/spark/ui/static/timeline-view.css b/core/src/main/resources/org/apache/spark/ui/static/timeline-view.css index 35ef14e5aa..d40de70422 100644 --- a/core/src/main/resources/org/apache/spark/ui/static/timeline-view.css +++ b/core/src/main/resources/org/apache/spark/ui/static/timeline-view.css @@ -23,6 +23,10 @@ div#application-timeline, div#job-timeline { margin-top: 5px; } +.vis.timeline { + line-height: 14px; +} + .vis.timeline div.content { width: 100%; } @@ -32,48 +36,55 @@ div#application-timeline, div#job-timeline { } .vis.timeline .item.stage.succeeded { - background-color: #D5DDF6; + background-color: #A0DFFF; + border-color: #3EC0FF; } .vis.timeline .item.stage.succeeded.selected { - background-color: #D5DDF6; - border-color: #97B0F8; - z-index: auto; + background-color: #A0DFFF; + border-color: #3EC0FF; + z-index: auto; } .legend-area rect.completed-stage-legend { - fill: #D5DDF6; - stroke: #97B0F8; + fill: #A0DFFF; + stroke: #3EC0FF; } .vis.timeline .item.stage.failed { - background-color: #FF5475; + background-color: #FFA1B0; + border-color: #FF4D6D; } .vis.timeline .item.stage.failed.selected { - background-color: #FF5475; - border-color: #97B0F8; - z-index: auto; + background-color: #FFA1B0; + border-color: #FF4D6D; + z-index: auto; } .legend-area rect.failed-stage-legend { - fill: #FF5475; - stroke: #97B0F8; + fill: #FFA1B0; + stroke: #FF4D6D; } .vis.timeline .item.stage.running { - background-color: #FDFFCA; + background-color: #A2FCC0; + border-color: #36F572; } .vis.timeline .item.stage.running.selected { - background-color: #FDFFCA; - border-color: #97B0F8; - z-index: auto; + background-color: #A2FCC0; + border-color: #36F572; + z-index: auto; } .legend-area rect.active-stage-legend { - fill: #FDFFCA; - stroke: #97B0F8; + fill: #A2FCC0; + stroke: #36F572; +} + +.vis.timeline .foreground { + cursor: move; } .vis.timeline .item.job { @@ -81,76 +92,81 @@ div#application-timeline, div#job-timeline { } .vis.timeline .item.job.succeeded { - background-color: #D5DDF6; + background-color: #A0DFFF; + border-color: #3EC0FF; } .vis.timeline .item.job.succeeded.selected { - background-color: #D5DDF6; - border-color: #97B0F8; - z-index: auto; + background-color: #A0DFFF; + border-color: #3EC0FF; + z-index: auto; } .legend-area rect.succeeded-job-legend { - fill: #D5DDF6; - stroke: #97B0F8; + fill: #A0DFFF; + stroke: #3EC0FF; } .vis.timeline .item.job.failed { - background-color: #FF5475; + background-color: #FFA1B0; + border-color: #FF4D6D; } .vis.timeline .item.job.failed.selected { - background-color: #FF5475; - border-color: #97B0F8; - z-index: auto; + background-color: #FFA1B0; + border-color: #FF4D6D; + z-index: auto; } .legend-area rect.failed-job-legend { - fill: #FF5475; - stroke: #97B0F8; + fill: #FFA1B0; + stroke: #FF4D6D; } .vis.timeline .item.job.running { - background-color: #FDFFCA; + background-color: #A2FCC0; + border-color: #36F572; } .vis.timeline .item.job.running.selected { - background-color: #FDFFCA; - border-color: #97B0F8; - z-index: auto; + background-color: #A2FCC0; + border-color: #36F572; + z-index: auto; } .legend-area rect.running-job-legend { - fill: #FDFFCA; - stroke: #97B0F8; + fill: #A2FCC0; + stroke: #36F572; } .vis.timeline .item.executor.added { - background-color: #D5DDF6; + background-color: #A0DFFF; + border-color: #3EC0FF; } .legend-area rect.executor-added-legend { - fill: #D5DDF6; - stroke: #97B0F8; + fill: #A0DFFF; + stroke: #3EC0FF; } .vis.timeline .item.executor.removed { - background-color: #EBCA59; + background-color: #FFA1B0; + border-color: #FF4D6D; } .legend-area rect.executor-removed-legend { - fill: #EBCA59; - stroke: #97B0F8; + fill: #FFA1B0; + stroke: #FF4D6D; } .vis.timeline .item.executor.selected { - border-color: #FFC200; - background-color: #FFF785; + background-color: #A2FCC0; + border-color: #36F572; z-index: 2; } -tr.corresponding-item-hover>td, tr.corresponding-item-hover>th { - background-color: #FFE1FA !important; +tr.corresponding-item-hover > td, tr.corresponding-item-hover > th { + background-color: #D6FFE4 !important; } #application-timeline.collapsed { @@ -165,11 +181,15 @@ tr.corresponding-item-hover>td, tr.corresponding-item-hover>th { margin-bottom: 5px; } +.control-panel input[type="checkbox"] { + margin: 0; +} + span.expand-application-timeline, span.expand-job-timeline { cursor: pointer; } -.control-panel input+span { +.control-panel input + span { cursor: pointer; } @@ -180,3 +200,17 @@ span.expand-application-timeline, span.expand-job-timeline { .vis.timeline .item .tooltip-inner { max-width: unset !important; } + +.vispanel.center { + font-size: 12px; + line-height: 12px; +} + +.legend-area text { + fill: #4D4D4D; +} + +.additional-metrics ul { + list-style: none; + margin-left: 15px; +} diff --git a/core/src/main/resources/org/apache/spark/ui/static/timeline-view.js b/core/src/main/resources/org/apache/spark/ui/static/timeline-view.js index e4a891d47f..48fbb33b11 100644 --- a/core/src/main/resources/org/apache/spark/ui/static/timeline-view.js +++ b/core/src/main/resources/org/apache/spark/ui/static/timeline-view.js @@ -156,9 +156,9 @@ function setupExecutorEventAction() { function setupZoomable(id, timeline) { $(id + '>input[type="checkbox"]').click(function() { if (this.checked) { - timeline.setOptions({zoomable: false}); - } else { timeline.setOptions({zoomable: true}); + } else { + timeline.setOptions({zoomable: false}); } }); diff --git a/core/src/main/resources/org/apache/spark/ui/static/webui.css b/core/src/main/resources/org/apache/spark/ui/static/webui.css index 669ad48937..e7c1d475d4 100644 --- a/core/src/main/resources/org/apache/spark/ui/static/webui.css +++ b/core/src/main/resources/org/apache/spark/ui/static/webui.css @@ -106,14 +106,18 @@ span.rest-uri { } pre { - font-size: 0.8em; + font-size: 12px; + line-height: 18px; + padding: 6px; + margin: 0; + border-radius: 3px; } .stage-details { max-height: 100px; overflow-y: auto; margin: 0; - transition: max-height 0.5s ease-out, padding 0.5s ease-out; + transition: max-height 0.25s ease-out, padding 0.25s ease-out; } .stage-details.collapsed { @@ -135,7 +139,7 @@ pre { max-height: 300px; overflow-y: auto; margin: 0; - transition: max-height 0.5s ease-out, padding 0.5s ease-out; + transition: max-height 0.25s ease-out, padding 0.25s ease-out; } .stacktrace-details.collapsed { @@ -158,7 +162,7 @@ span.additional-metric-title { } .tooltip { - font-weight: normal; + font-weight: normal; } .arrow-open { @@ -166,9 +170,9 @@ span.additional-metric-title { height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; - border-top: 5px solid black; - float: left; - margin-top: 6px; + border-top: 5px solid #08c; + display: inline-block; + margin-bottom: 2px; } .arrow-closed { @@ -176,8 +180,10 @@ span.additional-metric-title { height: 0; border-top: 5px solid transparent; border-bottom: 5px solid transparent; - border-left: 5px solid black; + border-left: 5px solid #08c; display: inline-block; + margin-left: 2px; + margin-right: 3px; } .version { @@ -196,3 +202,17 @@ span.additional-metric-title { .serialization_time, .getting_result_time { display: none; } + +.accordion-inner { + background: #f5f5f5; +} + +.accordion-inner pre { + border: 0; + padding: 0; + background: none; +} + +a.expandbutton { + cursor: pointer; +} diff --git a/core/src/main/scala/org/apache/spark/ui/ToolTips.scala b/core/src/main/scala/org/apache/spark/ui/ToolTips.scala index 24f3236456..063e2a1f8b 100644 --- a/core/src/main/scala/org/apache/spark/ui/ToolTips.scala +++ b/core/src/main/scala/org/apache/spark/ui/ToolTips.scala @@ -57,4 +57,23 @@ private[spark] object ToolTips { val GC_TIME = """Time that the executor spent paused for Java garbage collection while the task was running.""" + + val JOB_TIMELINE = + """Shows when jobs started and ended and when executors joined or left. Drag to scroll. + Click Enable Zooming and use mouse wheel to zoom in/out.""" + + val STAGE_TIMELINE = + """Shows when stages started and ended and when executors joined or left. Drag to scroll. + Click Enable Zooming and use mouse wheel to zoom in/out.""" + + val JOB_DAG = + """Shows a graph of stages executed for this job, each of which can contain + multiple RDD operations (e.g. map() and filter()), and of RDDs inside each operation + (shown as dots).""" + + val STAGE_DAG = + """Shows a graph of RDD operations in this stage, and RDDs inside each one. A stage can run + multiple operations (e.g. two map() functions) if they can be pipelined. Some operations + also create multiple RDDs internally. Cached RDDs are shown in green. + """ } diff --git a/core/src/main/scala/org/apache/spark/ui/UIUtils.scala b/core/src/main/scala/org/apache/spark/ui/UIUtils.scala index 97eed13c2d..6a0f5c5d16 100644 --- a/core/src/main/scala/org/apache/spark/ui/UIUtils.scala +++ b/core/src/main/scala/org/apache/spark/ui/UIUtils.scala @@ -156,10 +156,10 @@ private[spark] object UIUtils extends Logging { def commonHeaderNodes: Seq[Node] = { <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> - <link rel="stylesheet" href={prependBaseUri("/static/bootstrap.min.css")} type="text/css" /> - <link rel="stylesheet" href={prependBaseUri("/static/webui.css")} type="text/css" /> - <link rel="stylesheet" href={prependBaseUri("/static/vis.min.css")} type="text/css" /> - <link rel="stylesheet" href={prependBaseUri("/static/timeline-view.css")} type="text/css" /> + <link rel="stylesheet" href={prependBaseUri("/static/bootstrap.min.css")} type="text/css"/> + <link rel="stylesheet" href={prependBaseUri("/static/vis.min.css")} type="text/css"/> + <link rel="stylesheet" href={prependBaseUri("/static/webui.css")} type="text/css"/> + <link rel="stylesheet" href={prependBaseUri("/static/timeline-view.css")} type="text/css"/> <script src={prependBaseUri("/static/sorttable.js")} ></script> <script src={prependBaseUri("/static/jquery-1.11.1.min.js")}></script> <script src={prependBaseUri("/static/vis.min.js")}></script> @@ -250,7 +250,7 @@ private[spark] object UIUtils extends Logging { <h3 style="vertical-align: middle; display: inline-block;"> <a style="text-decoration: none" href={prependBaseUri("/")}> <img src={prependBaseUri("/static/spark-logo-77x50px-hd.png")} /> - <span class="version" + <span class="version" style="margin-right: 15px;">{org.apache.spark.SPARK_VERSION}</span> </a> {title} @@ -350,7 +350,10 @@ private[spark] object UIUtils extends Logging { <div> <span class="expand-dag-viz" onclick={s"toggleDagViz($forJob);"}> <span class="expand-dag-viz-arrow arrow-closed"></span> - <strong>DAG visualization</strong> + <a data-toggle="tooltip" title={if (forJob) ToolTips.JOB_DAG else ToolTips.STAGE_DAG} + data-placement="right"> + DAG Visualization + </a> </span> <div id="dag-viz-graph"></div> <div id="dag-viz-metadata"> diff --git a/core/src/main/scala/org/apache/spark/ui/jobs/AllJobsPage.scala b/core/src/main/scala/org/apache/spark/ui/jobs/AllJobsPage.scala index 09323d1d80..e010ebef3b 100644 --- a/core/src/main/scala/org/apache/spark/ui/jobs/AllJobsPage.scala +++ b/core/src/main/scala/org/apache/spark/ui/jobs/AllJobsPage.scala @@ -18,12 +18,12 @@ package org.apache.spark.ui.jobs import scala.collection.mutable.{HashMap, ListBuffer} -import scala.xml.{Node, NodeSeq, Unparsed} +import scala.xml.{Node, NodeSeq, Unparsed, Utility} import java.util.Date import javax.servlet.http.HttpServletRequest -import org.apache.spark.ui.{UIUtils, WebUIPage} +import org.apache.spark.ui.{ToolTips, UIUtils, WebUIPage} import org.apache.spark.ui.jobs.UIData.{ExecutorUIData, JobUIData} import org.apache.spark.JobExecutionStatus @@ -81,6 +81,9 @@ private[ui] class AllJobsPage(parent: JobsTab) extends WebUIPage("") { case JobExecutionStatus.RUNNING => "running" } + // The timeline library treats contents as HTML, so we have to escape them; for the + // data-title attribute string we have to escape them twice since that's in a string. + val escapedDesc = Utility.escape(displayJobDescription) val jobEventJsonAsStr = s""" |{ @@ -90,16 +93,17 @@ private[ui] class AllJobsPage(parent: JobsTab) extends WebUIPage("") { | 'end': new Date(${completionTime}), | 'content': '<div class="application-timeline-content"' + | 'data-html="true" data-placement="top" data-toggle="tooltip"' + - | 'data-title="${displayJobDescription} (Job ${jobId})<br>Status: ${status}<br>' + - | 'Submission Time: ${UIUtils.formatDate(new Date(submissionTime))}' + + | 'data-title="${Utility.escape(escapedDesc)} (Job ${jobId})<br>' + + | 'Status: ${status}<br>' + + | 'Submitted: ${UIUtils.formatDate(new Date(submissionTime))}' + | '${ if (status != JobExecutionStatus.RUNNING) { - s"""<br>Completion Time: ${UIUtils.formatDate(new Date(completionTime))}""" + s"""<br>Completed: ${UIUtils.formatDate(new Date(completionTime))}""" } else { "" } }">' + - | '${displayJobDescription} (Job ${jobId})</div>' + | '${escapedDesc} (Job ${jobId})</div>' |} """.stripMargin jobEventJsonAsStr @@ -179,13 +183,15 @@ private[ui] class AllJobsPage(parent: JobsTab) extends WebUIPage("") { <span class="expand-application-timeline"> <span class="expand-application-timeline-arrow arrow-closed"></span> - <strong>Event timeline</strong> + <a data-toggle="tooltip" title={ToolTips.JOB_TIMELINE} data-placement="right"> + Event Timeline + </a> </span> ++ <div id="application-timeline" class="collapsed"> <div class="control-panel"> <div id="application-timeline-zoom-lock"> - <input type="checkbox" checked="checked"></input> - <span>Zoom Lock</span> + <input type="checkbox"></input> + <span>Enable zooming</span> </div> </div> </div> ++ @@ -283,7 +289,7 @@ private[ui] class AllJobsPage(parent: JobsTab) extends WebUIPage("") { {if (parent.sc.isDefined) { // Total duration is not meaningful unless the UI is live <li> - <strong>Total Duration: </strong> + <strong>Total Uptime: </strong> {UIUtils.formatDuration(System.currentTimeMillis() - startTime)} </li> }} @@ -336,9 +342,8 @@ private[ui] class AllJobsPage(parent: JobsTab) extends WebUIPage("") { failedJobsTable } - val helpText = """A job is triggered by an action, like "count()" or "saveAsTextFile()".""" + - " Click on a job's title to see information about the stages of tasks associated with" + - " the job." + val helpText = """A job is triggered by an action, like count() or saveAsTextFile().""" + + " Click on a job to see information about the stages of tasks inside it." UIUtils.headerSparkPage("Spark Jobs", content, parent, helpText = Some(helpText)) } diff --git a/core/src/main/scala/org/apache/spark/ui/jobs/AllStagesPage.scala b/core/src/main/scala/org/apache/spark/ui/jobs/AllStagesPage.scala index a37f739ab9..5e52942b64 100644 --- a/core/src/main/scala/org/apache/spark/ui/jobs/AllStagesPage.scala +++ b/core/src/main/scala/org/apache/spark/ui/jobs/AllStagesPage.scala @@ -73,19 +73,6 @@ private[ui] class AllStagesPage(parent: StagesTab) extends WebUIPage("") { val summary: NodeSeq = <div> <ul class="unstyled"> - { - if (sc.isDefined) { - // Total duration is not meaningful unless the UI is live - <li> - <strong>Total Duration: </strong> - {UIUtils.formatDuration(now - sc.get.startTime)} - </li> - } - } - <li> - <strong>Scheduling Mode: </strong> - {listener.schedulingMode.map(_.toString).getOrElse("Unknown")} - </li> { if (shouldShowActiveStages) { <li> @@ -145,7 +132,7 @@ private[ui] class AllStagesPage(parent: StagesTab) extends WebUIPage("") { content ++= <h4 id ="failed">Failed Stages ({numFailedStages})</h4> ++ failedStagesTable.toNodeSeq } - UIUtils.headerSparkPage("Spark Stages (for all jobs)", content, parent) + UIUtils.headerSparkPage("Stages for All Jobs", content, parent) } } } diff --git a/core/src/main/scala/org/apache/spark/ui/jobs/JobPage.scala b/core/src/main/scala/org/apache/spark/ui/jobs/JobPage.scala index 7163217e1f..2cad0a7969 100644 --- a/core/src/main/scala/org/apache/spark/ui/jobs/JobPage.scala +++ b/core/src/main/scala/org/apache/spark/ui/jobs/JobPage.scala @@ -20,13 +20,13 @@ package org.apache.spark.ui.jobs import java.util.Date import scala.collection.mutable.{Buffer, HashMap, ListBuffer} -import scala.xml.{NodeSeq, Node, Unparsed} +import scala.xml.{NodeSeq, Node, Unparsed, Utility} import javax.servlet.http.HttpServletRequest import org.apache.spark.JobExecutionStatus import org.apache.spark.scheduler.StageInfo -import org.apache.spark.ui.{UIUtils, WebUIPage} +import org.apache.spark.ui.{ToolTips, UIUtils, WebUIPage} import org.apache.spark.ui.jobs.UIData.ExecutorUIData /** Page showing statistics and stage list for a given job */ @@ -64,6 +64,9 @@ private[ui] class JobPage(parent: JobsTab) extends WebUIPage("job") { val submissionTime = stage.submissionTime.get val completionTime = stage.completionTime.getOrElse(System.currentTimeMillis()) + // The timeline library treats contents as HTML, so we have to escape them; for the + // data-title attribute string we have to escape them twice since that's in a string. + val escapedName = Utility.escape(name) s""" |{ | 'className': 'stage job-timeline-object ${status}', @@ -72,17 +75,17 @@ private[ui] class JobPage(parent: JobsTab) extends WebUIPage("job") { | 'end': new Date(${completionTime}), | 'content': '<div class="job-timeline-content" data-toggle="tooltip"' + | 'data-placement="top" data-html="true"' + - | 'data-title="${name} (Stage ${stageId}.${attemptId})<br>' + + | 'data-title="${Utility.escape(escapedName)} (Stage ${stageId}.${attemptId})<br>' + | 'Status: ${status.toUpperCase}<br>' + - | 'Submission Time: ${UIUtils.formatDate(new Date(submissionTime))}' + + | 'Submitted: ${UIUtils.formatDate(new Date(submissionTime))}' + | '${ if (status != "running") { - s"""<br>Completion Time: ${UIUtils.formatDate(new Date(completionTime))}""" + s"""<br>Completed: ${UIUtils.formatDate(new Date(completionTime))}""" } else { "" } }">' + - | '${name} (Stage ${stageId}.${attemptId})</div>', + | '${escapedName} (Stage ${stageId}.${attemptId})</div>', |} """.stripMargin } @@ -161,13 +164,15 @@ private[ui] class JobPage(parent: JobsTab) extends WebUIPage("job") { <span class="expand-job-timeline"> <span class="expand-job-timeline-arrow arrow-closed"></span> - <strong>Event timeline</strong> + <a data-toggle="tooltip" title={ToolTips.STAGE_TIMELINE} data-placement="right"> + Event Timeline + </a> </span> ++ <div id="job-timeline" class="collapsed"> <div class="control-panel"> <div id="job-timeline-zoom-lock"> - <input type="checkbox" checked="checked"></input> - <span>Zoom Lock</span> + <input type="checkbox"></input> + <span>Enable zooming</span> </div> </div> </div> ++ diff --git a/core/src/main/scala/org/apache/spark/ui/jobs/StagePage.scala b/core/src/main/scala/org/apache/spark/ui/jobs/StagePage.scala index b01fad8e45..8f7b1c2f09 100644 --- a/core/src/main/scala/org/apache/spark/ui/jobs/StagePage.scala +++ b/core/src/main/scala/org/apache/spark/ui/jobs/StagePage.scala @@ -81,7 +81,7 @@ private[ui] class StagePage(parent: StagesTab) extends WebUIPage("stage") { <div> <ul class="unstyled"> <li> - <strong>Total task time across all tasks: </strong> + <strong>Total Time Across All Tasks: </strong> {UIUtils.formatDuration(stageData.executorRunTime)} </li> {if (stageData.hasInput) { @@ -98,25 +98,25 @@ private[ui] class StagePage(parent: StagesTab) extends WebUIPage("stage") { }} {if (stageData.hasShuffleRead) { <li> - <strong>Shuffle read: </strong> + <strong>Shuffle Read: </strong> {s"${Utils.bytesToString(stageData.shuffleReadTotalBytes)} / " + s"${stageData.shuffleReadRecords}"} </li> }} {if (stageData.hasShuffleWrite) { <li> - <strong>Shuffle write: </strong> + <strong>Shuffle Write: </strong> {s"${Utils.bytesToString(stageData.shuffleWriteBytes)} / " + s"${stageData.shuffleWriteRecords}"} </li> }} {if (stageData.hasBytesSpilled) { <li> - <strong>Shuffle spill (memory): </strong> + <strong>Shuffle Spill (Memory): </strong> {Utils.bytesToString(stageData.memoryBytesSpilled)} </li> <li> - <strong>Shuffle spill (disk): </strong> + <strong>Shuffle Spill (Disk): </strong> {Utils.bytesToString(stageData.diskBytesSpilled)} </li> }} @@ -127,10 +127,10 @@ private[ui] class StagePage(parent: StagesTab) extends WebUIPage("stage") { <div> <span class="expand-additional-metrics"> <span class="expand-additional-metrics-arrow arrow-closed"></span> - <strong>Show additional metrics</strong> + <a>Show Additional Metrics</a> </span> <div class="additional-metrics collapsed"> - <ul style="list-style-type:none"> + <ul> <li> <input type="checkbox" id="select-all-metrics"/> <span class="additional-metric-title"><em>(De)select All</em></span> @@ -457,9 +457,9 @@ private[ui] class StagePage(parent: StagesTab) extends WebUIPage("stage") { val content = summary ++ - showAdditionalMetrics ++ dagViz ++ maybeExpandDagViz ++ + showAdditionalMetrics ++ <h4>Summary Metrics for {numCompleted} Completed Tasks</h4> ++ <div>{summaryTable.getOrElse("No tasks have reported metrics yet.")}</div> ++ <h4>Aggregated Metrics by Executor</h4> ++ executorTable.toNodeSeq ++ diff --git a/core/src/main/scala/org/apache/spark/ui/scope/RDDOperationGraph.scala b/core/src/main/scala/org/apache/spark/ui/scope/RDDOperationGraph.scala index 2b2db9e62b..c7045c98c8 100644 --- a/core/src/main/scala/org/apache/spark/ui/scope/RDDOperationGraph.scala +++ b/core/src/main/scala/org/apache/spark/ui/scope/RDDOperationGraph.scala @@ -182,7 +182,7 @@ private[ui] object RDDOperationGraph extends Logging { if (forJob) { s"""${node.id} [label="$label" shape="circle" padding="5" labelStyle="font-size: 0"]""" } else { - s"""${node.id} [label="$label" padding="5" labelStyle="font-size: 10"]""" + s"""${node.id} [label="$label" padding="5" labelStyle="font-size: 12px"]""" } } -- GitLab