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