Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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,53 @@
/*
* 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.
*/

/* Register functions to show/hide columns based on checkboxes. These need
* to be registered after the page loads. */
$(function() {
$("span.expand-additional-metrics").click(function(){
// Expand the list of additional metrics.
var additionalMetricsDiv = $(this).parent().find('.additional-metrics');
$(additionalMetricsDiv).toggleClass('collapsed');

// Switch the class of the arrow from open to closed.
$(this).find('.expand-additional-metrics-arrow').toggleClass('arrow-open');
$(this).find('.expand-additional-metrics-arrow').toggleClass('arrow-closed');

// If clicking caused the metrics to expand, automatically check all options for additional
// metrics (don't trigger a click when collapsing metrics, because it leads to weird
// toggling behavior).
if (!$(additionalMetricsDiv).hasClass('collapsed')) {
$(this).parent().find('input:checkbox:not(:checked)').trigger('click');
}
});

$("input:checkbox:not(:checked)").each(function() {
var column = "table ." + $(this).attr("name");
$(column).hide();
});

$("input:checkbox").click(function() {
var column = "table ." + $(this).attr("name");
$(column).toggle();
stripeTables();
});

// Trigger a click on the checkbox if a user clicks the label next to it.
$("span.additional-metric-title").click(function() {
$(this).parent().find('input:checkbox').trigger('click');
});
});
35 changes: 35 additions & 0 deletions core/src/main/resources/org/apache/spark/ui/static/table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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.
*/

/* Adds background colors to stripe table rows. This is necessary (instead of using css or the
* table striping provided by bootstrap) to appropriately stripe tables with hidden rows. */
function stripeTables() {
$("table.table-striped-custom").each(function() {
$(this).find("tr:not(:hidden)").each(function (index) {
if (index % 2 == 1) {
$(this).css("background-color", "#f9f9f9");
} else {
$(this).css("background-color", "#ffffff");
}
});
});
}

/* Stripe all tables after pages finish loading. */
$(function() {
stripeTables();
});
30 changes: 30 additions & 0 deletions core/src/main/resources/org/apache/spark/ui/static/webui.css
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,37 @@ pre {
border: none;
}

span.expand-additional-metrics {
cursor: pointer;
}

span.additional-metric-title {
cursor: pointer;
}

.additional-metrics.collapsed {
display: none;
}

.tooltip {
font-weight: normal;
}

.arrow-open {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid black;
float: left;
margin-top: 6px;
}

.arrow-closed {
width: 0;
height: 0;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 5px solid black;
display: inline-block;
}
12 changes: 12 additions & 0 deletions core/src/main/scala/org/apache/spark/ui/ToolTips.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,16 @@ private[spark] object ToolTips {
val SHUFFLE_READ =
"""Bytes read from remote executors. Typically less than shuffle write bytes
because this does not include shuffle data read locally."""

val GETTING_RESULT_TIME =
"""Time that the driver spends fetching task results from workers. If this is large, consider
decreasing the amount of data returned from each task."""

val RESULT_SERIALIZATION_TIME =
"""Time spent serializing the task result on the executor before sending it back to the
driver."""

val GC_TIME =
"""Time that the executor spent paused for Java garbage collection while the task was
running."""
}
44 changes: 28 additions & 16 deletions core/src/main/scala/org/apache/spark/ui/UIUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ package org.apache.spark.ui
import java.text.SimpleDateFormat
import java.util.{Locale, Date}

import scala.xml.{Text, Node}
import scala.xml.{Node, Text}

import org.apache.spark.Logging

/** Utility functions for generating XML pages with spark content. */
private[spark] object UIUtils extends Logging {
val TABLE_CLASS = "table table-bordered table-striped table-condensed sortable"
val TABLE_CLASS = "table table-bordered table-striped-custom table-condensed sortable"

// SimpleDateFormat is not thread-safe. Don't expose it to avoid improper use.
private val dateFormat = new ThreadLocal[SimpleDateFormat]() {
Expand Down Expand Up @@ -160,6 +160,8 @@ private[spark] object UIUtils extends Logging {
<script src={prependBaseUri("/static/jquery-1.11.1.min.js")}></script>
<script src={prependBaseUri("/static/bootstrap-tooltip.js")}></script>
<script src={prependBaseUri("/static/initialize-tooltips.js")}></script>
<script src={prependBaseUri("/static/table.js")}></script>
<script src={prependBaseUri("/static/additional-metrics.js")}></script>
}

/** Returns a spark page with correctly formatted headers */
Expand Down Expand Up @@ -240,28 +242,38 @@ private[spark] object UIUtils extends Logging {
generateDataRow: T => Seq[Node],
data: Iterable[T],
fixedWidth: Boolean = false,
id: Option[String] = None): Seq[Node] = {
id: Option[String] = None,
headerClasses: Seq[String] = Seq.empty): Seq[Node] = {

var listingTableClass = TABLE_CLASS
if (fixedWidth) {
listingTableClass += " table-fixed"
}
val colWidth = 100.toDouble / headers.size
val colWidthAttr = if (fixedWidth) colWidth + "%" else ""
val headerRow: Seq[Node] = {
// if none of the headers have "\n" in them
if (headers.forall(!_.contains("\n"))) {
// represent header as simple text
headers.map(h => <th width={colWidthAttr}>{h}</th>)

def getClass(index: Int): String = {
if (index < headerClasses.size) {
headerClasses(index)
} else {
// represent header text as list while respecting "\n"
headers.map { case h =>
<th width={colWidthAttr}>
<ul class ="unstyled">
{ h.split("\n").map { case t => <li> {t} </li> } }
</ul>
</th>
}
""
}
}

val newlinesInHeader = headers.exists(_.contains("\n"))
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you just inline this?

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 could but then it becomes a n^2 computation on the headers, because it has to scan through all of the headers each time it adds new header content. n is not huge so maybe a non-issue; would you still prefer inlining it?

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I see, don't worry about it then. I thought it's just a coding style thing.

def getHeaderContent(header: String): Seq[Node] = {
if (newlinesInHeader) {
<ul class="unstyled">
{ header.split("\n").map { case t => <li> {t} </li> } }
</ul>
} else {
Text(header)
}
}

val headerRow: Seq[Node] = {
headers.view.zipWithIndex.map { x =>
<th width={colWidthAttr} class={getClass(x._2)}>{getHeaderContent(x._1)}</th>
}
}
<table class={listingTableClass} id={id.map(Text.apply)}>
Expand Down
Loading