This is an automated email from the ASF dual-hosted git repository.
yao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new aa968ee5a088 [SPARK-55779][UI] Add tooltip helper utilities for Spark
Web UI
aa968ee5a088 is described below
commit aa968ee5a0884a17828c5dd99163e3348d979e0f
Author: Kent Yao <[email protected]>
AuthorDate: Wed Mar 4 08:48:07 2026 +0800
[SPARK-55779][UI] Add tooltip helper utilities for Spark Web UI
### What changes were proposed in this pull request?
This PR introduces helper utilities for tooltip markup generation in the
Spark Web UI:
**Scala helpers in `UIUtils.scala`:**
- `tooltipSpan(content, text, placement)` — wraps content in a `<span>`
with BS5 tooltip attributes
- `tooltipLink(content, text, placement)` — wraps content in an `<a>` with
BS5 tooltip attributes
**JS helper in `stagepage.js`:**
- `setTooltip(selector, text)` — sets BS5 tooltip attributes on a jQuery
element
These helpers replace 21 inline tooltip patterns across 10 Scala files and
12 `.attr()` call chains in stagepage.js, reducing boilerplate and ensuring
consistent tooltip markup.
### Why are the changes needed?
Part of the Bootstrap 5 migration
([SPARK-55760](https://issues.apache.org/jira/browse/SPARK-55760)). Tooltip
markup was duplicated across many files with slight variations. Centralizing it:
1. Reduces code duplication
2. Ensures consistent BS5 tooltip attributes (`data-bs-toggle`,
`data-bs-placement`, `title`)
3. Makes future tooltip-related changes easier (single point of change)
### Does this PR introduce _any_ user-facing change?
No. The rendered HTML is functionally identical — tooltips now consistently
include `data-bs-placement="top"`.
### How was this patch tested?
- Updated `UIUtilsSuite` to match new tooltip markup
- All existing tests pass
- Scalastyle and JS lint checks pass
### Was this patch authored or co-authored using generative AI tooling?
Yes, GitHub Copilot CLI was used.
Closes #54588 from yaooqinn/SPARK-55779.
Authored-by: Kent Yao <[email protected]>
Signed-off-by: Kent Yao <[email protected]>
---
.../org/apache/spark/ui/static/stagepage.js | 64 ++++++++--------------
.../spark/deploy/master/ui/ApplicationPage.scala | 8 +--
.../scala/org/apache/spark/ui/PagedTable.scala | 17 +++---
.../main/scala/org/apache/spark/ui/UIUtils.scala | 26 ++++++---
.../spark/ui/exec/ExecutorThreadDumpPage.scala | 6 +-
.../org/apache/spark/ui/jobs/AllJobsPage.scala | 4 +-
.../scala/org/apache/spark/ui/jobs/JobPage.scala | 4 +-
.../scala/org/apache/spark/ui/jobs/PoolTable.scala | 9 ++-
.../apache/spark/ui/jobs/TaskThreadDumpPage.scala | 6 +-
.../scala/org/apache/spark/ui/UIUtilsSuite.scala | 2 +-
.../apache/spark/ui/storage/StoragePageSuite.scala | 2 +-
.../apache/spark/streaming/ui/StreamingPage.scala | 5 +-
12 files changed, 66 insertions(+), 87 deletions(-)
diff --git a/core/src/main/resources/org/apache/spark/ui/static/stagepage.js
b/core/src/main/resources/org/apache/spark/ui/static/stagepage.js
index ac5397419356..1f2142f4f808 100644
--- a/core/src/main/resources/org/apache/spark/ui/static/stagepage.js
+++ b/core/src/main/resources/org/apache/spark/ui/static/stagepage.js
@@ -26,6 +26,12 @@ import {
export {setTaskThreadDumpEnabled};
+function setTooltip(selector, text) {
+ $(selector).attr("data-bs-toggle", "tooltip")
+ .attr("data-bs-placement", "top")
+ .attr("title", text);
+}
+
var shouldBlockUI = true;
var taskThreadDumpEnabled = false;
@@ -356,35 +362,19 @@ $(document).ready(function () {
"<div id='executor_direct_mapped_pool_memory'
class='executor-jvm-metrics-checkbox-div'><input type='checkbox'
class='toggle-vis' id='executor-box-18' data-column='18'
data-metrics-type='executor'> Peak Pool Memory Direct / Mapped</div>" +
"</div>");
- $('#scheduler_delay').attr("data-bs-toggle", "tooltip")
- .attr("data-bs-placement", "top")
- .attr("title", "Scheduler delay includes time to ship the task from the
scheduler to the executor, and time to send " +
- "the task result from the executor to the scheduler. If scheduler delay
is large, consider decreasing the size of tasks or decreasing the size of task
results.");
- $('#task_deserialization_time').attr("data-bs-toggle", "tooltip")
- .attr("data-bs-placement", "top")
- .attr("title", "Time spent deserializing the task closure on the executor,
including the time to read the broadcasted task.");
- $('#shuffle_read_fetch_wait_time').attr("data-bs-toggle", "tooltip")
- .attr("data-bs-placement", "top")
- .attr("title", "Time that the task spent blocked waiting for shuffle data
to be read from remote machines.");
- $('#shuffle_remote_reads').attr("data-bs-toggle", "tooltip")
- .attr("data-bs-placement", "top")
- .attr("title", "Total shuffle bytes read from remote executors. This is a
subset of the shuffle read bytes; the remaining shuffle data is read locally.
");
- $('#shuffle_write_time').attr("data-bs-toggle", "tooltip")
- .attr("data-bs-placement", "top")
- .attr("title", "Time that the task spent writing shuffle data.");
- $('#result_serialization_time').attr("data-bs-toggle", "tooltip")
- .attr("data-bs-placement", "top")
- .attr("title", "Time spent serializing the task result on the executor
before sending it back to the driver.");
- $('#getting_result_time').attr("data-bs-toggle", "tooltip")
- .attr("data-bs-placement", "top")
- .attr("title", "Time that the driver spends fetching task results from
workers. If this is large, consider decreasing the amount of data returned from
each task.");
- $('#peak_execution_memory').attr("data-bs-toggle", "tooltip")
- .attr("data-bs-placement", "top")
- .attr("title", "Execution memory refers to the memory used by internal
data structures created during " +
- "shuffles, aggregations and joins when Tungsten is enabled. The value of
this accumulator " +
- "should be approximately the sum of the peak sizes across all such data
structures created " +
- "in this task. For SQL jobs, this only tracks all unsafe operators,
broadcast joins, and " +
- "external sort.");
+ setTooltip('#scheduler_delay', "Scheduler delay includes time to ship the
task from the scheduler to the executor, and time to send " +
+ "the task result from the executor to the scheduler. If scheduler delay is
large, consider decreasing the size of tasks or decreasing the size of task
results.");
+ setTooltip('#task_deserialization_time', "Time spent deserializing the task
closure on the executor, including the time to read the broadcasted task.");
+ setTooltip('#shuffle_read_fetch_wait_time', "Time that the task spent
blocked waiting for shuffle data to be read from remote machines.");
+ setTooltip('#shuffle_remote_reads', "Total shuffle bytes read from remote
executors. This is a subset of the shuffle read bytes; the remaining shuffle
data is read locally. ");
+ setTooltip('#shuffle_write_time', "Time that the task spent writing shuffle
data.");
+ setTooltip('#result_serialization_time', "Time spent serializing the task
result on the executor before sending it back to the driver.");
+ setTooltip('#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.");
+ setTooltip('#peak_execution_memory', "Execution memory refers to the memory
used by internal data structures created during " +
+ "shuffles, aggregations and joins when Tungsten is enabled. The value of
this accumulator " +
+ "should be approximately the sum of the peak sizes across all such data
structures created " +
+ "in this task. For SQL jobs, this only tracks all unsafe operators,
broadcast joins, and " +
+ "external sort.");
var tasksSummary = $("#parent-container");
getStandAloneAppId(function (appId) {
// rendering the UI page
@@ -624,27 +614,19 @@ $(document).ready(function () {
executorSummaryTableSelector.column(9).visible(dataToShow.showInputData);
if (dataToShow.showInputData) {
- $('#executor-summary-input').attr("data-bs-toggle", "tooltip")
- .attr("data-bs-placement", "top")
- .attr("title", "Bytes and records read from Hadoop or from
Spark storage.");
+ setTooltip('#executor-summary-input', "Bytes and records read
from Hadoop or from Spark storage.");
}
executorSummaryTableSelector.column(10).visible(dataToShow.showOutputData);
if (dataToShow.showOutputData) {
- $('#executor-summary-output').attr("data-bs-toggle", "tooltip")
- .attr("data-bs-placement", "top")
- .attr("title", "Bytes and records written to Hadoop.");
+ setTooltip('#executor-summary-output', "Bytes and records
written to Hadoop.");
}
executorSummaryTableSelector.column(11).visible(dataToShow.showShuffleReadData);
if (dataToShow.showShuffleReadData) {
- $('#executor-summary-shuffle-read').attr("data-bs-toggle",
"tooltip")
- .attr("data-bs-placement", "top")
- .attr("title", "Total shuffle bytes and records read (includes
both data read locally and data read from remote executors).");
+ setTooltip('#executor-summary-shuffle-read', "Total shuffle
bytes and records read (includes both data read locally and data read from
remote executors).");
}
executorSummaryTableSelector.column(12).visible(dataToShow.showShuffleWriteData);
if (dataToShow.showShuffleWriteData) {
- $('#executor-summary-shuffle-write').attr("data-bs-toggle",
"tooltip")
- .attr("data-bs-placement", "top")
- .attr("title", "Bytes and records written to disk in order to
be read by a shuffle in a future stage.");
+ setTooltip('#executor-summary-shuffle-write', "Bytes and records
written to disk in order to be read by a shuffle in a future stage.");
}
executorSummaryTableSelector.column(13).visible(dataToShow.showBytesSpilledData);
executorSummaryTableSelector.column(14).visible(dataToShow.showBytesSpilledData);
diff --git
a/core/src/main/scala/org/apache/spark/deploy/master/ui/ApplicationPage.scala
b/core/src/main/scala/org/apache/spark/deploy/master/ui/ApplicationPage.scala
index 886cff3d6b39..3de530f1d252 100644
---
a/core/src/main/scala/org/apache/spark/deploy/master/ui/ApplicationPage.scala
+++
b/core/src/main/scala/org/apache/spark/deploy/master/ui/ApplicationPage.scala
@@ -72,14 +72,12 @@ private[ui] class ApplicationPage(parent: MasterWebUI)
extends WebUIPage("app")
}
</li>
<li>
- <span data-bs-toggle="tooltip"
title={ToolTips.APPLICATION_EXECUTOR_LIMIT}
- data-bs-placement="top">
- <strong>Executor Limit: </strong>
+ {UIUtils.tooltipSpan(
+ <xml:group><strong>Executor Limit: </strong>
{
if (app.getExecutorLimit == Int.MaxValue) "Unlimited" else
app.getExecutorLimit
}
- ({app.executors.size} granted)
- </span>
+ ({app.executors.size} granted)</xml:group>,
ToolTips.APPLICATION_EXECUTOR_LIMIT)}
</li>
<li>
<strong>Executor Memory - Default Resource Profile:</strong>
diff --git a/core/src/main/scala/org/apache/spark/ui/PagedTable.scala
b/core/src/main/scala/org/apache/spark/ui/PagedTable.scala
index 5ee482ed4343..726c79edab86 100644
--- a/core/src/main/scala/org/apache/spark/ui/PagedTable.scala
+++ b/core/src/main/scala/org/apache/spark/ui/PagedTable.scala
@@ -364,9 +364,9 @@ private[spark] trait PagedTable[T] {
<th>
<a href={headerLink}>
- <span data-bs-toggle="tooltip" data-bs-placement="top"
title={tooltip.getOrElse("")}>
- {header} {Unparsed(arrow)}
- </span>
+ {UIUtils.tooltipSpan(
+ <xml:group>{header} {Unparsed(arrow)}</xml:group>,
+ tooltip.getOrElse(""))}
</a>
</th>
} else {
@@ -379,17 +379,14 @@ private[spark] trait PagedTable[T] {
<th>
<a href={headerLink}>
- <span data-bs-toggle="tooltip"
- data-bs-placement="top" title={tooltip.getOrElse("")}>
- {header}
- </span>
+ {UIUtils.tooltipSpan(<xml:group>{header}</xml:group>,
+ tooltip.getOrElse(""))}
</a>
</th>
} else {
<th>
- <span data-bs-toggle="tooltip" data-bs-placement="top"
title={tooltip.getOrElse("")}>
- {header}
- </span>
+ {UIUtils.tooltipSpan(<xml:group>{header}</xml:group>,
+ tooltip.getOrElse(""))}
</th>
}
}
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 74cd4791399e..229561bc5081 100644
--- a/core/src/main/scala/org/apache/spark/ui/UIUtils.scala
+++ b/core/src/main/scala/org/apache/spark/ui/UIUtils.scala
@@ -431,9 +431,7 @@ private[spark] object UIUtils extends Logging {
getTooltip(x._2) match {
case Some(tooltip) =>
<th width={colWidthAttr} class={getClass(x._2)}>
- <span data-bs-toggle="tooltip" title={tooltip}>
- {getHeaderContent(x._1)}
- </span>
+ {tooltipSpan(getHeaderContent(x._1), tooltip)}
</th>
case None => <th width={colWidthAttr}
class={getClass(x._2)}>{getHeaderContent(x._1)}</th>
}
@@ -517,10 +515,8 @@ private[spark] object UIUtils extends Logging {
<span id={if (forJob) "job-dag-viz" else "stage-dag-viz"}
class="expand-dag-viz" data-forjob={forJob.toString}>
<span class="expand-dag-viz-arrow arrow-closed"></span>
- <a data-bs-toggle="tooltip" title={if (forJob) ToolTips.JOB_DAG else
ToolTips.STAGE_DAG}
- data-bs-placement="top">
- DAG Visualization
- </a>
+ {tooltipLink(<xml:group>DAG Visualization</xml:group>,
+ if (forJob) ToolTips.JOB_DAG else ToolTips.STAGE_DAG)}
</span>
<div id="dag-viz-graph"></div>
<div id="dag-viz-metadata" style="display:none">
@@ -556,6 +552,22 @@ private[spark] object UIUtils extends Logging {
</sup>
}
+ /** Wrap content in a span with a Bootstrap 5 tooltip. */
+ def tooltipSpan(content: Seq[Node], text: String,
+ placement: String = "top"): Seq[Node] = {
+ <span data-bs-toggle="tooltip" data-bs-placement={placement} title={text}>
+ {content}
+ </span>
+ }
+
+ /** Create a link with a Bootstrap 5 tooltip. */
+ def tooltipLink(content: Seq[Node], text: String,
+ placement: String = "top"): Seq[Node] = {
+ <a data-bs-toggle="tooltip" data-bs-placement={placement} title={text}>
+ {content}
+ </a>
+ }
+
/**
* Returns HTML rendering of a job or stage description. It will try to
parse the string as HTML
* and make sure that it only contains anchors with root-relative links.
Otherwise,
diff --git
a/core/src/main/scala/org/apache/spark/ui/exec/ExecutorThreadDumpPage.scala
b/core/src/main/scala/org/apache/spark/ui/exec/ExecutorThreadDumpPage.scala
index b4a205e09fb8..fb971b2078ad 100644
--- a/core/src/main/scala/org/apache/spark/ui/exec/ExecutorThreadDumpPage.scala
+++ b/core/src/main/scala/org/apache/spark/ui/exec/ExecutorThreadDumpPage.scala
@@ -117,10 +117,8 @@ private[ui] class ExecutorThreadDumpPage(
<th data-action="collapseAllThreadStackTrace"
data-toggle-button="false">Thread Name</th>
<th data-action="collapseAllThreadStackTrace"
data-toggle-button="false">Thread State</th>
<th data-action="collapseAllThreadStackTrace"
data-toggle-button="false">
- <span data-bs-toggle="tooltip" data-bs-placement="top"
- title="Objects whose lock the thread currently holds">
- Thread Locks
- </span>
+ {UIUtils.tooltipSpan(<xml:group>Thread Locks</xml:group>,
+ "Objects whose lock the thread currently holds")}
</th>
</thead>
<tbody>{dumpRows}</tbody>
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 a270d4b71ac3..019bebcadeaa 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
@@ -199,9 +199,7 @@ private[ui] class AllJobsPage(parent: JobsTab, store:
AppStatusStore) extends We
<span class="expand-application-timeline">
<span class="expand-application-timeline-arrow arrow-closed"></span>
- <a data-bs-toggle="tooltip" title={ToolTips.JOB_TIMELINE}
data-bs-placement="top">
- Event Timeline
- </a>
+ {UIUtils.tooltipLink(<xml:group>Event Timeline</xml:group>,
ToolTips.JOB_TIMELINE)}
</span> ++
<div id="application-timeline" class="collapsed">
{
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 ef9ce225d08a..77869163c8d0 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
@@ -179,9 +179,7 @@ private[ui] class JobPage(parent: JobsTab, store:
AppStatusStore) extends WebUIP
<span class="expand-job-timeline">
<span class="expand-job-timeline-arrow arrow-closed"></span>
- <a data-bs-toggle="tooltip" title={ToolTips.STAGE_TIMELINE}
data-bs-placement="top">
- Event Timeline
- </a>
+ {UIUtils.tooltipLink(<xml:group>Event Timeline</xml:group>,
ToolTips.STAGE_TIMELINE)}
</span> ++
<div id="job-timeline" class="collapsed">
{
diff --git a/core/src/main/scala/org/apache/spark/ui/jobs/PoolTable.scala
b/core/src/main/scala/org/apache/spark/ui/jobs/PoolTable.scala
index cb4ed7190ad7..64dc961f77a8 100644
--- a/core/src/main/scala/org/apache/spark/ui/jobs/PoolTable.scala
+++ b/core/src/main/scala/org/apache/spark/ui/jobs/PoolTable.scala
@@ -37,13 +37,12 @@ private[ui] class PoolTable(pools: Map[Schedulable,
PoolData], parent: StagesTab
<tr>
<th>Pool Name</th>
<th>
- <span data-bs-toggle="tooltip" data-bs-placement="top"
- title="Pool's minimum share of CPU
- cores">Minimum Share</span>
+ {UIUtils.tooltipSpan(<xml:group>Minimum Share</xml:group>,
+ "Pool's minimum share of CPU cores")}
</th>
<th>
- <span data-bs-toggle="tooltip" data-bs-placement="top"
title="Pool's share of cluster
- resources relative to others">Pool Weight</span>
+ {UIUtils.tooltipSpan(<xml:group>Pool Weight</xml:group>,
+ "Pool's share of cluster resources relative to others")}
</th>
<th>Active Stages</th>
<th>Running Tasks</th>
diff --git
a/core/src/main/scala/org/apache/spark/ui/jobs/TaskThreadDumpPage.scala
b/core/src/main/scala/org/apache/spark/ui/jobs/TaskThreadDumpPage.scala
index ac595b810d62..60677216ed93 100644
--- a/core/src/main/scala/org/apache/spark/ui/jobs/TaskThreadDumpPage.scala
+++ b/core/src/main/scala/org/apache/spark/ui/jobs/TaskThreadDumpPage.scala
@@ -78,10 +78,8 @@ private[spark] class TaskThreadDumpPage(
<th>Thread Name</th>
<th>Thread State</th>
<th>
- <span data-bs-toggle="tooltip" data-bs-placement="top"
- title="Objects whose lock the thread currently holds">
- Thread Locks
- </span>
+ {UIUtils.tooltipSpan(<xml:group>Thread Locks</xml:group>,
+ "Objects whose lock the thread currently holds")}
</th>
</thead>
<tbody>
diff --git a/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala
b/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala
index ebb22de7c538..896e5b828017 100644
--- a/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala
+++ b/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala
@@ -165,7 +165,7 @@ class UIUtilsSuite extends SparkFunSuite {
<thead>
<th width="" class="">{header(0)}</th>
<th width="" class="">
- <span data-bs-toggle="tooltip" title="tooltip">
+ <span data-bs-toggle="tooltip" data-bs-placement="top"
title="tooltip">
{header(1)}
</span>
</th>
diff --git
a/core/src/test/scala/org/apache/spark/ui/storage/StoragePageSuite.scala
b/core/src/test/scala/org/apache/spark/ui/storage/StoragePageSuite.scala
index ada799c863ee..26ed10fc69f5 100644
--- a/core/src/test/scala/org/apache/spark/ui/storage/StoragePageSuite.scala
+++ b/core/src/test/scala/org/apache/spark/ui/storage/StoragePageSuite.scala
@@ -89,7 +89,7 @@ class StoragePageSuite extends SparkFunSuite {
}
}
}
- assert((xmlNodes \\ "th").map(_.text) === headerRow.map(_.text))
+ assert((xmlNodes \\ "th").map(_.text.trim) === headerRow.map(_.text.trim))
assert((xmlNodes \\ "tr").size === 3)
assert(((xmlNodes \\ "tr")(0) \\ "td").map(_.text.trim) ===
diff --git
a/streaming/src/main/scala/org/apache/spark/streaming/ui/StreamingPage.scala
b/streaming/src/main/scala/org/apache/spark/streaming/ui/StreamingPage.scala
index e2caf5256226..11ab7ccc4e61 100644
--- a/streaming/src/main/scala/org/apache/spark/streaming/ui/StreamingPage.scala
+++ b/streaming/src/main/scala/org/apache/spark/streaming/ui/StreamingPage.scala
@@ -281,9 +281,8 @@ private[ui] class StreamingPage(parent: StreamingTab)
if (hasStream) {
<span class="expand-input-rate">
<span class="expand-input-rate-arrow arrow-closed"></span>
- <a data-bs-toggle="tooltip" title="Show/hide details of
each receiver" data-bs-placement="top">
- <strong>Input Rate</strong>
- </a>
+ {SparkUIUtils.tooltipLink(<xml:group><strong>Input
Rate</strong></xml:group>,
+ "Show/hide details of each receiver")}
</span>
} else {
<strong>Input Rate</strong>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]