Skip to content

bench: keep the expect_plan check out of the measured region - #25441

Merged
adriangb merged 1 commit into
apache:mainfrom
pydantic:bench-expect-plan-cost
Sep 18, 2026
Merged

adriangb merged 1 commit into
apache:mainfrom
pydantic:bench-expect-plan-cost

Conversation

@adriangb

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

A benchmark file can assert a string in the physical plan with the expect_plan directive. That check ran on every iteration, inside the measured region, and it rendered the plan with {:#?}.

A derived Debug for an in-memory source prints every RecordBatch the source holds. Thus, for a benchmark whose tables come from CREATE TABLE ... AS SELECT, the check builds a multi-megabyte string per iteration and then searches it. For the null_aware_join suite, whose tables are built from range(), that string is 1.85 MB and the check is most of the measured time: the suite reports 17.6 ms for Q01, while datafusion-cli runs the same query in 4 ms.

This inflates absolute numbers and, more importantly, dilutes the A/B difference the benchmark exists to show.

What changes are included in this PR?

The check now renders the plan as EXPLAIN displays it, through DisplayableExecutionPlan. It also runs on the first iteration only, because a plan does not change between iterations.

null_aware_join, release build, both sides at the same commit (c4f5a9e0f2), median of 30 iterations:

Query before after
Q01 17.6 ms 4.6 ms
Q02 15.3 ms 2.6 ms
Q03 14.7 ms 2.1 ms
Q04 0.9 ms 0.4 ms
Q05 0.9 ms 0.4 ms
Q06 0.8 ms 0.3 ms
Q07 0.9 ms 0.3 ms
Q08 1.2 ms 0.5 ms

A failed check now also prints an 8-line plan instead of a 1.8 MB dump.

There are 96 expect_plan strings in the suites. 89 are operator names, which both forms print. The 6 null_aware_join strings that read null_aware: true become null_aware, which is how HashJoinExec displays the flag; this PR changes those 6 lines. The one remaining string, h2o's output_ordering=[pk@0 ASC NULLS LAST, ob@1 DESC], holds in the display form as well.

What is the testing strategy for this PR?

  • New unit test run_checks_expect_plan_once_per_benchmark: the first run checks the plan, and a later run does not repeat the check. The existing tests still cover the accept and reject paths of a first run.
  • I ran the suites that need no downloaded data, and all their expect_plan strings still hold: null_aware_join (8 queries), smj (26 queries, which include the 3 LeftMark checks), nlj (4 queries) and array_agg_distinct.
  • For the h2o output_ordering string, which needs data I do not have, I built an equivalent case: a Parquet table with WITH ORDER, a window query over it, and that expect_plan line. It passes.
  • Counterfactual check: with a string that is not in the plan, the first iteration still fails, and the message names the string.
  • cargo test -p datafusion-benchmarks --lib passes (143 tests). cargo fmt and clippy are clean.

Are there any user-facing changes?

No. This changes the benchmark harness and its documentation only.

🤖 Generated with Claude Code

`expect_plan` was checked on every iteration, inside the timed region,
and it rendered the plan with `{:#?}`. A derived `Debug` prints every
`RecordBatch` an in-memory source holds, so for a benchmark whose tables
come from `CREATE TABLE ... AS SELECT` the dump is megabytes per
iteration. In the `null_aware_join` suite this was most of the measured
time.

The check now renders the plan as `EXPLAIN` displays it, and runs on the
first iteration only, because a plan does not change between iterations.
A failure also prints an 8-line plan instead of a 1.8 MB dump.

`null_aware_join`, release, apache/main, median of 30 iterations:

| Query | before | after |
|-------|--------|-------|
| Q01   | 17.6 ms | 4.6 ms |
| Q02   | 15.3 ms | 2.6 ms |
| Q03   | 14.7 ms | 2.1 ms |
| Q04   | 0.9 ms  | 0.4 ms |
| Q05   | 0.9 ms  | 0.4 ms |
| Q06   | 0.8 ms  | 0.3 ms |
| Q07   | 0.9 ms  | 0.3 ms |
| Q08   | 1.2 ms  | 0.5 ms |

The display form spells the flag as `null_aware`, not `null_aware: true`,
so the six `null_aware_join` checks change with it. The other 89
`expect_plan` strings are operator names and are not affected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.33%. Comparing base (c4f5a9e) to head (ae18f6b).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #25441      +/-   ##
==========================================
- Coverage   82.33%   82.33%   -0.01%     
==========================================
  Files        1137     1137              
  Lines      432391   432530     +139     
  Branches   432391   432530     +139     
==========================================
+ Hits       356029   356141     +112     
- Misses      54845    54847       +2     
- Partials    21517    21542      +25     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@adriangb
adriangb requested review from Dandandan and alamb September 17, 2026 23:12

@alamb alamb 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 @adriangb -- FYI @Omega359

@adriangb
adriangb added this pull request to the merge queue Sep 18, 2026
@Omega359

Copy link
Copy Markdown
Contributor

Thanks @adriangb this is a nice improvement!

Merged via the queue into apache:main with commit 696dd18 Sep 18, 2026
41 checks passed
@adriangb
adriangb deleted the bench-expect-plan-cost branch September 18, 2026 21:05
haohuaijin pushed a commit to haohuaijin/arrow-datafusion that referenced this pull request Sep 19, 2026
…25441)

