You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by ur...@apache.org on 2026/09/01 10:49:48 UTC
(spark) branch branch-4.3 updated: [SPARK-58775][UI] Fix SQL metrics filter and min,med,max table interaction
This is an automated email from the ASF dual-hosted git repository.
uros-b pushed a commit to branch branch-4.3
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.3 by this push:
new b489b6bdf2b8 [SPARK-58775][UI] Fix SQL metrics filter and min,med,max table interaction
b489b6bdf2b8 is described below
commit b489b6bdf2b89101ff3648fb2e4a460fc8dab769
Author: Will Gan <19...@users.noreply.github.com>
AuthorDate: Tue Sep 1 10:48:40 2026 +0000
[SPARK-58775][UI] Fix SQL metrics filter and min,med,max table interaction
### What changes were proposed in this pull request?
Change the way filtering metrics is done in `spark-sql-viz.js` to fix bug with metrics showing blank
### Why are the changes needed?
https://github.com/apache/spark/pull/54887 introduced a filter for SQL metrics. It is very useful, but when a metric has min, med, and max values, it shows the metric as blank
<img width="581" height="68" alt="image" src="https://github.com/user-attachments/assets/a6563927-a048-4fac-b514-e4dab8f4d6d9" />
<img width="611" height="111" alt="image" src="https://github.com/user-attachments/assets/12f224bd-482d-41b5-9564-c2a7a959113a" />
### Does this PR introduce _any_ user-facing change?
Yes, it affects the Spark UI
### How was this patch tested?
- Added test in ui-test/
- Verified locally
<img width="607" height="134" alt="image" src="https://github.com/user-attachments/assets/3808e7bb-6372-4334-84ee-247de02316ab" />
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code / Opus 4.8
Closes #57997 from wjgan7/spark-sql-viz-metric-filter-fix.
Lead-authored-by: Will Gan <19...@users.noreply.github.com>
Co-authored-by: Will Gan <wi...@databricks.com>
Signed-off-by: Uros Bojanic <22...@users.noreply.github.com>
(cherry picked from commit e484d9adfe341476a84a151444996944f88e6135)
Signed-off-by: Uros Bojanic <22...@users.noreply.github.com>
---
.../spark/sql/execution/ui/static/spark-sql-viz.js | 14 ++--
ui-test/tests/spark-sql-viz.test.js | 80 ++++++++++++++++++++++
2 files changed, 89 insertions(+), 5 deletions(-)
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 7c1391c9d2b4..38cdd6a7bcd9 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
@@ -500,15 +500,19 @@ function updateDetailsPanel(nodeId, nodeDetails) {
var searchBox = document.getElementById("metric-search");
if (searchBox) {
searchBox.addEventListener("input", function () {
- var query = this.value.toLowerCase();
- bodyEl.querySelectorAll("table tbody tr").forEach(function (row) {
- var metricName = row.cells[0] ? row.cells[0].textContent.toLowerCase() : "";
- row.style.display = metricName.indexOf(query) >= 0 ? "" : "none";
- });
+ filterMetricRows(bodyEl, this.value);
});
}
}
+function filterMetricRows(container, query) {
+ var q = query.toLowerCase();
+ container.querySelectorAll("table.sortable > tbody > tr").forEach(function (row) {
+ var metricName = row.cells[0] ? row.cells[0].textContent.toLowerCase() : "";
+ row.style.display = metricName.indexOf(q) >= 0 ? "" : "none";
+ });
+}
+
function htmlEscape(str) {
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
.replace(/"/g, """).replace(/'/g, "'");
diff --git a/ui-test/tests/spark-sql-viz.test.js b/ui-test/tests/spark-sql-viz.test.js
new file mode 100644
index 000000000000..1597b8619b42
--- /dev/null
+++ b/ui-test/tests/spark-sql-viz.test.js
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+
+/**
+ * @jest-environment jsdom
+ */
+
+import { readFileSync } from 'fs';
+import { join, dirname } from 'path';
+import { fileURLToPath } from 'url';
+
+// spark-sql-viz.js is a classic (non-module) script, so read it and return the helpers under test.
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const vizPath = join(
+ __dirname,
+ '../../sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.js');
+const src = readFileSync(vizPath, 'utf8');
+
+window.$ = function () {};
+document.body.innerHTML = '<button id="plan-viz-download-btn"></button>';
+const { buildMetricsTable, filterMetricRows } = new Function(
+ src + '\nreturn { buildMetricsTable, filterMetricRows };')();
+
+// A size metric renders a nested Total/Min/Med/Max stat sub-table inside its value cell.
+const SIZE_VALUE =
+ 'total (min, med, max (stageId: taskId))\n25.0 MiB (3.8 MiB, N/A, 4.8 MiB (stage 2.0: task 4))';
+
+function renderPanel(metrics) {
+ document.body.innerHTML = buildMetricsTable(metrics, false, true);
+ return document.body;
+}
+
+test('filterMetricRows keeps the matched metric value visible', function () {
+ const body = renderPanel([
+ { name: 'shuffle bytes written', value: SIZE_VALUE, type: 'size' },
+ { name: 'number of output rows', value: '10', type: 'sum' }
+ ]);
+
+ filterMetricRows(body, 'shuffle');
+
+ const outerRows = [...body.querySelectorAll('table.sortable > tbody > tr')];
+ const matched = outerRows.find(r => /shuffle bytes written/.test(r.cells[0].textContent));
+ const other = outerRows.find(r => /output rows/.test(r.cells[0].textContent));
+
+ expect(matched.style.display).toBe('');
+ expect(other.style.display).toBe('none');
+
+ // The nested stat sub-table rows must not be hidden, else the value renders blank.
+ const statRows = matched.querySelectorAll('td table tr');
+ expect(statRows.length).toBeGreaterThan(0);
+ statRows.forEach(r => expect(r.style.display).not.toBe('none'));
+ expect(matched.cells[1].textContent).toContain('25.0 MiB');
+});
+
+test('filterMetricRows restores all rows when the query is cleared', function () {
+ const body = renderPanel([
+ { name: 'shuffle bytes written', value: SIZE_VALUE, type: 'size' },
+ { name: 'number of output rows', value: '10', type: 'sum' }
+ ]);
+
+ filterMetricRows(body, 'shuffle');
+ filterMetricRows(body, '');
+
+ body.querySelectorAll('table.sortable > tbody > tr')
+ .forEach(r => expect(r.style.display).toBe(''));
+});
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org