andygrove commented on code in PR #6027:
URL: https://github.com/apache/datafusion-comet/pull/6027#discussion_r4051234032
##########
spark/src/main/scala/org/apache/spark/sql/comet/CometIcebergNativeScanExec.scala:
##########
@@ -208,16 +212,49 @@ case class CometIcebergNativeScanExec(
override lazy val metrics: Map[String, SQLMetric] = {
val baseMetrics = Map(
"output_rows" -> SQLMetrics.createMetric(sparkContext, "number of output
rows"),
- "bytes_scanned" -> SQLMetrics.createSizeMetric(sparkContext, "number of
bytes scanned"))
+ "bytes_scanned" -> SQLMetrics.createSizeMetric(sparkContext, "number of
bytes scanned"),
+ // Native read/decode time, aggregated across tasks. Fed by
iceberg-rust's BaselineMetrics
+ // (elapsed_compute, in nanoseconds) through the standard JNI metric
path.
+ "elapsed_compute" -> SQLMetrics.createNanoTimingMetric(sparkContext,
"scan time"))
// Add num_splits as a runtime metric (incremented on the native side
during execution)
val numSplitsMetric = SQLMetrics.createMetric(sparkContext, "number of
file splits processed")
baseMetrics ++ icebergPlanningMetrics + ("num_splits" -> numSplitsMetric)
}
+ /**
+ * Posts the Iceberg planning metrics (data/delete file and manifest counts,
file sizes, and
+ * total planning duration) to the SQL UI as driver metrics. Iceberg-Java
produces these during
+ * planFiles(); they live on the driver and are never updated by executor
tasks, so without an
+ * explicit post the SQL UI never receives their accumulator ids and the
scan node shows
+ * nothing. Mirrors [[CometScanExec.sendDriverMetrics]] for the Parquet
path. The native runtime
+ * metrics (output_rows, bytes_scanned, num_splits, elapsed_compute) travel
the separate JNI
+ * path and are not posted here.
+ *
+ * Called from two places because a native leaf scan does not always run its
own
+ * doExecuteColumnar: when this scan is fused under a parent native operator
(e.g. a CometFilter
+ * for a pushed predicate), the parent runs the whole subtree as one RDD and
this node's
+ * doExecuteColumnar is never invoked. CometNativeExec.findAllPlanData walks
the subtree at
+ * execution time and reaches every leaf scan (calling this leaf lifecycle
hook alongside
+ * ensureSubqueriesResolved), so it calls this too. Re-posting the same
values is harmless.
Review Comment:
I do not think re-posting is harmless.
`SQLAppStatusListener.onDriverAccumUpdates` appends with
`exec.driverAccumUpdates ++ accumUpdates`, and `aggregateMetrics` appends each
driver value into that accumulator's value array, after which
`SQLMetrics.stringValue` returns `values.sum` for `SUM_METRIC`. So a second
post of the same (id, value) pair would double `totalDataManifest`,
`resultDataFiles` and the rest, and shift the total and the min/med/max shown
for the size and timing ones. Same behaviour on 3.4, 3.5 and 4.0.
I walked the call graph and could not find a path that fires both sites
today. This class overrides `doExecuteColumnar` so it never reaches
`buildNativeContext`, and `buildNativeContext` no-ops `CometNativeExec`
children when it builds `inputs`, so the fused case and the standalone case
really are exclusive. Nothing is broken right now.
But that is a quiet invariant to rest on for a hook whose whole point is
being callable from two places, and the precedent we are citing goes the other
way. `CometScanExec.sendDriverMetrics` posts from inside the `filePartitions`
lazy val and so runs exactly once, and `FileSourceScanExec` does the same.
Could we guard on the execution id in a follow-up, something like a `@transient
private var postedExecutionId` that skips when it matches? Then the comment
would be promising something that actually holds.
##########
spark/src/main/scala/org/apache/spark/sql/comet/operators.scala:
##########
@@ -435,6 +435,11 @@ private[comet] object PlanDataInjector extends Logging {
// executeQuery, leaving DPP unresolved and forcing a sync-on-this
await inside
// the serializedPartitionData lazy val initializer (a known deadlock
surface).
iceberg.ensureSubqueriesResolved()
+ // Post the Iceberg planning metrics to the SQL UI here rather than
only from the scan's
+ // doExecuteColumnar: when the scan is fused under a parent native
operator, its own
+ // doExecuteColumnar never runs, so this walk is the only
execution-time hook that reaches
+ // it. Safe to also call for a root scan (re-posting the same values
is a no-op).
+ iceberg.sendDriverMetrics()
Review Comment:
Same point as on `CometIcebergNativeScanExec.sendDriverMetrics`. A second
post is not a no-op, Spark appends driver accumulator updates and sums them for
sum-typed metrics.
##########
spark/src/test/scala/org/apache/comet/CometIcebergNativeSuite.scala:
##########
@@ -2048,6 +2048,104 @@ class CometIcebergNativeSuite
}
}
+ test("Iceberg planning metrics and scan time are posted to the SQL UI") {
Review Comment:
Building on Sun Chao's point about the raw duration. I think this test stays
green if the Rust timer is removed. `createNanoTimingMetric` starts at -1, and
`CometMetricNode.set` writes whatever native reports including 0, which is
enough to move it off -1 and get an entry into the store, so
`uiValues.contains` passes either way. Could we add
`assert(metrics("elapsed_compute").value > 0)` to "verify all Iceberg planning
metrics are populated" instead? That test already has the table and the node
handle sitting next to the `bytes_scanned` and `num_splits` assertions, so it
would also save a second 10k-row Iceberg write in a suite that is already slow.
Separately, `WHERE id < 5000` is on a non-partition column, so Iceberg
leaves it in `postScanFilters` and the scan ends up fused under a
`CometFilterExec`. If that is right then only the `findAllPlanData` hook runs
here and `doExecuteColumnar` never does, so we are covering one of the two call
sites and cannot tell which from the assertions. A bare `SELECT *`, or a
partition-column predicate like the one "verify manifest pruning metrics" uses,
would give the other shape. An AQE variant would be worth having too. This
query has no exchange and no subquery so `InsertAdaptiveSparkPlan` skips it,
and fused plus DPP is the case the hook exists for.
##########
spark/src/main/scala/org/apache/spark/sql/comet/CometIcebergNativeScanExec.scala:
##########
@@ -208,16 +212,49 @@ case class CometIcebergNativeScanExec(
override lazy val metrics: Map[String, SQLMetric] = {
val baseMetrics = Map(
"output_rows" -> SQLMetrics.createMetric(sparkContext, "number of output
rows"),
- "bytes_scanned" -> SQLMetrics.createSizeMetric(sparkContext, "number of
bytes scanned"))
+ "bytes_scanned" -> SQLMetrics.createSizeMetric(sparkContext, "number of
bytes scanned"),
+ // Native read/decode time, aggregated across tasks. Fed by
iceberg-rust's BaselineMetrics
+ // (elapsed_compute, in nanoseconds) through the standard JNI metric
path.
+ "elapsed_compute" -> SQLMetrics.createNanoTimingMetric(sparkContext,
"scan time"))
Review Comment:
Could we get a `CometIcebergNativeScan` section into
`docs/source/user-guide/latest/metrics.md`? The only `scan time` row there
today is under `CometScanExec` and reads "Total time to scan a Parquet file",
and this one means something different. The timer wraps
`IcebergStreamWrapper::poll_next`, so it covers driving the reader plus schema
adaptation but drops anything that comes back `Pending`, which makes it decode
time rather than scan latency. Someone comparing the two numbers would not
guess that.
This is also the first time the Iceberg planning metrics are visible in the
UI at all, so a table covering those plus `bytes_scanned` and `num_splits`
would earn its keep. The `numDeletes` explanation you added just above reads
like it belongs in that doc too, it is more useful to users than to readers of
this file.
##########
spark/src/main/scala/org/apache/spark/sql/comet/CometIcebergNativeScanExec.scala:
##########
@@ -208,16 +212,49 @@ case class CometIcebergNativeScanExec(
override lazy val metrics: Map[String, SQLMetric] = {
val baseMetrics = Map(
"output_rows" -> SQLMetrics.createMetric(sparkContext, "number of output
rows"),
- "bytes_scanned" -> SQLMetrics.createSizeMetric(sparkContext, "number of
bytes scanned"))
+ "bytes_scanned" -> SQLMetrics.createSizeMetric(sparkContext, "number of
bytes scanned"),
+ // Native read/decode time, aggregated across tasks. Fed by
iceberg-rust's BaselineMetrics
+ // (elapsed_compute, in nanoseconds) through the standard JNI metric
path.
+ "elapsed_compute" -> SQLMetrics.createNanoTimingMetric(sparkContext,
"scan time"))
// Add num_splits as a runtime metric (incremented on the native side
during execution)
val numSplitsMetric = SQLMetrics.createMetric(sparkContext, "number of
file splits processed")
baseMetrics ++ icebergPlanningMetrics + ("num_splits" -> numSplitsMetric)
}
+ /**
+ * Posts the Iceberg planning metrics (data/delete file and manifest counts,
file sizes, and
+ * total planning duration) to the SQL UI as driver metrics. Iceberg-Java
produces these during
+ * planFiles(); they live on the driver and are never updated by executor
tasks, so without an
+ * explicit post the SQL UI never receives their accumulator ids and the
scan node shows
+ * nothing. Mirrors [[CometScanExec.sendDriverMetrics]] for the Parquet
path. The native runtime
+ * metrics (output_rows, bytes_scanned, num_splits, elapsed_compute) travel
the separate JNI
+ * path and are not posted here.
+ *
+ * Called from two places because a native leaf scan does not always run its
own
+ * doExecuteColumnar: when this scan is fused under a parent native operator
(e.g. a CometFilter
+ * for a pushed predicate), the parent runs the whole subtree as one RDD and
this node's
+ * doExecuteColumnar is never invoked. CometNativeExec.findAllPlanData walks
the subtree at
+ * execution time and reaches every leaf scan (calling this leaf lifecycle
hook alongside
+ * ensureSubqueriesResolved), so it calls this too. Re-posting the same
values is harmless.
+ */
+ override def sendDriverMetrics(): Unit = {
+ if (icebergPlanningMetrics.isEmpty) {
+ return
+ }
+ // Force planning so originalPlan.metrics are populated;
LazyIcebergMetric.value reads them.
+ val _ = serializedPartitionData
Review Comment:
Is this force needed? `postDriverMetricUpdates` reads `m.value`, and
`LazyIcebergMetric.value` already does `ensureSubqueriesResolved()` and then
forces `serializedPartitionData` itself. This line is the one variant that
forces planning without resolving DPP first, so dropping it would keep that
ordering in a single place.
--
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]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]