You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by do...@apache.org on 2026/04/10 17:22:57 UTC
(spark) branch master updated: [SPARK-56331][UI] Truncate long node labels in SQL plan visualization
This is an automated email from the ASF dual-hosted git repository.
dongjoon 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 1967f7205c83 [SPARK-56331][UI] Truncate long node labels in SQL plan visualization
1967f7205c83 is described below
commit 1967f7205c8371c62ca2ec6ccc40289b50a45bc3
Author: Kent Yao <ke...@microsoft.com>
AuthorDate: Fri Apr 10 10:22:44 2026 -0700
[SPARK-56331][UI] Truncate long node labels in SQL plan visualization
### What changes were proposed in this pull request?
Truncate DOT graph node labels to 36 characters using middle-ellipsis when node names are long (e.g., scan nodes with fully-qualified `catalog.schema.table` paths from custom catalogs).
The full name remains available in:
- **Tooltip** (hover over the node)
- **Side panel** (click the node)
Example:
```
Before: ScanTransformer parquet spark_catalog.longschemaname.catalog_sales (60+ chars)
After: ScanTransformer pa…catalog_sales (36 chars)
```
### Why are the changes needed?
When custom catalogs generate long schema/table identifiers, the plan visualization graph becomes excessively wide and hard to read. Most operator labels are under 30 characters; only scan nodes with fully-qualified paths exceed 36 characters.
### Does this PR introduce _any_ user-facing change?
Yes. Long node labels in the SQL plan visualization are now truncated with a middle ellipsis. The full name is still accessible via tooltip and side panel.
### How was this patch tested?
- Updated existing test for truncated label in DOT output
- Added 4 new tests:
- Short labels unchanged (Sort, Filter, HashAggregate, null, empty)
- Long labels truncated with middle ellipsis (boundary, realistic scan names)
- DOT label truncated while tooltip keeps full name
- JSON side panel details keep full untruncated name
All 6 tests pass in `SparkPlanGraphSuite`.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: GitHub Copilot (Claude Opus 4.6)
Closes #55156 from yaooqinn/SPARK-56331.
Authored-by: Kent Yao <ke...@microsoft.com>
Signed-off-by: Dongjoon Hyun <do...@apache.org>
---
.../spark/sql/execution/ui/SparkPlanGraph.scala | 28 ++++++++++-
.../sql/execution/ui/SparkPlanGraphSuite.scala | 58 ++++++++++++++++++++++
2 files changed, 85 insertions(+), 1 deletion(-)
diff --git a/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/SparkPlanGraph.scala b/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/SparkPlanGraph.scala
index cd5c5bd5ad0f..16537641471f 100644
--- a/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/SparkPlanGraph.scala
+++ b/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/SparkPlanGraph.scala
@@ -88,6 +88,31 @@ case class SparkPlanGraph(
object SparkPlanGraph {
+ /** Maximum character length for DOT graph node labels. */
+ private val MAX_LABEL_LENGTH = 36
+
+ /** Pattern to detect dot-separated qualified names (e.g., catalog.schema.table). */
+ private val QUALIFIED_NAME_PATTERN =
+ java.util.regex.Pattern.compile(".*\\w+\\.\\w+\\.\\w+.*")
+
+ /**
+ * Truncate a node label for compact display in the plan visualization graph.
+ * Only truncates labels containing dot-separated qualified names (e.g.,
+ * catalog.schema.table) which can become very long with custom catalogs.
+ * Uses middle-ellipsis to preserve the operator prefix and the table name.
+ * The full name is always available in the tooltip and side panel.
+ */
+ private[ui] def truncateLabel(label: String): String = {
+ if (label == null || label.length <= MAX_LABEL_LENGTH ||
+ !QUALIFIED_NAME_PATTERN.matcher(label).matches()) {
+ label
+ } else {
+ val half = (MAX_LABEL_LENGTH - 3) / 2
+ label.substring(0, half) + "..." +
+ label.substring(label.length - half)
+ }
+ }
+
/**
* Build a SparkPlanGraph from the root of a SparkPlan tree.
*/
@@ -199,7 +224,8 @@ class SparkPlanGraphNode(
def makeDotNode(metricsValue: Map[Long, String]): String = {
val nodeId = s"node$id"
val tooltip = StringEscapeUtils.escapeJava(desc)
- val labelStr = StringEscapeUtils.escapeJava(name)
+ val labelStr = StringEscapeUtils.escapeJava(
+ SparkPlanGraph.truncateLabel(name))
s""" $id [id="$nodeId" label="$labelStr" tooltip="$tooltip"];"""
}
}
diff --git a/sql/core/src/test/scala/org/apache/spark/sql/execution/ui/SparkPlanGraphSuite.scala b/sql/core/src/test/scala/org/apache/spark/sql/execution/ui/SparkPlanGraphSuite.scala
index 4188f2b0896e..ec979dac8e32 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/execution/ui/SparkPlanGraphSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/execution/ui/SparkPlanGraphSuite.scala
@@ -66,4 +66,62 @@ class SparkPlanGraphSuite extends SparkFunSuite {
assert(json.contains("\"desc\":\"WholeStageCodegen (1)\""),
"cluster should include desc field")
}
+
+ test("SPARK-56331: truncateLabel returns short labels unchanged") {
+ assert(SparkPlanGraph.truncateLabel("Sort") === "Sort")
+ assert(SparkPlanGraph.truncateLabel("Filter") === "Filter")
+ assert(SparkPlanGraph.truncateLabel("HashAggregate") === "HashAggregate")
+ assert(SparkPlanGraph.truncateLabel("SortMergeJoin Inner") === "SortMergeJoin Inner")
+ assert(SparkPlanGraph.truncateLabel(null) === null)
+ assert(SparkPlanGraph.truncateLabel("") === "")
+ }
+
+ test("SPARK-56331: truncateLabel does not truncate long operator names") {
+ // Long operator names without qualified paths should NOT be truncated
+ val insertCmd = "Execute InsertIntoHadoopFsRelationCommand"
+ assert(SparkPlanGraph.truncateLabel(insertCmd) === insertCmd)
+ val longOp = "BroadcastHashJoin Inner BuildRight with very long hint"
+ assert(SparkPlanGraph.truncateLabel(longOp) === longOp)
+ }
+
+ test("SPARK-56331: truncateLabel truncates qualified catalog paths") {
+ val exactly36 = "Scan spark_catalog.default.store_sal"
+ assert(SparkPlanGraph.truncateLabel(exactly36) === exactly36)
+
+ val longScan =
+ "Scan parquet spark_catalog.very_long_schema_name.catalog_sales"
+ val truncated = SparkPlanGraph.truncateLabel(longScan)
+ assert(truncated.length <= 36,
+ s"Truncated label should be <= 36 chars but was ${truncated.length}")
+ assert(truncated.startsWith("Scan parquet spa"),
+ "Should preserve operator prefix")
+ assert(truncated.endsWith("catalog_sales"),
+ "Should preserve table name at the end")
+ assert(truncated.contains("..."),
+ "Should contain ellipsis in the middle")
+ }
+
+ test("SPARK-56331: DOT label truncated, tooltip keeps full name") {
+ val longName = "ScanTransformer parquet " +
+ "spark_catalog.abcdefghijklmnopqrstuvwxyz.store_sales"
+ val node = new SparkPlanGraphNode(
+ id = 1, name = longName, desc = longName,
+ metrics = Seq.empty)
+ val dot = node.makeDotNode(Map.empty)
+ assert(dot.contains("..."), "DOT label should contain ellipsis")
+ assert(dot.contains("tooltip=\"" + longName + "\""),
+ "Tooltip should contain the full name")
+ }
+
+ test("SPARK-56331: JSON details keep full name") {
+ val longName = "ScanTransformer parquet " +
+ "spark_catalog.abcdefghijklmnopqrstuvwxyz.store_sales"
+ val node = new SparkPlanGraphNode(
+ id = 1, name = longName, desc = longName,
+ metrics = Seq.empty)
+ val graph = SparkPlanGraph(Seq(node), Seq.empty)
+ val json = graph.makeNodeDetailsJson(Map.empty)
+ assert(json.contains(longName),
+ "JSON details should contain the full untruncated name")
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org