Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9b33018
init structured streaming ui
uncleGen Oct 22, 2019
1e349ac
fix code style
uncleGen Oct 22, 2019
5633431
resolve mima check
uncleGen Oct 22, 2019
cb2b44d
fix time zone and ut failure
uncleGen Oct 22, 2019
3dca76d
add stream query summary
uncleGen Oct 23, 2019
b8fc23c
code refactor to reduce duplicate code references
uncleGen Oct 23, 2019
d2095b0
fix comment: add query information on top
uncleGen Oct 23, 2019
389a1c8
add uts
uncleGen Oct 23, 2019
a2aaa46
Merge branch 'master' of github.com:apache/spark into SPARK-29543
uncleGen Oct 23, 2019
7d6c8bf
move streaming-page css/js to core package
uncleGen Oct 24, 2019
1a994b3
minor update
uncleGen Oct 24, 2019
ecee267
save
uncleGen Oct 25, 2019
60d8d9e
fix comments
uncleGen Oct 28, 2019
db1c2b2
minor update
uncleGen Oct 28, 2019
a034548
fix ut failure
uncleGen Oct 28, 2019
2273f82
save
uncleGen Nov 18, 2019
5104a42
save
uncleGen Nov 20, 2019
32ecfc3
save
uncleGen Nov 21, 2019
8c9a851
save
uncleGen Nov 21, 2019
0e5166f
fix comments
uncleGen Nov 21, 2019
cb3f338
Merge branch 'master' into SPARK-29543
uncleGen Nov 21, 2019
e30af98
fix ut failure
uncleGen Nov 22, 2019
6de18cc
Merge branch 'master' into SPARK-29543
uncleGen Nov 25, 2019
596d6bb
Merge branch 'master' into SPARK-29543
uncleGen Jan 3, 2020
c036cfb
fix comments
uncleGen Jan 3, 2020
c1457f0
fix wrong inactive query duration
uncleGen Jan 7, 2020
7c47223
Add StreamingQueryStatusListener
xuanyuanking Jan 7, 2020
81f121b
Merge branch 'SPARK-29543' of https://github.com/uncleGen/spark into …
xuanyuanking Jan 7, 2020
b5907cc
fix compile and test
xuanyuanking Jan 7, 2020
2bb9ce1
adjust ui format
uncleGen Jan 8, 2020
5accc93
comment address; more comments; retention logic added
xuanyuanking Jan 8, 2020
380aab5
add ut
xuanyuanking Jan 8, 2020
be4bec8
Merge pull request #20 from xuanyuanking/pr-26201
uncleGen Jan 9, 2020
a14aa38
Merge branch 'SPARK-29543' of github.com:uncleGen/spark into SPARK-29543
uncleGen Jan 9, 2020
f2926a7
quick fix
xuanyuanking Jan 9, 2020
44b0a37
Merge pull request #21 from xuanyuanking/pr-26201
uncleGen Jan 9, 2020
b2a334e
fix comments
uncleGen Jan 14, 2020
b3bf311
fix
uncleGen Jan 14, 2020
a0022a7
fix mima test failure
uncleGen Jan 14, 2020
1e35c0c
Merge branch 'master' of github.com:apache/spark into SPARK-29543
uncleGen Jan 14, 2020
dd5ca20
fix ut failure
uncleGen Jan 15, 2020
b8a4891
address comments
xuanyuanking Jan 22, 2020
b3697a2
private package
xuanyuanking Jan 23, 2020
cb1e68c
Merge pull request #22 from xuanyuanking/SPARK-29543
uncleGen Jan 26, 2020
2d5f66c
fix ut failure
uncleGen Jan 27, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// pre-define some colors for legends.
var colorPool = ["#F8C471", "#F39C12", "#B9770E", "#73C6B6", "#16A085", "#117A65", "#B2BABB", "#7F8C8D", "#616A6B"];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those colors have very similar hue so it's difficult to distinguish one from another one.
I think, previous choice of colors are much better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For color and display stuff maybe we could keep the current approach, and wait for more advice from end-users. All the changes could be done in follow-ups.


function drawAreaStack(id, labels, values, minX, maxX, minY, maxY) {
d3.select(d3.select(id).node().parentNode)
.style("padding", "8px 0 8px 8px")
.style("border-right", "0px solid white");

// Setup svg using Bostock's margin convention
var margin = {top: 20, right: 40, bottom: 30, left: maxMarginLeftForTimeline};
var width = 850 - margin.left - margin.right;
var height = 300 - margin.top - margin.bottom;

var svg = d3.select(id)
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var data = values;

var parse = d3.time.format("%H:%M:%S.%L").parse;

// Transpose the data into layers
var dataset = d3.layout.stack()(labels.map(function(fruit) {
return data.map(function(d) {
return {_x: d.x, x: parse(d.x), y: +d[fruit]};
});
}));


// Set x, y and colors
var x = d3.scale.ordinal()
.domain(dataset[0].map(function(d) { return d.x; }))
.rangeRoundBands([10, width-10], 0.02);

var y = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) { return d3.max(d, function(d) { return d.y0 + d.y; }); })])
.range([height, 0]);

