Skip to content

fix: correct extended explain operator counts for reuse wrappers and CometSubqueryBroadcast - #5206

Open
andygrove wants to merge 4 commits into
apache:mainfrom
andygrove:explain-operator-count-fixes
Open

fix: correct extended explain operator counts for reuse wrappers and CometSubqueryBroadcast#5206
andygrove wants to merge 4 commits into
apache:mainfrom
andygrove:explain-operator-count-fixes

Conversation

@andygrove

Copy link
Copy Markdown
Member

Which issue does this PR close?

Closes #5203.

Stacked on #5201 (explain-expression-coverage) because that PR also regenerates every
TPC-DS extended.txt golden. Review that one first; the base will be retargeted to main
once it merges.

Rationale for this change

The operator coverage summary produced by ExtendedExplainInfo counted three node kinds
incorrectly, so it understated acceleration on most TPC-DS plans and massively overcounted
operators wherever an exchange was reused. The same counts feed the comet metrics source
via CometMetricsListener -> CometSource, so the skew also biased the
acceleration.ratio gauge.

What changes are included in this PR?

All three items from the issue:

  1. ReusedSubqueryExec is no longer counted as an un-accelerated Spark operator. It is
    pure reuse bookkeeping and a LeafExecNode, so it now joins the ignore list.
  2. CometSubqueryBroadcastExec now mixes in CometPlan and counts as accelerated. The
    other CometPlan match sites (CometExecRule, EliminateRedundantTransitions,
    foreachUntilCometInput, CometShuffleExchangeExec.isCometPlan) all reach nodes through
    children, and a BaseSubqueryExec hangs off innerChildren/expressions, so none of
    them change behaviour. The one that would is CometExecRule's "never replace" guard,
    which is the correct classification anyway.
  3. Reused exchanges are counted once. getActualPlan unwrapped ReusedExchangeExec to
    its child, so the reused subtree was traversed and counted in full at every reference. The
    coverage traversal now uses a new CometExplainInfo.actualPlanForCoverage, which is
    getActualPlan minus that unwrapping. Because ReusedExchangeExec is a LeafExecNode,
    keeping the wrapper renders a ReusedExchange leaf and stops the walk, matching how
    ReusedSubqueryExec is handled and keeping the rendered tree consistent with the counts
    below it. getActualPlan itself is unchanged, so fallback-reason collection still
    descends into reused subtrees.

Effect on the goldens: 143 extended.txt files change, and 107 of them now report 100%
acceleration where only 6 did before. q1 is the example from the issue:

-         :     :     :                 +- CometBroadcastExchange
-         :     :     :                    +- CometProject
-         :     :     :                       +- CometFilter
-         :     :     :                          +- CometNativeScan parquet spark_catalog.default.date_dim
+         :     :     :                 +- ReusedExchange
...
-Comet accelerated 47 out of 49 eligible operators (95%). ...
+Comet accelerated 40 out of 40 eligible operators (100%). ...

Eight plans report a lower percentage than before, which is the double counting being
removed: the reused subtrees were fully accelerated, so counting them once per reference
inflated the ratio. The clearest case is approved-plans-v2_7/q14a, which went from
2167 out of 2302 to 127 out of 128 — that plan does not contain 2302 operators.

Also documents in understanding-comet-plans.md which nodes are excluded from the counts.

How are these changes tested?

New CometCoverageStatsSuite covers each of the three cases directly: a ReusedSubquery
contributes nothing, a ReusedExchange contributes nothing at the reference site while the
exchange it points at is still counted where it is defined, and wrapping a plan in
CometSubqueryBroadcastExec adds exactly one accelerated operator.

TPC-DS plan stability goldens were regenerated for Spark 3.4, 3.5, 4.0, 4.1 and 4.2, and both
suites were then re-run in verification mode against the regenerated goldens on all five
versions (129 tests each, all passing). CometExecSuite, CometCodegenSuite and the new
suite also pass on the default profile (224 tests).

…ed explain

The extended explain summary reported operator coverage and transition count
but said nothing about expressions, so there was no way to see how much of a
plan's expression evaluation ran in native DataFusion kernels versus Spark's
own generated code inside the codegen dispatcher.

