dongjoon-hyun commented on code in PR #59764:
URL: https://github.com/apache/spark/pull/59764#discussion_r4368343232


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/TungstenAggregationIterator.scala:
##########
@@ -551,16 +541,10 @@ class TungstenAggregationIterator(
         }
       } else {
         // Adaptive partial aggregation bypassed partial aggregation: emit the 
remaining input
-        // rows as single-row partial buffers, after the frozen map output 
above.
-        if (passThroughDrainStart == -1L) {
-          passThroughDrainStart = System.nanoTime()
-        }
-        val out = nextPassThroughOutput()
-        if (!passThroughHasNext) {
-          aggTime += NANOSECONDS.toMillis(System.nanoTime() - 
passThroughDrainStart)
-          passThroughDrainStart = -1L
-        }
-        out
+        // rows as single-row partial buffers, after the frozen map output 
above. The drain is
+        // not timed: `next()` is pulled lazily, so timing from the first to 
the last pass-through

Review Comment:
   **1. Removing this timer brings back the `aggTime` gap that #57742 closed on 
review.** In #57742, viirya and peter-toth ([Finding 
7](https://github.com/apache/spark/pull/57742#discussion_r3734471987)) pointed 
out that `doExecute` times only this iterator's constructor, so once 
pass-through fires, the rows pulled through `nextPassThroughOutput()` are timed 
by no metric. dd0f15465e9 added the drain timer to fix that. Without it, a 
near-distinct 10M-row partition that bypasses at the default `minRows = 100000` 
reports the build of about 1% of its rows in `time in aggregation build`, and 
the projection, `processRow` and `generateOutput` work for the rest goes 
uncounted. That is the workload this feature targets, and `docs/web-ui.md` 
describes the metric as "the time spent on aggregation".
   
   The point about downstream work is right: timing from the first to the last 
row also counts the shuffle write. Summing nanoseconds inside 
`nextPassThroughOutput()` only, and adding them to `aggTime` once when 
`passThroughHasNext` turns false, would avoid both that and the millisecond 
truncation, at the cost of two `System.nanoTime()` calls per row. Could you 
either take that trade-off, or keep the timer out, document that `aggTime` 
excludes pass-through (see item 7), and say in the description that this 
reverts dd0f15465e9 from #57742?



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/HashAggregateExec.scala:
##########
@@ -106,7 +106,8 @@ case class HashAggregateExec(
     val aggTime = longMetric("aggTime")
     val numTasksFallBacked = longMetric("numTasksFallBacked")
     // Registered only when the feature applies, and only read from the 
pass-through path.
-    val numBypassingRows = if (adaptivePartialAggEnabled) 
longMetric("numBypassingRows") else null
+    val numPassthroughRows =
+      if (adaptivePartialAggEnabled) longMetric("numPassthroughRows") else null

Review Comment:
   5. Minor: the description says the new name is what the implementation calls 
these rows, but the implementation spells it `PassThrough`: 
`nextPassThroughOutput`, `handlePassThroughRow`, 
`passThroughAggregationBuffer`, `passThroughHasNext` and 
`adaptivePassThroughTerm`. With `numPassthroughRows`, a case-sensitive search 
for `PassThrough` finds every other pass-through identifier but not this 
metric. Could you name it `numPassThroughRows` while it is still unreleased?



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/HashAggregateExec.scala:
##########
@@ -841,14 +841,14 @@ case class HashAggregateExec(
     } else {
       ""
     }
+    // The resumed build is not timed: split by an exchange, every 
passed-through row returns and

Review Comment:
   **2. In the fused shape, `aggTime` still includes every pass-through row and 
the `Final`'s work on them.** This comment, and the description's "`aggTime` 
again covers only the build up to the point where pass-through starts", hold 
only when an `Exchange` separates `Partial` and `Final`. When the child already 
satisfies the `Final`'s distribution (single-partition input, 
`df.repartition($"k").groupBy($"k")`, or a join or bucketed scan on the 
grouping keys), no `Exchange` is inserted and the two aggregates fuse. 
`outputFunc` then feeds the `Final`'s `doConsume`, `shouldStop()` never fires, 
and the first timed `doAgg` at L872-874 runs the whole partition: the 
frozen-map output, every pass-through row, and the `Final`'s hash-map updates 
for all of them. So the same query reports all of its pass-through time when 
fused and none when split or interpreted. Could you state this exception in the 
comment and the description, or record `aggTime` at the point where 
pass-through fires, so that
  it stops there in both shapes?



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/HashAggregateExec.scala:
##########
@@ -79,7 +79,7 @@ case class HashAggregateExec(
     // Only the aggregates that can actually bypass report this, so the rest 
do not show a
     // constant 0 in the SQL UI (see `UnionExec.metrics` for the same 
approach).
     if (adaptivePartialAggEnabled) {
-      Map("numBypassingRows" -> SQLMetrics.createMetric(sparkContext, "number 
of bypassing rows"))
+      Map("numPassthroughRows" -> SQLMetrics.createMetric(sparkContext, 
"passthrough output rows"))

Review Comment:
   7. Minor: this is about `docs/web-ui.md:382`, which is outside the diff. The 
SQL metrics table there still describes `time in aggregation build` as "the 
time spent on aggregation" for HashAggregate, and it does not list the 
pass-through metric at all. After this PR, a task whose work is mostly 
pass-through shows a small aggregation time next to an undocumented row count, 
which reads as a cheap aggregate. Could you add the metric to that table and 
say there (or in the Adaptive Partial Aggregation section of 
`sql-performance-tuning.md`) that pass-through rows are counted but not timed?



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/aggregate/AdaptivePartialAggregationSuite.scala:
##########
@@ -137,7 +137,7 @@ class AdaptivePartialAggregationSuite extends QueryTest 
with SharedSparkSession
           df.collect()
           val skipped = collect(df.queryExecution.executedPlan) {
             case agg: HashAggregateExec if isPartialPhase(agg) =>
-              agg.metrics.get("numBypassingRows").map(_.value).getOrElse(0L)
+              agg.metrics.get("numPassthroughRows").map(_.value).getOrElse(0L)

Review Comment:
   10. Nit: `agg.metrics.get("numPassthroughRows").map(_.value).getOrElse(0L)` 
and the surrounding `collect { case agg: HashAggregateExec if 
isPartialPhase(agg) => ... }.sum` are repeated at L140, L203, L224 and L505, 
and the key appears as a string literal five times in this suite and three 
times in `HashAggregateExec.scala`. That is why this rename had to touch eight 
literals, and a typo at any one of them would silently read 0 through 
`getOrElse(0L)`, which an `== 0` assertion accepts. Could you add one helper, 
e.g. `passThroughRows(df: DataFrame): Long`, and use it at those four sites?



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/HashAggregateExec.scala:
##########
@@ -841,14 +841,14 @@ case class HashAggregateExec(
     } else {
       ""
     }
+    // The resumed build is not timed: split by an exchange, every 
passed-through row returns and
+    // re-enters here, so a timer would cost two `System.nanoTime()` calls per 
row while the

Review Comment:
   8. Minor: "every passed-through row returns and re-enters here" does not 
hold for one-to-many children. As the `needStopCheck` comment at L887-894 says, 
`GenerateExec` does not check `shouldStop()` inside its fan-out, so one resumed 
`doAgg` passes through the whole batch produced from one input row. Such a 
slice can exceed a millisecond, so the removed timer did record some time 
there. Could you narrow the sentence to children that check `shouldStop()` 
between rows?



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/HashAggregateExec.scala:
##########
@@ -79,7 +79,7 @@ case class HashAggregateExec(
     // Only the aggregates that can actually bypass report this, so the rest 
do not show a
     // constant 0 in the SQL UI (see `UnionExec.metrics` for the same 
approach).
     if (adaptivePartialAggEnabled) {
-      Map("numBypassingRows" -> SQLMetrics.createMetric(sparkContext, "number 
of bypassing rows"))
+      Map("numPassthroughRows" -> SQLMetrics.createMetric(sparkContext, 
"passthrough output rows"))

Review Comment:
   4. Minor: "passthrough output rows" breaks the naming of the other count 
metrics. 73 of the 85 `createMetric(sparkContext, ...)` call sites in 
`sql/core` main code start with "number of", including this operator's "number 
of output rows" and "number of sort fallback tasks", and the similar "number of 
anchor output rows". It also spells "passthrough" as one word, while the config 
docs, `sql-performance-tuning.md` and the code comments say "pass-through". 
Since the feature is unreleased, could you use "number of pass-through output 
rows" (or "number of passed-through rows")?



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/HashAggregateExec.scala:
##########
@@ -841,14 +841,14 @@ case class HashAggregateExec(
     } else {
       ""
     }
+    // The resumed build is not timed: split by an exchange, every 
passed-through row returns and
+    // re-enters here, so a timer would cost two `System.nanoTime()` calls per 
row while the
+    // millisecond division truncates each sub-millisecond slice to 0.

Review Comment:
   **3. The removed timer recorded about nothing because of per-slice 
truncation, which is fixable.** Each resumed slice was divided by 
`NANOS_PER_MILLIS` on its own, so every sub-millisecond slice added 0, and 
`beforeResumedAgg` was close to a no-op since #57742. That is a resolution bug 
in the timer rather than a reason to stop timing the resumed build. Summing 
`System.nanoTime() - start` into a mutable `long` and adding it to `aggTime` 
once, when `childrenConsumed` is set, would time only the `doAgg` work; unlike 
the interpreted drain, no downstream work runs inside it. The two 
`System.nanoTime()` calls per re-entry would stay, so it is a trade-off. Could 
you compare it with this removal in the description, so that dropping the time 
is a stated choice rather than a side effect of the truncation?



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/HashAggregateExec.scala:
##########
@@ -79,7 +79,7 @@ case class HashAggregateExec(
     // Only the aggregates that can actually bypass report this, so the rest 
do not show a

Review Comment:
   9. Nit: the description argues that pass-through is a better name than 
bypass, but the rename stops at the key, so the same counter is still described 
as bypass or skip around it: this comment ("Only the aggregates that can 
actually bypass report this"), `adaptiveRowBypassedTerm` guarding 
`numPassthroughRows.add(1)` at L1319, and in the suite `AggCounters.skipped`, 
`bypassRowsByGroupingKeyCount`, the "bypassed rows" assertion messages and the 
test title "(independent of the skip metric)" at 
`AdaptivePartialAggregationSuite.scala:1384`. Could you rename at least this 
comment and the test-side names that describe this metric?



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/aggregate/AdaptivePartialAggregationSuite.scala:
##########
@@ -200,15 +200,15 @@ class AdaptivePartialAggregationSuite extends QueryTest 
with SharedSparkSession
     val counters = AggCounters(
       // The metric is only registered on aggregates the feature applies to; 
an aggregate without
       // it bypassed nothing.
-      skipped = 
partialAggs.map(_.metrics.get("numBypassingRows").map(_.value).getOrElse(0L)).sum,
+      skipped = 
partialAggs.map(_.metrics.get("numPassthroughRows").map(_.value).getOrElse(0L)).sum,
       partialOutputRows = 
partialAggs.map(_.metrics("numOutputRows").value).sum,
       spillBytes = partialAggs.map(_.metrics("spillSize").value).sum,
       tasksFallBacked = 
partialAggs.map(_.metrics("numTasksFallBacked").value).sum)
     checkAgainstReference(df, build)
     counters
   }
 
-  private def numBypassingRows(build: () => DataFrame): Long = 
runAndReadCounters(build).skipped
+  private def numPassthroughRows(build: () => DataFrame): Long = 
runAndReadCounters(build).skipped

Review Comment:
   6. Minor: no test pins what this PR changes. The suite only reads the 
renamed key, and the `aggTime` test in `SQLMetricsSuite.scala:357` runs with 
the feature off. So if a later change put a per-row timer back on the resumed 
build or in `nextPassThroughOutput()`, every test would still pass. Could you 
add a generated-code check, like the `copyFrom` test from #59758, that the 
generated code of a split adaptive aggregate calls `System.nanoTime()` only for 
the one build timer?



-- 
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]

Reply via email to