From 16498a97c7fc933b587a371bd74af81b9364cf77 Mon Sep 17 00:00:00 2001 From: Kent Yao Date: Thu, 27 Mar 2025 09:45:36 +0800 Subject: [PATCH 1/3] plan svg/dot/text downloading --- .../sql/execution/ui/static/spark-sql-viz.css | 18 ++++----- .../sql/execution/ui/static/spark-sql-viz.js | 37 +++++++++++++++++++ .../sql/execution/ui/ExecutionPage.scala | 11 ++++++ 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.css b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.css index d6a498e93872..032957940681 100644 --- a/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.css +++ b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.css @@ -15,35 +15,35 @@ * limitations under the License. */ -#plan-viz-graph .label { +svg g.label { font-size: 0.85rem; font-weight: normal; text-shadow: none; color: #333; } -#plan-viz-graph svg g.cluster rect { +svg g.cluster rect { fill: #A0DFFF; stroke: #3EC0FF; stroke-width: 1px; } -#plan-viz-graph svg g.node rect { +svg g.node rect { fill: #C3EBFF; stroke: #3EC0FF; stroke-width: 1px; } /* Highlight the SparkPlan node name */ -#plan-viz-graph svg text :first-child:not(.stageId-and-taskId-metrics) { +svg text :first-child:not(.stageId-and-taskId-metrics) { font-weight: bold; } -#plan-viz-graph svg text { +svg text { fill: #333; } -#plan-viz-graph svg path { +svg path { stroke: #444; stroke-width: 1.5px; } @@ -58,19 +58,19 @@ word-wrap: break-word; } -#plan-viz-graph svg g.node rect.selected { +svg g.node rect.selected { fill: #E25A1CFF; stroke: #317EACFF; stroke-width: 2px; } -#plan-viz-graph svg g.node rect.linked { +svg g.node rect.linked { fill: #FFC106FF; stroke: #317EACFF; stroke-width: 2px; } -#plan-viz-graph svg path.linked { +svg path.linked { fill: #317EACFF; stroke: #317EACFF; stroke-width: 2px; diff --git a/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.js b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.js index d4cc45a1639a..2159b0c42908 100644 --- a/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.js +++ b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.js @@ -312,3 +312,40 @@ function collectLinks(map, key, value) { } map.get(key).add(value); } + +function downloadPlanBlob(b, ext) { + const link = document.createElement("a"); + link.href = URL.createObjectURL(b); + link.download = `plan.${ext}`; + link.click(); +} + +document.getElementById("plan-viz-download-btn").addEventListener("click", async function () { + const format = document.getElementById("plan-viz-format-select").value; + let blob; + switch (format) { + case "svg": + const svg = planVizContainer().select("svg").node().cloneNode(true); + let css = ""; + try { + css = await fetch("/static/sql/spark-sql-viz.css").then((resp) => resp.text()); + } catch (e) { + console.error("Failed to fetch CSS for SVG download", e); + } + d3.select(svg).insert("style", ":first-child").text(css); + const svgData = new XMLSerializer().serializeToString(svg); + blob = new Blob([svgData], { type: "image/svg+xml" }); + break; + case "dot": + const dot = d3.select("#plan-viz-metadata .dot-file").text().trim(); + blob = new Blob([dot], { type: "text/plain" }); + break; + case "txt": + const txt = d3.select("#physical-plan-details pre").text().trim(); + blob = new Blob([txt], { type: "text/plain" }); + break; + default: + return; + } + downloadPlanBlob(blob, format); +}); diff --git a/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/ExecutionPage.scala b/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/ExecutionPage.scala index bbdf9b4c4bd8..84e8291dfad0 100644 --- a/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/ExecutionPage.scala +++ b/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/ExecutionPage.scala @@ -122,6 +122,17 @@ class ExecutionPage(parent: SQLTab) extends WebUIPage("execution") with Logging +
+ + +
+
From d4145341744f5e7d220899d5fef579e970a8a91a Mon Sep 17 00:00:00 2001 From: Kent Yao Date: Thu, 27 Mar 2025 14:18:58 +0800 Subject: [PATCH 2/3] [SPARK-51629][UI] Add a download link on ExecutionPage for svg/dot/txt format plans --- .../sql/execution/ui/ExecutionPage.scala | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/ExecutionPage.scala b/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/ExecutionPage.scala index 84e8291dfad0..e705b5cf46ed 100644 --- a/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/ExecutionPage.scala +++ b/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/ExecutionPage.scala @@ -75,6 +75,16 @@ class ExecutionPage(parent: SQLTab) extends WebUIPage("execution") with Logging {jobLinks(JobExecutionStatus.SUCCEEDED, "Succeeded Jobs:")} {jobLinks(JobExecutionStatus.FAILED, "Failed Jobs:")} +
+ + +
val metrics = sqlStore.executionMetrics(executionId) @@ -122,17 +132,6 @@ class ExecutionPage(parent: SQLTab) extends WebUIPage("execution") with Logging
-
- - -
-
From a2d3faeaee185ba7b57462194fc6b648b82e1d89 Mon Sep 17 00:00:00 2001 From: Kent Yao Date: Thu, 27 Mar 2025 16:28:05 +0800 Subject: [PATCH 3/3] style fix --- dev/eslint.js | 3 +- .../sql/execution/ui/static/spark-sql-viz.js | 42 +++++++++---------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/dev/eslint.js b/dev/eslint.js index 24b5170b436a..abb06526fe96 100644 --- a/dev/eslint.js +++ b/dev/eslint.js @@ -40,6 +40,7 @@ module.exports = { "dataTables.rowsGroup.js" ], "parserOptions": { - "sourceType": "module" + "sourceType": "module", + "ecmaVersion": "latest" } } diff --git a/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.js b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.js index 2159b0c42908..37bae5e4b774 100644 --- a/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.js +++ b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.js @@ -323,29 +323,25 @@ function downloadPlanBlob(b, ext) { document.getElementById("plan-viz-download-btn").addEventListener("click", async function () { const format = document.getElementById("plan-viz-format-select").value; let blob; - switch (format) { - case "svg": - const svg = planVizContainer().select("svg").node().cloneNode(true); - let css = ""; - try { - css = await fetch("/static/sql/spark-sql-viz.css").then((resp) => resp.text()); - } catch (e) { - console.error("Failed to fetch CSS for SVG download", e); - } - d3.select(svg).insert("style", ":first-child").text(css); - const svgData = new XMLSerializer().serializeToString(svg); - blob = new Blob([svgData], { type: "image/svg+xml" }); - break; - case "dot": - const dot = d3.select("#plan-viz-metadata .dot-file").text().trim(); - blob = new Blob([dot], { type: "text/plain" }); - break; - case "txt": - const txt = d3.select("#physical-plan-details pre").text().trim(); - blob = new Blob([txt], { type: "text/plain" }); - break; - default: - return; + if (format === "svg") { + const svg = planVizContainer().select("svg").node().cloneNode(true); + let css = ""; + try { + css = await fetch("/static/sql/spark-sql-viz.css").then((resp) => resp.text()); + } catch (e) { + console.error("Failed to fetch CSS for SVG download", e); + } + d3.select(svg).insert("style", ":first-child").text(css); + const svgData = new XMLSerializer().serializeToString(svg); + blob = new Blob([svgData], { type: "image/svg+xml" }); + } else if (format === "dot") { + const dot = d3.select("#plan-viz-metadata .dot-file").text().trim(); + blob = new Blob([dot], { type: "text/plain" }); + } else if (format === "txt") { + const txt = d3.select("#physical-plan-details pre").text().trim(); + blob = new Blob([txt], { type: "text/plain" }); + } else { + return; } downloadPlanBlob(blob, format); });