Track both paths on the expression tree with a new `NATIVE_EXPRS` tag, the
counterpart of the existing `CODEGEN_DISPATCH_EXPRS`, and roll them up per
operator onto the converted Comet plan node alongside the info messages. The
summary line now ends with, for example:

  Comet accelerated 14 expressions (14 native, 1 codegen dispatch).

Counts are distinct expression names per plan. A name can appear in both
buckets when the same function is lowered natively for one set of arguments
and dispatched for another, so the total is the union rather than the sum.
Structural nodes (attribute references, literals, aliases, bound references)
are excluded; they appear in nearly every tree and would swamp the names that
matter, and excluding literals also keeps the dispatcher's closure-serialized
payload out of the native count.

Add `ExtendedExplainInfo.getNativeExpressions` and
`getCodegenDispatchExpressions` for programmatic access to the two name lists.

Dispatch tagging is no longer gated on `spark.comet.explain.codegen.enabled`;
that config now controls only whether the `[COMET-INFO: JVM codegen
dispatcher: ...]` segment is rendered, so the counts are available by default.

Enable that config in `CometPlanStabilitySuite` so the TPC-DS goldens record
which expressions took the dispatcher path, not just how many. Golden files
regenerated for Spark 3.4, 3.5, 4.0, 4.1 and 4.2; the only changes are summary
lines and `COMET-INFO` segments.
Quality pass over the previous commit, no behavior change:

- Collapse the three byte-identical "append a name to a Set-valued tag" bodies
  (`withInfo`, `withCodegenDispatchExpr`, `withNativeExpr`) onto one private
  `appendTagValue`, with a bulk `appendTagValues` for lifting a whole name set.
- Add `CometExplainInfo.collectTagValues` and use it from both roll-up sites in
  `CometExecRule` (including the pre-existing `EXTENSION_INFO` line) and from
  `liftCoverageTags`, replacing three spellings of the same union.
- `rollUpInfoMessages` now writes the coverage tags through `appendTagValues`
  rather than raw `setTagValue`, so there is one way to write these tags.
- `liftCoverageTags` walks the promoted tree once instead of twice, and drops
  the identity `collect { case e: Expression => e }`.
- Classify native-vs-dispatched from a dedicated `DISPATCHED_SELF` marker
  instead of sniffing `CODEGEN_DISPATCH_EXPRS`. That tag accumulates descendant
  names during roll-up and lifting, so reading it as "was I dispatched" made the
  classification depend on planner visit order.
- Route the two accessors through the existing `sortup` traversal, as
  `fallbackReasons` already does, so they no longer render and discard a full
  plan string just to read tags off nodes.
- Drop two no-op configs from the new test, fix an inaccurate comment about the
  dispatcher's closure payload (that `Literal` is built locally and is never
  reachable from `op.expressions`), and trim a comment that restated its code.
The summary read

  Comet accelerated 13 expressions (13 native, 1 codegen dispatch).

The total was the union of the two name sets rather than their sum, because the
same function can be lowered natively for one set of arguments and dispatched
for another, so a name can be in both. Correct by design, but it presents as an
arithmetic error to anyone reading the line. It now reads

  Accelerated expressions: 13 native, 1 codegen dispatch.

which reports the same two counts, has no total to misread, and is grammatical
at any count. Also drops the merged-set allocation the total needed.

Golden files regenerated for Spark 3.4, 3.5, 4.0, 4.1 and 4.2; the only changed
lines are the summary.
…CometSubqueryBroadcast

`ExtendedExplainInfo`'s coverage summary miscounted three kinds of node:

- `ReusedSubqueryExec` fell through to the Spark bucket, adding a phantom
  un-accelerated operator at every reuse site.
- `CometSubqueryBroadcastExec` did not mix in `CometPlan`, so Comet's own DPP
  operator was also counted as a Spark operator.
- `ReusedExchangeExec` was both ignored by the match and unwrapped by
  `getActualPlan`, so the reused subtree was traversed and counted in full at
  every reference. On q14a that inflated the eligible count from 128 to 2302.

Both reuse markers are now ignored and rendered as leaves, so the operators
they reference are counted once, where the plan they point at is shown.
`getActualPlan` keeps its existing unwrapping behaviour for fallback-reason
collection; the coverage traversal uses the new `actualPlanForCoverage`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extended explain operator stats miscount reuse wrappers and CometSubqueryBroadcast

1 participant