This is an automated email from the ASF dual-hosted git repository.

gengliangwang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/master by this push:
     new 7629d9ff0295 [SPARK-57979][SQL] Extract the SortExec metric-recording 
epilogue into a shared helper
7629d9ff0295 is described below

commit 7629d9ff0295a8625dfccb9963b4d43b4e481793
Author: Gengliang Wang <[email protected]>
AuthorDate: Wed Jul 15 22:13:35 2026 -0700

    [SPARK-57979][SQL] Extract the SortExec metric-recording epilogue into a 
shared helper
    
    ### What changes were proposed in this pull request?
    
    `SortExec` records the same three sort metrics -- sort time, peak memory 
and spill size -- in two
    places, using identical logic:
    
    - the interpreted path in `SortExec.doExecute`, and
    - the generated code in `SortExec.doProduce`, which re-emits the four 
metric-update statements into
      every SortExec whole-stage-codegen stage.
    
    This PR extracts that type-independent metric bookkeeping into a shared 
helper
    `SortExec.recordSortMetrics(sorter, metrics, spillSizeBefore, sortTime, 
peakMemory, spillSize)` and
    calls it from both paths. The generated `if (needToSort) { ... }` block 
shrinks from the four inline
    metric statements to a single static call:
    
    Before (generated code, per SortExec stage):
    ```java
    if (sort_needToSort_0) {
      long sort_spillSizeBefore_0 = sort_metrics_0.memoryBytesSpilled();
      sort_addToSorter_0(partitionIndex);
      sort_sortedIter_0 = sort_sorter_0.sort();
      ((SQLMetric) references[3] /* sortTime 
*/).add(sort_sorter_0.getSortTimeNanos() / 1000000);
      ((SQLMetric) references[1] /* peakMemory 
*/).add(sort_sorter_0.getPeakMemoryUsage());
      ((SQLMetric) references[2] /* spillSize */).add(
        sort_metrics_0.memoryBytesSpilled() - sort_spillSizeBefore_0);
      sort_metrics_0.incPeakExecutionMemory(sort_sorter_0.getPeakMemoryUsage());
      sort_needToSort_0 = false;
    }
    ```
    
    After:
    ```java
    if (sort_needToSort_0) {
      long sort_spillSizeBefore_0 = sort_metrics_0.memoryBytesSpilled();
      sort_addToSorter_0(partitionIndex);
      sort_sortedIter_0 = sort_sorter_0.sort();
      org.apache.spark.sql.execution.SortExec.recordSortMetrics(
        sort_sorter_0, sort_metrics_0, sort_spillSizeBefore_0,
        ((SQLMetric) references[3] /* sortTime */),
        ((SQLMetric) references[1] /* peakMemory */),
        ((SQLMetric) references[2] /* spillSize */));
      sort_needToSort_0 = false;
    }
    ```
    
    This follows the same "extract type-independent generated machinery into a 
compiled helper" pattern
    as SPARK-57909 (`ColumnarToRowExec.advanceBatch`), and additionally 
de-duplicates the interpreted
    path, which had the same four lines.
    
    Measured with `WholeStageCodegenSizeBenchmark` (added under SPARK-57915; 
plans all 135 TPC-DS
    queries, 717 of the 2247 whole-stage-codegen stages are SortExec), current 
`master` vs. this change:
    
    | metric | master | this PR | delta |
    | --- | --- | --- | --- |
    | max method bytecode, summed over stages | 841,185 | 821,349 | -2.4% |
    | constant pool, summed over stages | 432,881 | 427,454 | -1.3% |
    | source code size (chars) | 21,846,001 | 21,763,355 | -0.4% |
    | inner classes | 258 | 258 | unchanged |
    | codegen fallbacks | 0 | 0 | unchanged |
    
    The reduction is in compiled bytecode and constant-pool entries, not only 
source text -- these are
    the metrics that gate the 64KB method / constant-pool limits and HotSpot's 
8KB JIT threshold.
    
    `spillSizeBefore` is still captured in the generated code before 
`addToSorter` runs (row insertion
    can spill), and passed into the helper, so the reported `spillSize` 
continues to reflect only this
    operator's contribution.
    
    ### Why are the changes needed?
    
    This is part of the umbrella SPARK-56908 (reduce the size of code generated 
by whole-stage codegen).
    Moving the type-independent metric bookkeeping out of the generated code 
shrinks every SortExec
    stage: the four metric statements (each a `references[]` cast plus a method 
call, so real bytecode
    and constant-pool method-references that Janino cannot fold away) collapse 
to one call, compiled once
    per JVM instead of re-emitted per stage. Planning all 135 TPC-DS queries 
produces 717 SortExec
    whole-stage-codegen stages, so the reduction applies broadly. It also 
removes a copy of the metric
    logic, so the two paths can no longer drift.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No. The generated code and the interpreted path compute exactly the same 
