Skip to content

feat: report native vs codegen-dispatch expression coverage in extended explain - #5201

Open
andygrove wants to merge 3 commits into
apache:mainfrom
andygrove:explain-expression-coverage
Open

feat: report native vs codegen-dispatch expression coverage in extended explain#5201
andygrove wants to merge 3 commits into
apache:mainfrom
andygrove:explain-expression-coverage

Conversation

@andygrove

Copy link
Copy Markdown
Member

Which issue does this PR close?

N/A

Rationale for this change

The extended explain summary reports operator coverage and the number of
Spark/Comet transitions, but says nothing about expressions. 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 JVM codegen dispatcher.

What changes are included in this PR?

Expression coverage is tracked on the expression tree with a new NATIVE_EXPRS
tag, the counterpart of the existing CODEGEN_DISPATCH_EXPRS, and both are
rolled up per operator onto the converted Comet plan node alongside the info
messages. The summary line now ends with the counts:

Comet accelerated 55 out of 58 eligible operators (94%). Final plan contains 3 transitions between Spark and Comet. Comet accelerated 14 expressions (14 native, 1 codegen dispatch).

Two new accessors on ExtendedExplainInfo give the same information as data:

val native: Seq[String] = info.getNativeExpressions(plan)
val dispatched: Seq[String] = info.getCodegenDispatchExpressions(plan)

Notes on the counting:

  • 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. cast in
    the TPC-DS goldens is a real example.
  • Structural nodes (attribute references, literals, aliases, bound references) are
    excluded. They appear in nearly every expression tree and would swamp the names
    that matter, and excluding literals also keeps the dispatcher's
    closure-serialized payload out of the native count.
  • Names are the expression's prettyName lowercased (the UDF name for a
    ScalaUDF), matching the existing [COMET-INFO: JVM codegen dispatcher: ...]
    naming.

Dispatch tagging is no longer gated on spark.comet.explain.codegen.enabled.
That config now controls only whether the [COMET-INFO: ...] segment is
rendered, so the counts are available by default. The config is now enabled in
CometPlanStabilitySuite so the goldens record which expressions took the
dispatcher path, not just how many — across TPC-DS that is cast and upper.

How are these changes tested?

Two new tests in CometCodegenSuite cover the split counts and both accessors
(including that they are independent of explain.codegen.enabled), and the
all-fallback case.

TPC-DS plan stability goldens were regenerated for Spark 3.4, 3.5, 4.0, 4.1 and
4.2. All 158 changed extended.txt files differ only in summary lines and
COMET-INFO segments — no plan shapes changed. The suites were re-run against
the regenerated goldens for 3.4, 3.5, 4.0 and the default profile.

…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.
@andygrove andygrove added this to the 1.0.0 milestone Aug 1, 2026

@comphead comphead left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @andygrove would it also address issue I came across recently here https://github.com/apache/datafusion-comet/pull/4870/changes#diff-9d04b74555b859929052456a8fd83506fd2154fa2a5dbf246ec4f1a04b807b11R40

So plan looks fully native however coverage is 97% and 1 transition

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.
@andygrove

Copy link
Copy Markdown
Member Author

Thanks @andygrove would it also address issue I came across recently here https://github.com/apache/datafusion-comet/pull/4870/changes#diff-9d04b74555b859929052456a8fd83506fd2154fa2a5dbf246ec4f1a04b807b11R40

So plan looks fully native however coverage is 97% and 1 transition

The root of that plan has a c2r, so that explains the transition and one of the reported fallbacks.

CometNativeColumnarToRow
+- CometTakeOrderedAndProject
   +- CometProject

There is also a ReusedSubquery and perhaps that is not correctly counted as native? I will take a look

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.
@comphead

comphead commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

There is also a ReusedSubquery and perhaps that is not correctly counted as native? I will take a look

For this specific file https://github.com/comphead/arrow-datafusion-comet/blob/d781362f4ce284d5e87997be385fd68d31860b30/spark/src/test/resources/tpcds-plan-stability/approved-plans-v1_4-spark3_5/q67/extended.txt

there is no ReuseQuery, plan is fully Cometed, but coverage 97%, most likely because of top C2R

@andygrove

Copy link
Copy Markdown
Member Author

There is also a ReusedSubquery and perhaps that is not correctly counted as native? I will take a look

For this specific file https://github.com/comphead/arrow-datafusion-comet/blob/d781362f4ce284d5e87997be385fd68d31860b30/spark/src/test/resources/tpcds-plan-stability/approved-plans-v1_4-spark3_5/q67/extended.txt

there is no ReuseQuery, plan is fully Cometed, but coverage 97%, most likely because of top C2R

yes, there is bug with counting comet reused subquery. I filed an issue #5203

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.

2 participants