Copilot commented on code in PR #3793:
URL: https://github.com/apache/celeborn/pull/3793#discussion_r3803364532


##########
client-spark/spark-3-ui/src/main/scala/org/apache/spark/shuffle/celeborn/CelebornStatusStore.scala:
##########
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+package org.apache.spark.shuffle.celeborn
+
+import com.fasterxml.jackson.annotation.JsonIgnore
+import org.apache.spark.util.kvstore.{KVIndex, KVStore}
+
+private[celeborn] case class AggregatedTaskInfoUIData(
+    shuffleWriteBytes: Long,
+    shuffleWriteTimeMs: Long,
+    shuffleReadBytes: Long,
+    shuffleFetchWaitTimeMs: Long,
+    taskDurationMs: Long) {
+
+  @JsonIgnore
+  @KVIndex
+  def id: String = classOf[AggregatedTaskInfoUIData].getName
+}
+
+private[celeborn] class CelebornPropertiesUIData(
+    val info: Seq[(String, String)]) {
+
+  @JsonIgnore
+  @KVIndex
+  def id: String = classOf[CelebornPropertiesUIData].getName
+}

Review Comment:
   `CelebornPropertiesUIData` is persisted to Spark's `KVStore` 
(Jackson-backed). As a non-case class with a constructor parameter, it may not 
deserialize reliably across Spark components (especially in History Server 
replay). Make this a `case class` (preferred in Spark UI store models) or add 
an explicit Jackson creator/props. Also consider avoiding `Seq[(String, 
String)]` tuples in persisted data (use a small `case class` like `{ name: 
String, value: String }` or store as `Map[String, String]`) to reduce 
serialization edge cases.



##########
client-spark/spark-3-ui/src/main/scala/org/apache/spark/shuffle/celeborn/CelebornHistoryServerPlugin.scala:
##########
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+
+package org.apache.spark.shuffle.celeborn
+
+import org.apache.spark.SparkConf
+import org.apache.spark.scheduler.SparkListener
+import org.apache.spark.shuffle.celeborn.ui.CelebornUITab
+import org.apache.spark.status.{AppHistoryServerPlugin, ElementTrackingStore}
+import org.apache.spark.ui.SparkUI
+import org.apache.spark.util.kvstore.KVStore
+
+/** Registered via SPI at 
META-INF/services/org.apache.spark.status.AppHistoryServerPlugin. */
+class CelebornHistoryServerPlugin extends AppHistoryServerPlugin {
+
+  override def createListeners(
+      conf: SparkConf,
+      store: ElementTrackingStore): Seq[SparkListener] = {
+    Seq(new CelebornListener(store, conf))
+  }
+
+  override def setupUI(ui: SparkUI): Unit = {
+    val kvstore: KVStore = ui.store.store
+    val statusStore = new CelebornStatusStore(kvstore)
+    if (statusStore.hasData()) {
+      new CelebornUITab(statusStore, ui)
+    }

Review Comment:
   `setupUI` only attaches the tab when `hasData()` reports non-zero shuffle 
bytes. This can hide the tab in History Server for apps that have Celeborn 
configs captured (or want to see '0' summary) but no shuffle bytes recorded, 
which is inconsistent with the live UI path (always attaches the tab when the 
UI exists). Consider attaching unconditionally in SHS as well, or broadening 
the condition to include presence of stored Celeborn properties / stored 
records.



##########
client-spark/spark-3-ui/src/main/scala/org/apache/spark/shuffle/celeborn/ui/CelebornShufflePage.scala:
##########
@@ -0,0 +1,130 @@
+/*
+ * 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.
+ */
+
+package org.apache.spark.shuffle.celeborn.ui
+
+import scala.xml.Node
+
+import org.apache.spark.internal.Logging
+import 
org.apache.spark.shuffle.celeborn.ui.SparkServletBridge.HttpServletRequest
+import org.apache.spark.ui.{UIUtils, WebUIPage}
+import org.apache.spark.util.Utils
+
+private[celeborn] class CelebornShufflePage(parent: CelebornUITab)
+  extends WebUIPage("") with Logging {
+
+  private val store = parent.store
+
+  override def render(request: HttpServletRequest): Seq[Node] = {
+    try {
+      renderBody(request)
+    } catch {
+      case e: Throwable =>
+        logError("Failed to render Celeborn Shuffle page", e)
+        val errorContent =
+          <div class="row-fluid">
+            <div class="span12">
+              <h4>
+                <strong>Celeborn Shuffle</strong>
+              </h4>
+              <div class="alert alert-error">
+                <pre>Failed to render the Celeborn page: {e.getMessage}</pre>
+              </div>
+            </div>
+          </div>
+        UIUtils.headerSparkPage(request, "Celeborn Shuffle", errorContent, 
parent)
+    }
+  }
+
+  private def renderBody(request: HttpServletRequest): Seq[Node] = {
+    val taskInfo = store.aggregatedTaskInfo()
+    val properties = store.celebornProperties()
+
+    val writeBytes = taskInfo.shuffleWriteBytes
+    val readBytes = taskInfo.shuffleReadBytes
+    val writeMs = taskInfo.shuffleWriteTimeMs
+    val readMs = taskInfo.shuffleFetchWaitTimeMs
+    val durationMs = taskInfo.taskDurationMs
+
+    def mbps(bytes: Long, ms: Long): String =
+      if (ms <= 0) "N/A" else f"${bytes.toDouble / 1000.0 / 1000.0 / 
(ms.toDouble / 1000.0)}%.2f"
+    def pct(part: Long, total: Long): String =
+      if (total <= 0) "N/A" else f"${part.toDouble * 100.0 / 
total.toDouble}%.1f%%"
+
+    val summary =
+      <div>
+        <ul class="list-unstyled">
+          <li>
+            <strong>Shuffle Write: </strong>
+            {
+        s"${Utils.bytesToString(writeBytes)} | Time: ${UIUtils.formatDuration(
+          writeMs)} | Speed: ${mbps(writeBytes, writeMs)} MB/s"
+      }
+          </li>
+          <li>
+            <strong>Shuffle Read: </strong>
+            {
+        s"${Utils.bytesToString(readBytes)} | Time: ${UIUtils.formatDuration(
+          readMs)} | Speed: ${mbps(readBytes, readMs)} MB/s"
+      }
+          </li>
+          <li>
+            <strong>Shuffle Duration (write+read) / Task Duration: </strong>
+            {
+        s"${pct(writeMs + readMs, durationMs)} (Write ${pct(
+          writeMs,
+          durationMs)}, Read ${pct(readMs, durationMs)})"
+      }
+          </li>
+        </ul>
+      </div>
+
+    val propertiesTable = UIUtils.listingTable(
+      propertyHeader,
+      propertyRow,
+      properties.info,
+      fixedWidth = true,
+      headerClasses = headerClasses)
+
+    val content =
+      <span>
+        {summary}
+        <span class="collapse-aggregated-celebornProperties collapse-table"
+            onClick="collapseTable('collapse-aggregated-celebornProperties',
+            'aggregated-celebornProperties')">
+          <h4>
+            <span class="collapse-table-arrow arrow-open"></span>
+            <a>Celeborn Properties</a>
+          </h4>
+        </span>
+        <div class="aggregated-celebornProperties collapsible-table">