metrics as before; this is a
    pure code-organization change with no behavioral difference.
    
    ### How was this patch tested?
    
    Existing tests, run with whole-stage codegen both on and off:
    - `SortSuite` (the sort operator's correctness and metrics), and
    - the sort-related cases in `WholeStageCodegenSuite`.
    
    No behavior changes, so no new tests are added; the change is exercised by 
the existing SortExec
    coverage on both the interpreted and generated paths.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8)
    
    Closes #57058 from gengliangwang/SPARK-57979-sort-epilogue.
    
    Authored-by: Gengliang Wang <[email protected]>
    Signed-off-by: Gengliang Wang <[email protected]>
---
 .../org/apache/spark/sql/execution/SortExec.scala  | 38 ++++++++++++++++------
 1 file changed, 28 insertions(+), 10 deletions(-)

diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/SortExec.scala 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/SortExec.scala
index 351b3c009419..74afa396264e 100644
--- a/sql/core/src/main/scala/org/apache/spark/sql/execution/SortExec.scala
+++ b/sql/core/src/main/scala/org/apache/spark/sql/execution/SortExec.scala
@@ -26,8 +26,7 @@ import org.apache.spark.sql.catalyst.InternalRow
 import org.apache.spark.sql.catalyst.expressions._
 import org.apache.spark.sql.catalyst.expressions.codegen.{CodegenContext, 
CodeGenerator, ExprCode}
 import org.apache.spark.sql.catalyst.plans.physical._
-import org.apache.spark.sql.catalyst.util.DateTimeConstants.NANOS_PER_MILLIS
-import org.apache.spark.sql.execution.metric.SQLMetrics
+import org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics}
 
 /**
  * Performs (external) sorting.
@@ -120,10 +119,7 @@ case class SortExec(
       // figure out how many bytes we spilled for this operator.
       val spillSizeBefore = metrics.memoryBytesSpilled
       val sortedIterator = sorter.sort(iter.asInstanceOf[Iterator[UnsafeRow]])
-      sortTime += NANOSECONDS.toMillis(sorter.getSortTimeNanos)
-      peakMemory += sorter.getPeakMemoryUsage
-      spillSize += metrics.memoryBytesSpilled - spillSizeBefore
-      metrics.incPeakExecutionMemory(sorter.getPeakMemoryUsage)
+      SortExec.recordSortMetrics(sorter, metrics, spillSizeBefore, sortTime, 
peakMemory, spillSize)
 
       sortedIterator
     }
@@ -174,10 +170,8 @@ case class SortExec(
        |   long $spillSizeBefore = $metrics.memoryBytesSpilled();
        |   $addToSorterFuncName(partitionIndex);
        |   $sortedIterator = $sorterVariable.sort();
-       |   $sortTime.add($sorterVariable.getSortTimeNanos() / 
$NANOS_PER_MILLIS);
-       |   $peakMemory.add($sorterVariable.getPeakMemoryUsage());
-       |   $spillSize.add($metrics.memoryBytesSpilled() - $spillSizeBefore);
-       |   
$metrics.incPeakExecutionMemory($sorterVariable.getPeakMemoryUsage());
+       |   org.apache.spark.sql.execution.SortExec.recordSortMetrics(
+       |     $sorterVariable, $metrics, $spillSizeBefore, $sortTime, 
$peakMemory, $spillSize);
        |   $needToSort = false;
        | }
        |
@@ -212,3 +206,27 @@ case class SortExec(
   override protected def withNewChildInternal(newChild: SparkPlan): SortExec =
     copy(child = newChild)
 }
+
+object SortExec {
+  /**
+   * Records the sort metrics (sort time, peak memory and spill size) for a 
completed sort. This
+   * is called both by the interpreted path ([[SortExec.doExecute]]) and by 
the generated code of
+   * [[SortExec]] (through the static forwarder), so the type-independent 
metric bookkeeping is
+   * compiled once per JVM instead of being re-emitted into every SortExec 
stage's generated code.
+   *
+   * `spillSizeBefore` is the task's spilled-bytes count captured before the 
rows were fed to the
+   * sorter, so that `spillSize` reflects only this operator's contribution.
+   */
+  def recordSortMetrics(
+      sorter: UnsafeExternalRowSorter,
+      metrics: TaskMetrics,
+      spillSizeBefore: Long,
+      sortTime: SQLMetric,
+      peakMemory: SQLMetric,
+      spillSize: SQLMetric): Unit = {
+    sortTime += NANOSECONDS.toMillis(sorter.getSortTimeNanos)
+    peakMemory += sorter.getPeakMemoryUsage
+    spillSize += metrics.memoryBytesSpilled - spillSizeBefore
+    metrics.incPeakExecutionMemory(sorter.getPeakMemoryUsage)
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to