## Which issue does this PR close?

- N/A. This is a benchmark harness fix found while measuring
apache#25339.

## Rationale for this change

A benchmark file can assert a string in the physical plan with the
`expect_plan` directive. That check ran on every iteration, inside the
measured region, and it rendered the plan with `{:#?}`.

A derived `Debug` for an in-memory source prints every `RecordBatch` the
source holds. Thus, for a benchmark whose tables come from `CREATE TABLE
... AS SELECT`, the check builds a multi-megabyte string per iteration
and then searches it. For the `null_aware_join` suite, whose tables are
built from `range()`, that string is 1.85 MB and the check is most of
the measured time: the suite reports 17.6 ms for Q01, while
`datafusion-cli` runs the same query in 4 ms.

This inflates absolute numbers and, more importantly, dilutes the A/B
difference the benchmark exists to show.

## What changes are included in this PR?

The check now renders the plan as `EXPLAIN` displays it, through
`DisplayableExecutionPlan`. It also runs on the first iteration only,
because a plan does not change between iterations.

`null_aware_join`, release build, both sides at the same commit
(`c4f5a9e0f2`), median of 30 iterations:

| Query | before | after |
|---|---|---|
| Q01 | 17.6 ms | 4.6 ms |
| Q02 | 15.3 ms | 2.6 ms |
| Q03 | 14.7 ms | 2.1 ms |
| Q04 | 0.9 ms | 0.4 ms |
| Q05 | 0.9 ms | 0.4 ms |
| Q06 | 0.8 ms | 0.3 ms |
| Q07 | 0.9 ms | 0.3 ms |
| Q08 | 1.2 ms | 0.5 ms |

A failed check now also prints an 8-line plan instead of a 1.8 MB dump.

There are 96 `expect_plan` strings in the suites. 89 are operator names,
which both forms print. The 6 `null_aware_join` strings that read
`null_aware: true` become `null_aware`, which is how `HashJoinExec`
displays the flag; this PR changes those 6 lines. The one remaining
string, h2o's `output_ordering=[pk@0 ASC NULLS LAST, ob@1 DESC]`, holds
in the display form as well.

## What is the testing strategy for this PR?

- New unit test `run_checks_expect_plan_once_per_benchmark`: the first
run checks the plan, and a later run does not repeat the check. The
existing tests still cover the accept and reject paths of a first run.
- I ran the suites that need no downloaded data, and all their
`expect_plan` strings still hold: `null_aware_join` (8 queries), `smj`
(26 queries, which include the 3 `LeftMark` checks), `nlj` (4 queries)
and `array_agg_distinct`.
- For the h2o `output_ordering` string, which needs data I do not have,
I built an equivalent case: a Parquet table with `WITH ORDER`, a window
query over it, and that `expect_plan` line. It passes.
- Counterfactual check: with a string that is not in the plan, the first
iteration still fails, and the message names the string.
- `cargo test -p datafusion-benchmarks --lib` passes (143 tests). `cargo
fmt` and clippy are clean.

## Are there any user-facing changes?

No. This changes the benchmark harness and its documentation only.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

4 participants