Review Comment:
   The collapse target identifiers/classes include mixed casing 
(`celebornProperties`). Spark UI’s built-in collapse-table patterns typically 
use lowercase/hyphenated identifiers; sticking to that convention reduces the 
chance of mismatches in CSS/JS selectors and avoids subtle HTML/CSS 
case-sensitivity surprises. Consider renaming these to lowercase, 
hyphen-separated tokens.



##########
client-spark/spark-3-ui/src/main/scala/org/apache/spark/shuffle/celeborn/CelebornListener.scala:
##########
@@ -0,0 +1,101 @@
+/*
+ * 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.
+ */
+
+package org.apache.spark.shuffle.celeborn
+
+import java.util.concurrent.atomic.AtomicLong
+
+import org.apache.spark.SparkConf
+import org.apache.spark.internal.Logging
+import org.apache.spark.scheduler._
+import org.apache.spark.util.kvstore.KVStore
+
+/**
+ * Collects Celeborn shuffle metrics into the Spark KVStore for live UI and
+ * HistoryServer replay.
+ */
+private[celeborn] class CelebornListener(
+    val kvstore: KVStore,
+    val conf: SparkConf)
+  extends SparkListener with Logging {
+
+  private val totalWriteBytes = new AtomicLong(0L)
+  private val totalWriteTimeMs = new AtomicLong(0L)
+  private val totalReadBytes = new AtomicLong(0L)
+  private val totalFetchWaitTimeMs = new AtomicLong(0L)
+  private val totalTaskDurationMs = new AtomicLong(0L)
+
+  private val lastUpdateTimestamp = new AtomicLong(-1L)
+  private val updateIntervalMillis = 5000L

Review Comment:
   `updateIntervalMillis` is a magic constant. If this listener ends up being 
used in large apps, the best flush cadence may differ (write amplification vs 
UI freshness). Consider making this configurable via `SparkConf` (e.g., 
`spark.celeborn.ui.flushIntervalMs`) with a sensible default.



##########
client-spark/spark-3-ui/src/main/scala/org/apache/spark/shuffle/celeborn/CelebornListener.scala:
##########
@@ -0,0 +1,101 @@
+/*
+ * 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.
+ */
+
+package org.apache.spark.shuffle.celeborn
+
+import java.util.concurrent.atomic.AtomicLong
+
+import org.apache.spark.SparkConf
+import org.apache.spark.internal.Logging
+import org.apache.spark.scheduler._
+import org.apache.spark.util.kvstore.KVStore
+
+/**
+ * Collects Celeborn shuffle metrics into the Spark KVStore for live UI and
+ * HistoryServer replay.
+ */
+private[celeborn] class CelebornListener(
+    val kvstore: KVStore,
+    val conf: SparkConf)
+  extends SparkListener with Logging {
+
+  private val totalWriteBytes = new AtomicLong(0L)
+  private val totalWriteTimeMs = new AtomicLong(0L)
+  private val totalReadBytes = new AtomicLong(0L)
+  private val totalFetchWaitTimeMs = new AtomicLong(0L)
+  private val totalTaskDurationMs = new AtomicLong(0L)
+
+  private val lastUpdateTimestamp = new AtomicLong(-1L)
+  private val updateIntervalMillis = 5000L
+
+  def register(sc: org.apache.spark.SparkContext): Unit = {
+    sc.addSparkListener(this)
+    logInfo("CelebornListener registered successfully")
+  }
+
+  override def onTaskEnd(taskEnd: SparkListenerTaskEnd): Unit = {
+    Option(taskEnd.taskMetrics).foreach { metrics =>
+      totalWriteBytes.addAndGet(metrics.shuffleWriteMetrics.bytesWritten)
+      // writeTime is in nanoseconds; normalize to ms.
+      totalWriteTimeMs.addAndGet(metrics.shuffleWriteMetrics.writeTime / 
1000000L)
+      totalReadBytes.addAndGet(metrics.shuffleReadMetrics.totalBytesRead)
+      totalFetchWaitTimeMs.addAndGet(metrics.shuffleReadMetrics.fetchWaitTime)
+      totalTaskDurationMs.addAndGet(taskEnd.taskInfo.duration)
+    }
+    mayUpdate()
+  }
+
+  override def onEnvironmentUpdate(environmentUpdate: 
SparkListenerEnvironmentUpdate): Unit = {
+    val celebornProps = environmentUpdate.environmentDetails
+      .getOrElse("Spark Properties", Seq.empty)
+      .filter { case (k, _) => k.startsWith("spark.celeborn.") }
+      .sortBy(_._1)
+    if (celebornProps.nonEmpty) {
+      kvstore.write(new CelebornPropertiesUIData(celebornProps.toList))
+    }
+  }
+
+  override def onApplicationEnd(applicationEnd: SparkListenerApplicationEnd): 
Unit = {
+    mayUpdate(force = true)
+    logInfo("CelebornListener: application ended, final flush completed")
+  }
+
+  private def mayUpdate(force: Boolean = false): Unit = {
+    val now = System.currentTimeMillis()
+    val last = lastUpdateTimestamp.get()
+    if (!force && (last != -1L && (now - last) < updateIntervalMillis)) {
+      return
+    }
+    if (lastUpdateTimestamp.compareAndSet(last, now) || force) {
+      flushAggregations()
+    }

Review Comment:
   When `force = true`, the code flushes even if `compareAndSet` fails, but it 
does not update `lastUpdateTimestamp` in that path. This can lead to extra 
flushes immediately after a forced flush if subsequent events arrive. Prefer 
updating the timestamp on forced flushes as well (and typically structure the 
CAS as a loop so timestamp changes are consistent).



##########
pom.xml:
##########
@@ -1490,6 +1490,7 @@
         <module>client-spark/spark-3</module>
         <module>client-spark/spark-3-columnar-common</module>
         <module>client-spark/spark-3-columnar-shuffle</module>
+        <module>client-spark/spark-3-ui</module>

Review Comment:
   The module/artifact name `spark-3-ui` suggests Spark 3-only, but the PR 
description and build profiles indicate it also targets Spark 4 (via the 
servlet bridge). Consider renaming to something version-neutral (e.g., 
`spark-ui` / `spark-ui-plugin`) to avoid confusion for users and packagers 
(especially since Spark 4 shaded artifacts also depend on 
`celeborn-client-spark-3-ui_*`).



##########
client-spark/spark-3-ui/pom.xml:
##########
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.apache.celeborn</groupId>
+    <artifactId>celeborn-parent_${scala.binary.version}</artifactId>
+    <version>${project.version}</version>
+    <relativePath>../../pom.xml</relativePath>
+  </parent>
+
+  <artifactId>celeborn-client-spark-3-ui_${scala.binary.version}</artifactId>
+  <packaging>jar</packaging>
+  <name>Celeborn Spark UI Plugin</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.celeborn</groupId>
+      <artifactId>celeborn-client-spark-3_${scala.binary.version}</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.spark</groupId>
+      <artifactId>spark-core_${scala.binary.version}</artifactId>
+      <scope>provided</scope>
+      <exclusions>
+        <exclusion>
+          <groupId>org.xerial.snappy</groupId>
+          <artifactId>snappy-java</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>javax.servlet-api</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>jakarta.servlet</groupId>
+      <artifactId>jakarta.servlet-api</artifactId>
+      <scope>provided</scope>
+    </dependency>

Review Comment:
   Both `javax.servlet-api` and `jakarta.servlet-api` are declared for the same 
artifact. Even with `provided` scope, this increases the chance of accidental 
cross-version imports and complicates dependency resolution in downstream 
builds. Since the source root is already selected via `${servlet.source.dir}`, 
consider moving the servlet dependency selection into Spark-version-specific 
profiles (include only `javax` for Spark 3 builds and only `jakarta` for Spark 
4 builds).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to