var colors = colorPool.slice(0, labels.length)

// Define and draw axes
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(7)
.tickFormat( function(d) { return d } );

var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickFormat(d3.time.format("%H:%M:%S.%L"));

// Only show the first and last time in the graph
var xline = []
xline.push(x.domain()[0])
xline.push(x.domain()[x.domain().length - 1])
xAxis.tickValues(xline);

svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "translate(0," + unitLabelYOffset + ")")
.text("ms");

svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);

// Create groups for each series, rects for each segment
var groups = svg.selectAll("g.cost")
.data(dataset)
.enter().append("g")
.attr("class", "cost")
.style("fill", function(d, i) { return colors[i]; });

var rect = groups.selectAll("rect")
.data(function(d) { return d; })
.enter()
.append("rect")
.attr("x", function(d) { return x(d.x); })
.attr("y", function(d) { return y(d.y0 + d.y); })
.attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); })
.attr("width", x.rangeBand())
.on('mouseover', function(d) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's more user-friendly to show the labels without mouse over.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I met some front end format which was difficult to adjust. :(

var tip = '';
var idx = 0;
var _values = timeToValues[d._x]
_values.forEach(function (k) {
tip += labels[idx] + ': ' + k + ' ';
idx += 1;
});
tip += " at " + d._x
showBootstrapTooltip(d3.select(this).node(), tip);
})
.on('mouseout', function() {
hideBootstrapTooltip(d3.select(this).node());
})
.on("mousemove", function(d) {
var xPosition = d3.mouse(this)[0] - 15;
var yPosition = d3.mouse(this)[1] - 25;
tooltip.attr("transform", "translate(" + xPosition + "," + yPosition + ")");
tooltip.select("text").text(d.y);
});


// Draw legend
var legend = svg.selectAll(".legend")
.data(colors)
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(30," + i * 19 + ")"; });

legend.append("rect")
.attr("x", width - 20)
.attr("width", 18)
.attr("height", 18)
.style("fill", function(d, i) {return colors.slice().reverse()[i];})
.on('mouseover', function(d, i) {
var len = labels.length
showBootstrapTooltip(d3.select(this).node(), labels[len - 1 - i]);
})
.on('mouseout', function() {
hideBootstrapTooltip(d3.select(this).node());
})
.on("mousemove", function(d) {
var xPosition = d3.mouse(this)[0] - 15;
var yPosition = d3.mouse(this)[1] - 25;
tooltip.attr("transform", "translate(" + xPosition + "," + yPosition + ")");
tooltip.select("text").text(d.y);
});

// Prep the tooltip bits, initial display is hidden
var tooltip = svg.append("g")
.attr("class", "tooltip")
.style("display", "none");

tooltip.append("rect")
.attr("width", 30)
.attr("height", 20)
.attr("fill", "white")
.style("opacity", 0.5);

tooltip.append("text")
.attr("x", 15)
.attr("dy", "1.2em")
.style("text-anchor", "middle")
.attr("font-size", "12px")
.attr("font-weight", "bold");
}
2 changes: 2 additions & 0 deletions core/src/main/resources/org/apache/spark/ui/static/webui.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ $(function() {
collapseTablePageLoad('collapse-aggregated-sessionstat','aggregated-sessionstat');
collapseTablePageLoad('collapse-aggregated-sqlstat','aggregated-sqlstat');
collapseTablePageLoad('collapse-aggregated-sqlsessionstat','aggregated-sqlsessionstat');
collapseTablePageLoad('collapse-aggregated-activeQueries','aggregated-activeQueries');
collapseTablePageLoad('collapse-aggregated-completedQueries','aggregated-completedQueries');
});

