ulysses-you commented on code in PR #58591:
URL: https://github.com/apache/spark/pull/58591#discussion_r3956398899


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala:
##########
@@ -274,6 +307,34 @@ case class GroupPartitionsExec(
 
   @transient private lazy val hasCoalescing: Boolean = 
groupedPartitions.exists(_._2.size > 1)
 
+  // All values are computed on the driver by `grouping`, so they are reported 
through
+  // `sendDriverMetrics` rather than task-side accumulators. Registration 
reads constructor
+  // parameters only: pruning and replication are facts of the alignment path 
and are registered
+  // only in the modes that produce them, while coalescing can happen in any 
mode and reports 0
+  // when nothing merged.
+  @transient override lazy val metrics: Map[String, SQLMetric] = Map(
+    "numInputPartitions" -> SQLMetrics.createMetric(sparkContext, "number of 
input partitions"),
+    "numPartitions" -> SQLMetrics.createMetric(sparkContext, "number of 
partitions"),
+    "numEmptyPartitions" -> SQLMetrics.createMetric(sparkContext, "number of 
empty partitions"),
+    "numCoalescedPartitions" ->
+      SQLMetrics.createMetric(sparkContext, "number of coalesced partitions"),
+    "maxPartitionsPerGroup" ->
+      SQLMetrics.createMetric(sparkContext, "max partitions per group")) ++ {
+    if (expectedPartitionKeys.isDefined && !distributePartitions) {
+      Map("numReplicatedPartitions" -> SQLMetrics.createMetric(sparkContext,
+        "number of replicated input partition reads"))

Review Comment:
   Fixed in a9a920e489a: narrowed the registration to 
`expectedPartitionKeys.exists(_.exists(_._2 > 1)) && !distributePartitions` and 
renamed the key to `numReplicatedPartitionReads`.
   
   @peter-toth your single-source-of-truth follow-up applied as well: `set` is 
now `metrics.get(name).foreach` and both `if`s in `sendDriverMetrics` are gone, 
so the predicates cannot drift apart. A mistyped name fails the tests, which 
assert every metric by value or absence.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala:
##########
@@ -274,6 +307,34 @@ case class GroupPartitionsExec(
 
   @transient private lazy val hasCoalescing: Boolean = 
groupedPartitions.exists(_._2.size > 1)
 
+  // All values are computed on the driver by `grouping`, so they are reported 
through
+  // `sendDriverMetrics` rather than task-side accumulators. Registration 
reads constructor
+  // parameters only: pruning and replication are facts of the alignment path 
and are registered
+  // only in the modes that produce them, while coalescing can happen in any 
mode and reports 0
+  // when nothing merged.
+  @transient override lazy val metrics: Map[String, SQLMetric] = Map(
+    "numInputPartitions" -> SQLMetrics.createMetric(sparkContext, "number of 
input partitions"),
+    "numPartitions" -> SQLMetrics.createMetric(sparkContext, "number of 
partitions"),
+    "numEmptyPartitions" -> SQLMetrics.createMetric(sparkContext, "number of 
empty partitions"),
+    "numCoalescedPartitions" ->
+      SQLMetrics.createMetric(sparkContext, "number of coalesced partitions"),
+    "maxPartitionsPerGroup" ->
+      SQLMetrics.createMetric(sparkContext, "max partitions per group")) ++ {
+    if (expectedPartitionKeys.isDefined && !distributePartitions) {
+      Map("numReplicatedPartitions" -> SQLMetrics.createMetric(sparkContext,
+        "number of replicated input partition reads"))
+    } else {
+      Map.empty[String, SQLMetric]
+    }
+  } ++ {
+    if (expectedPartitionKeys.isDefined) {

Review Comment:
   Softened the comment in a9a920e489a: pruning is registered over the whole 
alignment path; the ordering producer expects the child's own keys and so never 
prunes, but no constructor parameter separates it from the join producers that 
can.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala:
##########
@@ -274,6 +307,34 @@ case class GroupPartitionsExec(
 
   @transient private lazy val hasCoalescing: Boolean = 
groupedPartitions.exists(_._2.size > 1)
 
+  // All values are computed on the driver by `grouping`, so they are reported 
through
+  // `sendDriverMetrics` rather than task-side accumulators. Registration 
reads constructor
+  // parameters only: pruning and replication are facts of the alignment path 
and are registered
+  // only in the modes that produce them, while coalescing can happen in any 
mode and reports 0
+  // when nothing merged.
+  @transient override lazy val metrics: Map[String, SQLMetric] = Map(
+    "numInputPartitions" -> SQLMetrics.createMetric(sparkContext, "number of 
input partitions"),
+    "numPartitions" -> SQLMetrics.createMetric(sparkContext, "number of 
partitions"),
+    "numEmptyPartitions" -> SQLMetrics.createMetric(sparkContext, "number of 
empty partitions"),
+    "numCoalescedPartitions" ->
+      SQLMetrics.createMetric(sparkContext, "number of coalesced partitions"),
+    "maxPartitionsPerGroup" ->
+      SQLMetrics.createMetric(sparkContext, "max partitions per group")) ++ {

Review Comment:
   This only matters for a repeated post, which cannot happen as peter-toth 
measured: `SparkPlan.execute()` memoizes `doExecute` through a `LazyTry` 
(SparkPlan.scala:185-201), so each value is posted exactly once and the SUM 
aggregation sees a single update per metric. a9a920e489a notes the memoization 
in a comment.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala:
##########
@@ -359,7 +420,47 @@ case class GroupPartitionsExec(
   private[v2] def kWayMergeOrdering: Seq[SortOrder] =
     child.outputOrdering.map(_.copy(sameOrderExpressions = Seq.empty))
 
+  private def sendDriverMetrics(): Unit = {
+    val executionId = 
sparkContext.getLocalProperty(SQLExecution.EXECUTION_ID_KEY)
+    val driverAccumUpdates = ArrayBuffer.empty[(Long, Long)]
+    def set(name: String, value: Long): Unit = {
+      val metric = metrics(name)
+      metric.set(value)
+      driverAccumUpdates += (metric.id -> value)
+    }
+    // A single pass for the three per-group counts; an empty group and a 
coalesced one are
+    // mutually exclusive.
+    var numEmptyPartitions = 0
+    var numCoalescedPartitions = 0
+    var maxPartitionsPerGroup = 0
+    groupedPartitions.foreach { case (_, group) =>
+      val size = group.size
+      if (size == 0) {
+        numEmptyPartitions += 1
+      } else if (size > 1) {
+        numCoalescedPartitions += 1
+      }
+      if (size > maxPartitionsPerGroup) {
+        maxPartitionsPerGroup = size
+      }
+    }
+    set("numInputPartitions", grouping.numInputPartitions)
+    set("numPartitions", groupedPartitions.size)
+    set("numEmptyPartitions", numEmptyPartitions)
+    set("numCoalescedPartitions", numCoalescedPartitions)
+    set("maxPartitionsPerGroup", maxPartitionsPerGroup)
+    if (expectedPartitionKeys.isDefined && !distributePartitions) {
+      set("numReplicatedPartitions", grouping.numReplicatedPartitions)
+    }
+    if (expectedPartitionKeys.isDefined) {
+      set("numPrunedPartitions", grouping.numPrunedPartitions)
+    }
+    SQLMetrics.postDriverMetricsUpdatedByValue(
+      sparkContext, executionId, driverAccumUpdates.toSeq)
+  }
+
   override protected def doExecute(): RDD[InternalRow] = {
+    sendDriverMetrics()

Review Comment:
   Confirmed against SparkPlan.scala:185-201: `execute()` is `executeRDD.get` 
over `LazyTry { doExecute() }`, so `doExecute` runs at most once per instance 
and an extra guard would memoize something already memoized. Thanks @peter-toth 
for the mechanism and the measurement. a9a920e489a keeps the direct call with a 
comment noting the memoization.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala:
##########
@@ -380,6 +481,7 @@ case class GroupPartitionsExec(
     child.supportsColumnar && !(hasCoalescing && enableSortedMerge && 
canUseSortedMerge)
 
   override protected def doExecuteColumnar(): RDD[ColumnarBatch] = {
+    sendDriverMetrics()

Review Comment:
   Same resolution as the thread above: `executeColumnar()` memoizes 
`doExecuteColumnar` through `executeColumnarRDD` (SparkPlan.scala:221-236), so 
both entry points post at most once without an extra guard (a9a920e489a).
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala:
##########
@@ -450,12 +552,20 @@ case class GroupPartitionsExec(
   }
 }
 
-/** What a [[GroupPartitionsExec]] computes once and reports from several 
members. */
+/**
+ * What a [[GroupPartitionsExec]] computes once and reports from several 
members.
+ * `numInputPartitions` is the child's split count; the last two fields count 
the alignment's
+ * effect on the reads of those splits (see `alignToExpectedKeys`), and are 0 
outside the
+ * alignment path.
+ */
 private case class PartitionGrouping(
     partitions: Seq[(InternalRowComparableWrapper, Seq[Int])],
     isGrouped: Boolean,
     isCollapsed: Boolean,
-    keysRewritten: Boolean)
+    keysRewritten: Boolean,
+    numInputPartitions: Int,

Review Comment:
   Removed in a9a920e489a: `sendDriverMetrics` reads 
`child.outputPartitioning.numPartitions`; `alignToExpectedKeys` keeps the 
parameter.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala:
##########
@@ -149,22 +150,52 @@ case class GroupPartitionsExec(
     })
   }
 
-  /** Aligns partitions based on `expectedPartitionKeys` and clustering mode. 
*/
-  private def alignToExpectedKeys(keyMap: Map[InternalRowComparableWrapper, 
Seq[Int]]) = {
+  /**
+   * Aligns partitions based on `expectedPartitionKeys` and clustering mode.
+   *
+   * Returns the aligned groups, whether their keys ended up unique, and two 
counts, both in
+   * input partition reads relative to the baseline of every input being read 
exactly once.
+   * The pruned count is the reads that never happen: inputs of a key the 
alignment never
+   * references. The join proved such keys cannot produce output, e.g. an 
inner join keeps
+   * only the intersection. Which keys are expected is orthogonal to the mode: 
an
+   * `OrderedDistribution` producer expects the child's own keys and so never 
prunes, while a
+   * join producer can prune in either mode. The replicated count is the reads 
that happen
+   * again: in the replicate mode of partial clustering every expected 
partition of a held key
+   * re-reads all its splits, counted beyond the first pass (3 splits over 2 
slots count 3).
+   * Total input reads = `numInputPartitions` - pruned + replicated.
+   */
+  private def alignToExpectedKeys(
+      keyMap: Map[InternalRowComparableWrapper, Seq[Int]],
+      numInputPartitions: Int) = {
     var isGrouped = true
+    var numReplicatedPartitions = 0
+    // Splits the alignment references, accumulated over the expected keys. 
Every producer of
+    // `expectedPartitionKeys` deduplicates the keys, so no split is counted 
twice here.
+    var numMatchedPartitions = 0
     val alignedPartitions = expectedPartitionKeys.get.flatMap { case (key, 
numSplits) =>
+      // Every producer derives the split counts from a `groupBy` size or the 
literal 1; a
+      // non-positive count would emit a different partition count for the key 
than the other
+      // side expects, breaking the pairing this alignment exists for.
+      assert(numSplits > 0, s"expected partition key split count must be 
positive: $numSplits")

Review Comment:
   This assert is sort of required by this change itself: `numSplits <= 0` 
would make the new replicated-reads product `(numSplits - 1) * splits.size` 
negative, so it guards the derivation added here, not only the pre-existing 
planner contract. If you don't mind, I'd prefer to keep it in this PR.
   



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExecSuite.scala:
##########
@@ -18,17 +18,22 @@
 package org.apache.spark.sql.execution.datasources.v2
 
 import org.apache.spark.rdd.RDD
+import org.apache.spark.sql.Row
 import org.apache.spark.sql.catalyst.InternalRow
 import org.apache.spark.sql.catalyst.expressions.{Ascending, Attribute, 
AttributeReference, SortOrder, TransformExpression}
 import org.apache.spark.sql.catalyst.plans.physical.{ClusteredDistribution, 
KeyedPartitioning, KeyedShuffleSpec, KeyReducer, Partitioning, 
PartitioningCollection, UnknownPartitioning}
 import org.apache.spark.sql.catalyst.util.InternalRowComparableWrapper
+import org.apache.spark.sql.connector.KeyGroupedPartitioningSuiteBase
 import org.apache.spark.sql.connector.catalog.functions.{BucketFunction, 
BucketReducer, Reducer}
+import org.apache.spark.sql.connector.expressions.Expressions.identity
 import org.apache.spark.sql.execution.{DummySparkPlan, LeafExecNode, 
SafeForKWayMerge}
+import org.apache.spark.sql.execution.metric.SQLMetricsTestUtils
+import org.apache.spark.sql.execution.ui.SparkPlanGraphNode
 import org.apache.spark.sql.internal.SQLConf
-import org.apache.spark.sql.test.SharedSparkSession
 import org.apache.spark.sql.types.{DataType, IntegerType}
 
-class GroupPartitionsExecSuite extends SharedSparkSession {
+class GroupPartitionsExecSuite
+  extends KeyGroupedPartitioningSuiteBase with SQLMetricsTestUtils {

Review Comment:
   Moved the three e2e tests to `KeyGroupedPartitioningSuite` (carries 
`@ExtendedSQLTest`); `GroupPartitionsExecSuite` is back to `SharedSparkSession` 
with unit tests only (a9a920e489a).
   



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