$(function() {
Expand Down
169 changes: 169 additions & 0 deletions core/src/main/scala/org/apache/spark/ui/GraphUIData.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.spark.ui

import java.{util => ju}
import java.lang.{Long => JLong}

import scala.collection.JavaConverters._
import scala.collection.mutable.ArrayBuffer
import scala.xml.{Node, Unparsed}

/**
* A helper class to generate JavaScript and HTML for both timeline and histogram graphs.
*
* @param timelineDivId the timeline `id` used in the html `div` tag
* @param histogramDivId the timeline `id` used in the html `div` tag
* @param data the data for the graph
* @param minX the min value of X axis
* @param maxX the max value of X axis
* @param minY the min value of Y axis
* @param maxY the max value of Y axis
* @param unitY the unit of Y axis
* @param batchInterval if `batchInterval` is not None, we will draw a line for `batchInterval` in
* the graph
*/
private[spark] class GraphUIData(
Copy link
Contributor Author

@uncleGen uncleGen Oct 24, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: This class is moved from org.apache.spark.streaming.ui.StreamingPage with a new added function.

timelineDivId: String,
histogramDivId: String,
data: Seq[(Long, Double)],
minX: Long,
maxX: Long,
minY: Double,
maxY: Double,
unitY: String,
batchInterval: Option[Double] = None) {

private var dataJavaScriptName: String = _

def generateDataJs(jsCollector: JsCollector): Unit = {
val jsForData = data.map { case (x, y) =>
s"""{"x": $x, "y": $y}"""
}.mkString("[", ",", "]")
dataJavaScriptName = jsCollector.nextVariableName
jsCollector.addPreparedStatement(s"var $dataJavaScriptName = $jsForData;")
}

def generateTimelineHtml(jsCollector: JsCollector): Seq[Node] = {
jsCollector.addPreparedStatement(s"registerTimeline($minY, $maxY);")
if (batchInterval.isDefined) {
jsCollector.addStatement(
"drawTimeline(" +
Copy link
Member

@sarutak sarutak Nov 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't just reuse existing drawTimeline because onclick action is not implemented in this page.
If we click a data point in the graph, we'll get error.

スクリーンショット 2019-11-25 19 59 38

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not meet this error, could you please test again base on latest commit?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@uncleGen I hit the error as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created a PR #27883 for this.

s"'#$timelineDivId', $dataJavaScriptName, $minX, $maxX, $minY, $maxY, '$unitY'," +
s" ${batchInterval.get}" +
");")
} else {
jsCollector.addStatement(
s"drawTimeline('#$timelineDivId', $dataJavaScriptName, $minX, $maxX, $minY, $maxY," +
s" '$unitY');")
}
<div id={timelineDivId}></div>
}

def generateHistogramHtml(jsCollector: JsCollector): Seq[Node] = {
val histogramData = s"$dataJavaScriptName.map(function(d) { return d.y; })"
jsCollector.addPreparedStatement(s"registerHistogram($histogramData, $minY, $maxY);")
if (batchInterval.isDefined) {
jsCollector.addStatement(
"drawHistogram(" +
s"'#$histogramDivId', $histogramData, $minY, $maxY, '$unitY', ${batchInterval.get}" +
");")
} else {
jsCollector.addStatement(
s"drawHistogram('#$histogramDivId', $histogramData, $minY, $maxY, '$unitY');")
}
<div id={histogramDivId}></div>
}

def generateAreaStackHtmlWithData(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: newly added

jsCollector: JsCollector,
values: Array[(Long, ju.Map[String, JLong])]): Seq[Node] = {
val operationLabels = values.flatMap(_._2.keySet().asScala).toSet
val durationDataPadding = UIUtils.durationDataPadding(values)
val jsForData = durationDataPadding.map { case (x, y) =>
val s = y.toSeq.sortBy(_._1).map(e => s""""${e._1}": "${e._2}"""").mkString(",")
s"""{x: "${UIUtils.formatBatchTime(x, 1, showYYYYMMSS = false)}", $s}"""
}.mkString("[", ",", "]")
val jsForLabels = operationLabels.toSeq.sorted.mkString("[\"", "\",\"", "\"]")

val (maxX, minX, maxY, minY) = if (values != null && values.length > 0) {
val xValues = values.map(_._1.toLong)
val yValues = values.map(_._2.asScala.toSeq.map(_._2.toLong).sum)
(xValues.max, xValues.min, yValues.max, yValues.min)
} else {
(0L, 0L, 0L, 0L)
}

dataJavaScriptName = jsCollector.nextVariableName
jsCollector.addPreparedStatement(s"var $dataJavaScriptName = $jsForData;")
val labels = jsCollector.nextVariableName
jsCollector.addPreparedStatement(s"var $labels = $jsForLabels;")
jsCollector.addStatement(
s"drawAreaStack('#$timelineDivId', $labels, $dataJavaScriptName, $minX, $maxX, $minY, $maxY)")
<div id={timelineDivId}></div>
}
}

/**
* A helper class that allows the user to add JavaScript statements which will be executed when the
* DOM has finished loading.
*/
private[spark] class JsCollector {

private var variableId = 0

/**
* Return the next unused JavaScript variable name
*/
def nextVariableName: String = {
variableId += 1
"v" + variableId
}

/**
* JavaScript statements that will execute before `statements`
*/
private val preparedStatements = ArrayBuffer[String]()

/**
* JavaScript statements that will execute after `preparedStatements`
*/
private val statements = ArrayBuffer[String]()

def addPreparedStatement(js: String): Unit = {
preparedStatements += js
}

def addStatement(js: String): Unit = {
statements += js
}

/**
* Generate a html snippet that will execute all scripts when the DOM has finished loading.
*/
def toHtml: Seq[Node] = {
val js =
s"""
|$$(document).ready(function() {
| ${preparedStatements.mkString("\n")}
| ${statements.mkString("\n")}
|});""".stripMargin

<script>{Unparsed(js)}</script>
}
}
Loading