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

github-actions[bot] pushed a commit to branch asf-site
in repository https://gitbox.apache.org/repos/asf/datafusion-ballista.git


The following commit(s) were added to refs/heads/asf-site by this push:
     new 5c05df167 Publish built docs triggered by 
d738ca9bd6b86aecaf9587af9025ae0dd12c5af1
5c05df167 is described below

commit 5c05df16764ad823b765777a9360b2cce31c90bb
Author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Jul 23 22:26:23 2026 +0000

    Publish built docs triggered by d738ca9bd6b86aecaf9587af9025ae0dd12c5af1
---
 _sources/contributors-guide/benchmarking.md.txt | 476 +++++------------
 contributors-guide/benchmarking.html            | 661 ++++++++----------------
 searchindex.js                                  |   2 +-
 3 files changed, 361 insertions(+), 778 deletions(-)

diff --git a/_sources/contributors-guide/benchmarking.md.txt 
b/_sources/contributors-guide/benchmarking.md.txt
index 94b608623..fb3b911bf 100644
--- a/_sources/contributors-guide/benchmarking.md.txt
+++ b/_sources/contributors-guide/benchmarking.md.txt
@@ -19,384 +19,176 @@
 
 # Benchmarking
 
-This page describes how Ballista is benchmarked at scale, and records the 
current
-results. It is aimed at contributors who want to reproduce a number, 
understand why
-a comparison is set up the way it is, or add a result of their own.
+Current TPC-H **SF1000** results for Ballista, compared against a vanilla
+**Spark 3.4** baseline running on the same cluster shape.
 
-The benchmark used here is derived from TPC-H. For running TPC-H locally at 
small
-scale factors, see [`benchmarks/README.md`][bench-readme] in the repository; 
this
-page covers the multi-node, large-scale-factor setup and the cross-engine
-comparisons.
+## Versions under test
 
-[bench-readme]: 
https://github.com/apache/datafusion-ballista/blob/main/benchmarks/README.md
-
-## What we measure, and why
-
-Ballista's central performance question is **how evenly work is spread across
-executor tasks**. A distributed query is only as fast as its slowest stage, 
and a
-stage is only as fast as its slowest task, so a query whose work concentrates 
onto
-a few partitions will underperform no matter how fast the underlying operators 
are.
-Benchmarks are therefore run at a scale where that imbalance actually shows up.
-
-These benchmarks are run with **adaptive query execution (AQE) enabled**
-(`ballista.planner.adaptive.enabled=true`). This is the configuration Ballista 
is
-being actively developed against, so it is the one measured here.
-
-AQE selects the adaptive planner, which can re-plan stages using runtime 
statistics —
-coalescing partitions, re-optimising joins, and promoting a small join side to 
a
-broadcast at runtime. The alternative, AQE off, selects the static
-`DefaultDistributedPlanner`; it is a different planner with materially 
different join
-behaviour, and it is no longer benchmarked on this page.
+| Engine   | Version                                                           
                                                                                
                        |
+| -------- | 
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 |
+| Ballista | 
[`696ca29b`](https://github.com/apache/datafusion-ballista/commit/696ca29b3c1fe3013238822b7f5d8cdb918fdaa3)
 (`main`, 2026-07-23), Cargo pkg `54.0.0`, DataFusion `54.1.0` |
+| Spark    | 3.4 (vanilla, no acceleration plugin)                             
                                                                                
                        |
 
 ## Environment
 
-The results on this page currently come from a **small homelab cluster**: two
-bare-metal nodes running Kubernetes, with the TPC-H data staged on node-local 
disk.
-This is a deliberate starting point rather than a final destination. A two-node
-cluster with local disk removes as many confounds as possible — no object-store
-latency, no cloud network variance, no noisy neighbours — so that when a query 
is
-slow, the cause is Ballista and not the environment.
-
-The intent is to **move these benchmarks to AWS with data in S3** once the 
results
-here are good. That environment is the one users actually run, and it exercises
-things a homelab cannot: object-store reads instead of local disk, higher and 
more
-variable network latency between executors, and larger executor counts. Some of
-Ballista's behaviour is expected to change there — a shuffle that is cheap 
over a
-local link is not cheap over a cloud network, and object-store reads make scan
-parallelism matter differently.
-
-So results on this page should be read as **relative comparisons on controlled
-hardware**, useful for "did this change help", not as absolute throughput 
numbers
-for a cloud deployment.
-
-## Reference cluster
-
-All results on this page use the following shape. It is a reference point, not 
a
-requirement — the commands below work on any cluster, but numbers are only
-comparable when the shape matches.
-
-|                     |                                             |
-| ------------------- | ------------------------------------------- |
-| Executors           | 2, one per physical node                    |
-| Per executor        | 16 cores, 56 GiB, `--memory-pool-size=48GB` |
-| Per task slot       | 16 concurrent tasks → 3 GB pool each        |
-| Scheduler           | 1                                           |
-| Data                | TPC-H SF1000 Parquet, node-local disk       |
-| `target_partitions` | 64                                          |
-
-Two details matter more than they look:
-
-- **Executors are spread one per node.** Packing two executors onto one node 
makes
-  them contend for the same disk and memory bandwidth, which is measuring the 
host,
-  not the engine.
-- **Data is on node-local disk, not object storage.** With a modest network 
between
-  nodes, reading from object storage makes the interconnect the bottleneck and 
the
-  engine comparison becomes an I/O comparison.
-
-The memory pool is deliberately set below the container limit. Ballista splits
-`--memory-pool-size` into one `FairSpillPool` per task slot, and that 
accounting
-does not cover every allocation, so leaving headroom between the pool and the
-container limit avoids the container being killed outright instead of 
reporting a
-graceful resource error.
+- **Cluster:** Kubernetes on AWS (`us-west-2`); one driver/scheduler pod and
+  32 executor pods for each engine, launched on the same node pool.
+- **Executor pod (Ballista):** x86_64, 8 vCPU, 64 GiB memory.
+- **Executor pod (Spark):** x86_64, 8 vCPU, 64 GiB + 10 GiB overhead.
+- **Client pod (Ballista):** the Python benchmark runner submits SQL to
+  Ballista via `BallistaSessionContext` and collects results locally.
+  Requires 64 GiB memory limit to complete the full 22-query suite —
+  smaller limits (16 GiB) OOM the client partway through, even though the
+  executor cluster is healthy.
+- **Data:** TPC-H SF1000 Parquet on S3 (`us-west-2`), ZSTD compression,
+  ~512 MiB row groups, one directory per table.
+
+## Ballista configuration
+
+| Flag / config key                                             | Value        
 |
+| ------------------------------------------------------------- | 
------------- |
+| `--concurrent-tasks`                                          | `8`          
 |
+| `--memory-pool-size` (bytes; ≈70 % of the 64 GiB container)   | 
`48103633715` |
+| `datafusion.execution.target_partitions`                      | `256`        
 |
+| `datafusion.execution.collect_statistics`                     | `true`       
 |
+| `datafusion.execution.listing_table_factory_infer_partitions` | `false`      
 |
+| `datafusion.catalog.information_schema`                       | `true`       
 |
+| `ballista.planner.adaptive.enabled`                           | `true` (AQE) 
 |
+| `ballista.shuffle.sort_based.memory_limit_per_task_bytes`     | `0`          
 |
+
+`datafusion.optimizer.prefer_hash_join` is left at its default; under AQE the
+join strategy is selected at runtime by `DelayJoinSelectionRule` /
+`DynamicJoinSelectionExec` from runtime statistics and the broadcast /
+`ballista.optimizer.hash_join_max_build_partition_bytes` thresholds.
+
+## Spark configuration (highlights)
+
+Vanilla Spark 3.4 — no Comet plugin, stock `SortShuffleManager`.
+
+| Key                                                        | Value           
                     |
+| ---------------------------------------------------------- | 
------------------------------------ |
+| `spark.executor.instances`                                 | `32`            
                     |
+| `spark.executor.cores`                                     | `16` (task 
parallelism per executor) |
+| `spark.kubernetes.executor.limit.cores` / `.request.cores` | `8` (physical 
vCPU)                  |
+| `spark.executor.memory`                                    | `64G`           
                     |
+| `spark.executor.memoryOverhead`                            | `10G`           
                     |
+| `spark.memory.fraction`                                    | `0.6`           
                     |
+| `spark.memory.storageFraction`                             | `0.2`           
                     |
+| `spark.sql.shuffle.partitions`                             | `512`           
                     |
+| `spark.sql.broadcastTimeout`                               | `900`           
                     |
+| `spark.serializer`                                         | 
`KryoSerializer`                     |
+| `spark.io.compression.codec`                               | `zstd`          
                     |
+
+Spark AQE is left at its Spark 3.4 defaults. Shuffle spills to a `gp3`-backed
+per-executor volume (`spark.kubernetes.executor.volumes...spark-local-dir-1`).
+
+Note that `spark.executor.cores=16` is Spark's **task parallelism** setting,
+not a CPU allocation — each executor pod is given only **8 physical vCPU**
+via `spark.kubernetes.executor.limit.cores` / `.request.cores`, so Spark
+schedules 16 concurrent tasks onto 8 physical cores (2× oversubscription).
+The matching Ballista executor runs `--concurrent-tasks=8` on the same
+8 physical vCPU (1:1).
 
 ## Queries
 
-All engines run the **same SQL**, which is what makes a cross-engine comparison
-apples-to-apples.
+The SQLBench-H phrasing of the 22 TPC-H queries from
+[apache/datafusion-benchmarks](https://github.com/apache/datafusion-benchmarks).
 
-The shared set is the TPC-H queries from [apache/datafusion-benchmarks][dfb]
-(SQLBench-H). Ballista's bundled queries in `benchmarks/queries/` are the 
classic
-TPC-H phrasing and differ in places, so the shared set is overlaid over them 
for a
-comparison run.
-
-Spark and Comet are driven through Comet's benchmark harness, which reads its 
own
-bundled copy of the queries (labelled CometBench-H). That copy is textually
-identical to the SQLBench-H set — all 22 queries match once the licence header
-comment is ignored — so the engines are executing the same statements even 
though
-they load them from different paths. Worth re-checking if either set is ever
-regenerated.
-
-[dfb]: https://github.com/apache/datafusion-benchmarks
+## Results
 
-## Running the benchmark
+**Times in seconds; lower is better.** Ballista: single iteration. Spark:
+mean of 3 iterations (cold iteration dropped by the harness).
+
+|              Query | Ballista (s) | Spark 3.4 (s) |
+| -----------------: | -----------: | ------------: |
+|                  1 |        21.16 |         67.58 |
+|                  2 |        32.37 |         29.80 |
+|                  3 |        32.69 |         25.13 |
+|                  4 |        27.75 |         21.19 |
+|                  5 |        49.37 |         54.12 |
+|                  6 |        16.17 |          1.23 |
+|                  7 |        48.24 |         19.57 |
+|                  8 |       189.30 |         48.60 |
+|                  9 |       154.88 |         69.38 |
+|                 10 |        67.72 |         35.92 |
+|                 11 |        26.72 |         30.88 |
+|                 12 |        24.46 |         10.78 |
+|                 13 |        18.36 |         20.45 |
+|                 14 |        19.93 |          7.00 |
+|                 15 |        27.56 |         23.75 |
+|                 16 |        18.03 |         23.41 |
+|                 17 |        60.46 |         82.30 |
+|                 18 |       282.79 |        129.40 |
+|                 19 |        23.28 |         11.26 |
+|                 20 |        88.93 |         19.22 |
+|                 21 |       110.87 |        101.53 |
+|                 22 |        12.17 |         12.71 |
+| **Total (Q1–Q22)** |  **1353.21** |    **845.21** |
+
+Row counts agree across engines for every query. Ballista completed the
+full 22-query suite in a single continuous run.
+
+## Reproducing
 
 ### Ballista
 
-Start a scheduler and one executor per node:
+Bring up the cluster (one scheduler, N executors), then run the suite from a
+client. Executor sizing on each node:
 
 ```sh
-# scheduler
-ballista-scheduler --bind-host 0.0.0.0 --bind-port 50050
-
-# executor (one per node; --concurrent-tasks defaults to the detected core 
count)
 ballista-executor \
-  --bind-host 0.0.0.0 \
+  --bind-host 0.0.0.0 --bind-port 50051 \
   --scheduler-host <scheduler> --scheduler-port 50050 \
-  --memory-pool-size=48GB \
-  --work-dir /work \
-  --client-ttl=60
+  --concurrent-tasks 8 \
+  --memory-pool-size 48103633715
 ```
 
-`--client-ttl=60` enables shuffle-client connection caching. Without it every
-shuffle fetch opens a new connection, and a high `target_partitions` can 
exhaust
-ephemeral ports.
-
-Run a query:
+Run all 22 queries:
 
 ```sh
 tpch benchmark ballista \
   --host <scheduler> --port 50050 \
-  --query 18 \
-  --path /mnt/bigdata/tpch/sf1000 --format parquet \
-  --partitions 64 --iterations 1 \
-  -c ballista.planner.adaptive.enabled=true
+  --path s3://<bucket>/tpch/sf1000 --format parquet \
+  --partitions 256 --iterations 2 \
+  -c ballista.planner.adaptive.enabled=true \
+  -c datafusion.execution.collect_statistics=true \
+  -c ballista.shuffle.sort_based.memory_limit_per_task_bytes=0
 ```
 
-Omit `--query` to run all 22, and add `--skip <n>` (repeatable) to leave 
individual
-queries out of that run — `--skip 18` is what produced the Ballista column 
below.
-`ballista.planner.adaptive.enabled=true` is the AQE-on configuration these 
results
-use.
-
-Note that `datafusion.optimizer.prefer_hash_join` is deliberately left at its 
default.
-Under AQE it does not select the join strategy: `DelayJoinSelectionRule` folds 
both
-`HashJoinExec` and `SortMergeJoinExec` into a single 
`DynamicJoinSelectionExec`, and
-the strategy is then chosen from runtime statistics — the broadcast 
thresholds, and
-`ballista.optimizer.hash_join_max_build_partition_bytes` (64 MiB by default), 
which
-lowers a Partitioned hash join to the spillable `SortMergeJoin` when the build 
side's
-largest partition would not fit. Setting `prefer_hash_join=false` does not 
force
-sort-merge; it only changes which conversion path the plan takes, and the
-`SortMergeJoinExec` path discards the join's projection, so it measures a 
slower plan
-rather than a different join strategy.
-
 ### Spark
 
-Spark runs the same queries via `tpcbench.py` from
-[apache/datafusion-benchmarks][dfb]:
+Runs the same queries via `tpcbench.py` from
+[apache/datafusion-benchmarks](https://github.com/apache/datafusion-benchmarks),
+with the highlights above and stock Spark 3.4 defaults for everything else:
 
 ```sh
 spark-submit \
   --master <master> \
-  --conf spark.executor.instances=2 \
+  --conf spark.executor.instances=32 \
   --conf spark.executor.cores=16 \
-  --conf spark.executor.memory=32G \
-  --conf spark.executor.memoryOverhead=8G \
-  --conf spark.memory.offHeap.enabled=true \
-  --conf spark.memory.offHeap.size=16g \
-  --conf spark.comet.enabled=false \
-  --conf 
spark.shuffle.manager=org.apache.spark.shuffle.sort.SortShuffleManager \
+  --conf spark.executor.memory=64G \
+  --conf spark.executor.memoryOverhead=10G \
+  --conf spark.sql.shuffle.partitions=512 \
   tpcbench.py \
     --benchmark tpch \
-    --data /mnt/bigdata/tpch/sf1000 \
+    --data s3a://<bucket>/tpch/sf1000 \
     --format parquet \
-    --iterations 1 \
-    --query 18
+    --iterations 3
 ```
 
-`spark.comet.enabled=false` and the stock `SortShuffleManager` are what make 
this a
-**vanilla** Spark baseline, in case the image being used ships Comet.
-
-### Comet
-
-[Apache DataFusion Comet][comet] accelerates Spark by translating supported
-operators to DataFusion. Same queries, same sizing; the difference is the 
plugin,
-the Comet shuffle manager, and the jar on the classpath:
-
-```sh
-spark-submit \
-  --master <master> \
-  --jars $COMET_JAR --driver-class-path $COMET_JAR \
-  --conf spark.executor.extraClassPath=$COMET_JAR \
-  --conf spark.plugins=org.apache.spark.CometPlugin \
-  --conf 
spark.shuffle.manager=org.apache.spark.sql.comet.execution.shuffle.CometShuffleManager
 \
-  --conf spark.comet.exec.replaceSortMergeJoin=false \
-  --conf spark.comet.exec.memoryPool=fair_unified \
-  --conf spark.comet.exec.memoryPool.fraction=0.8 \
-  --conf spark.executor.instances=2 \
-  --conf spark.executor.cores=16 \
-  --conf spark.executor.memory=32G \
-  --conf spark.executor.memoryOverhead=8G \
-  --conf spark.memory.offHeap.enabled=true \
-  --conf spark.memory.offHeap.size=16g \
-  tpcbench.py \
-    --benchmark tpch \
-    --data /mnt/bigdata/tpch/sf1000 \
-    --format parquet \
-    --iterations 1 \
-    --query 18
-```
-
-`spark.comet.exec.replaceSortMergeJoin=false` keeps Comet on Spark's 
`SortMergeJoin`
-instead of converting it to a shuffled hash join.
-
-This no longer pins the two engines to the same join strategy. Ballista under 
AQE
-chooses per join at runtime — broadcast, Partitioned hash, or sort-merge — and 
there
-is no supported way to force it to sort-merge throughout (see the note in the 
Ballista
-section above). So a Ballista-vs-Comet gap may partly reflect different join
-strategies, not only different execution. Read the comparison with that in 
mind.
-
-[comet]: https://datafusion.apache.org/comet/
-
-## Why compare against Comet
-
-Comet is the most informative comparison available to Ballista, because 
**Comet and
-Ballista execute DataFusion physical plans using the same DataFusion 
operators**.
-The scan, filter, join, and aggregate implementations doing the work are 
largely
-shared code. When Ballista and Comet diverge on a query, the difference 
therefore
-points at what is _not_ shared — how work is distributed, scheduled, shuffled, 
and
-bounded by memory — rather than at the speed of the operators themselves. That 
is
-precisely the surface Ballista is trying to improve, which makes the comparison
-diagnostic rather than merely competitive.
-
-That said, **the two do not necessarily run the same plan shape**, and the 
numbers
-should not be read as an operator-level A/B:
-
-- **Different planners produce the plan.** Comet accelerates a plan that 
_Spark's_
-  optimizer produced: Spark chooses the join order and join strategies, and
-  **Spark's AQE** coalesces shuffle partitions, converts joins, and splits 
skewed
-  partitions at runtime. Ballista plans with DataFusion's optimizer and, when
-  enabled, its own experimental AQE. Spark's AQE is a mature implementation and
-  Ballista's is not, so the same SQL can arrive at execution with materially
-  different plans.
-- **Comet falls back to Spark.** Operators and expressions Comet does not 
support
-  stay on the JVM, so a Comet run is generally a mix of DataFusion and Spark
-  execution rather than an all-DataFusion one.
-- **The distribution models differ.** Comet executes within Spark's task model 
and
-  shuffle service; Ballista has its own scheduler, stage/task model, and 
shuffle.
-
-So a Comet-vs-Ballista gap is best read as a question — _what is Spark's 
planner or
-execution model doing here that Ballista's is not?_ — rather than as a verdict 
on
-DataFusion. Vanilla Spark is included as the third data point, since it 
isolates
-how much of any Comet result comes from DataFusion acceleration versus from 
Spark's
-planner.
-
-## Results
-
-TPC-H **SF1000**, reference cluster above, **AQE on**, 1 iteration,
-`target_partitions=64`, all Ballista join settings at their defaults. Ballista 
picks
-each join's strategy at runtime; Spark and Comet are held to `SortMergeJoin`. 
Times in
-seconds; lower is better.
-
-Versions under test:
-
-| Engine   | Version                                         |
-| -------- | ----------------------------------------------- |
-| Ballista | `#2124` @ `5290436d`                            |
-| Spark    | 3.5.3 (vanilla, Comet disabled)                 |
-| Comet    | `main` @ `b0165552` (1.0.0-SNAPSHOT, Spark 3.5) |
-
-Pin the **exact commit** the numbers came from, not "main": `main` moves, and 
a row
-that mixes numbers from different commits silently misattributes a regression.
-
-The Ballista column comes from a **single suite run of 21 queries** on freshly
-started executors, using `--skip 18` to leave out the one query that exhausts 
the
-memory pool and OOM-kills the executors (see below), which would otherwise 
wedge
-the rest of the suite. Comet and Spark each completed the full suite in one 
run.
-
-Keep the whole column to one continuous run wherever possible. Driving queries 
as
-individual single-query jobs measures something different: each job starts 
against
-a cold page cache, which on this cluster cost the two join-free queries (Q1, 
Q6)
-roughly 20 s and 20 s respectively when the two methods were compared 
directly. Use
-`--skip` rather than a loop of `--query` runs.
-
-Single iteration means **run-to-run variance is not quantified here**. 
Comparing the
-same commit across the two methods above showed spreads of up to ~20% on 
individual
-queries, so treat a per-query difference under about 20% as noise and read the 
total
-row instead.
-
-|                 Query |      Spark |      Comet | Ballista (AQE on) |   Rows 
|
-| --------------------: | ---------: | ---------: | ----------------: | -----: 
|
-|                     1 |      427.1 |       46.6 |              54.2 |      4 
|
-|                     2 |       75.9 |       39.2 |              60.3 |    100 
|
-|                     3 |      154.1 |      195.4 |             140.0 |     10 
|
-|                     4 |       82.0 |       42.6 |              46.3 |      5 
|
-|                     5 |      336.4 |      493.4 |             327.2 |      5 
|
-|                     6 |       28.9 |       14.6 |              27.2 |      1 
|
-|                     7 |      180.6 |      149.5 |             364.9 |      4 
|
-|                     8 |      391.8 |      642.1 |             481.1 |      2 
|
-|                     9 |      509.8 |      843.5 |             606.4 |    175 
|
-|                    10 |      151.1 |      104.8 |             148.4 |     20 
|
-|                    11 |       44.7 |       44.1 |              62.0 |  0 [1] 
|
-|                    12 |       74.1 |       49.9 |              58.3 |      2 
|
-|                    13 |       98.7 |       58.3 |             100.4 |     30 
|
-|                    14 |       43.0 |       27.4 |              71.1 |      1 
|
-|                    15 |      121.5 |       65.6 |              53.8 |  1 [2] 
|
-|                    16 |       24.5 |       17.5 |              34.3 |  27840 
|
-|                    17 |      406.7 |      285.8 |             417.8 |      1 
|
-|                    18 |      428.8 |      370.5 |               TBD |    100 
|
-|                    19 |       58.9 |       35.5 |              68.5 |      1 
|
-|                    20 |      105.9 |       67.6 |              93.7 | 110759 
|
-|                    21 |      562.2 |      460.1 |             698.0 |    100 
|
-|                    22 |       36.8 |       21.5 |              22.9 |      7 
|
-| **Total (excl. Q18)** | **3914.7** | **3705.0** |        **3936.8** |        
|
-
-The **Total** row sums the 21 queries **excluding Q18**, because Q18 has no 
Ballista
-figure at this commit (below), so a 22-query total would not be comparable 
across
-engines. Spark's and Comet's Q18 times are in its row.
-
-Row counts agree across all three engines on every query for which Ballista 
produced
-a result.
-
-The **AQE-off** Ballista column has been removed pending a re-run at a matched 
core
-count; the numbers above for AQE off were taken at a different executor size 
and are
-not comparable to a fresh AQE-off run, so they were dropped rather than left 
stale.
-
-[1] Q11 returns 0 rows for every engine at this scale factor: the query's 
threshold
-constant is tuned for SF1.
-
-[2] Q15 is a multi-statement query (`CREATE VIEW` / `SELECT` / `DROP VIEW`); 
all three
-engines report 1 row here. (A previous result set saw Spark report 0 for Q15 
depending
-on which statement the harness took as the result; it does not recur in this 
run.)
-
-**Q18 is not measured at this commit.** Q18's hash-join build side is 
`Partitioned`
-and does not spill; with 16 task slots sharing the 48 GB pool (3 GB per slot), 
the
-per-task build side exceeds the container limit and the executor is OOM-killed
-([#2025](https://github.com/apache/datafusion-ballista/issues/2025)). At the 
previous
-8-slot sizing (6 GB per slot) Q18 completed, so this is a direct consequence 
of the
-denser packing.
-
-It was excluded from the suite run above with `--skip 18` so that one query 
could not
-wedge the other 21, and it has not since been run to completion at this commit 
— hence
-`TBD` rather than `OOM`. An earlier attempt on this branch did OOM-kill an 
executor,
-but under a different configuration (`prefer_hash_join=false`, which sends the 
plan
-down a conversion path that discards the join's projection and so materialises 
more
-columns than the configuration documented above). That result does not 
transfer, so it
-is not recorded here.
-
-The `Rows` column is the row count the query returned, recorded so a time is 
never
-read without the answer it produced.
-
-This table records **one current result set**. When results are refreshed, the
-table and the pinned versions above are replaced together — a row must never 
mix
-numbers from different commits, because a stale row silently misattributes a
-regression.
-
-`TBD` means not yet measured on this cluster at this commit; a query that ran 
but
-did not produce an answer is recorded as `FAIL`, or `OOM` where the failure is 
a
-known memory exhaustion.
-
-### Recording a result
+## Recording a new result set
 
 - Pin the **exact commit** the numbers came from, not a branch name.
-- Report the **AQE-on** number (`ballista.planner.adaptive.enabled=true`) — the
-  configuration this page measures — from the same commit and cluster.
-- Prefer the figure from a **full suite run**, not a standalone single-query 
run: a
-  long-lived executor deep into a suite is not in the same state as a freshly 
started
-  one, and a single-query job pays a cold-cache penalty that is large enough 
to swamp
-  the effect being measured. When a query cannot complete in-suite (e.g. Q18's 
OOM
-  currently wedges the run), exclude it with `--skip <n>` so the rest of the 
column
-  still comes from one continuous run, and measure that query separately.
-- Note the **row count** each query returned. A fast wrong answer is not a 
result,
-  and distributed execution has produced silently wrong row counts before.
-- Flag any stage whose runtime is dominated by a few partitions — that is the
-  imbalance this page exists to surface.
-
-## Known issues found by benchmarking
-
-Benchmarking at SF1000 is how most of the following were found. They are worth
-knowing about before interpreting a number:
-
-| Issue                                                              | Summary 
                                                                                
                                                                                
                                 |
-| ------------------------------------------------------------------ | 
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 |
-| [#1359](https://github.com/apache/datafusion-ballista/issues/1359) | 
Umbrella issue for adaptive (AQE) query execution.                              
                                                                                
                                         |
-| [#2025](https://github.com/apache/datafusion-ballista/issues/2025) | Q18's 
hash-join build side exhausts the memory pool at SF1000. DataFusion's hash-join 
build side does not spill, so a per-partition build side larger than one task 
slot's pool fails the task outright. |
-| [#2063](https://github.com/apache/datafusion-ballista/issues/2063) | AQE can 
hang when a re-plan cancels an in-flight stage; the job never reports 
completion.                                                                     
                                           |
+- Replace the version, environment, config, and results tables together — a
+  row that mixes numbers from different commits silently misattributes a
+  regression.
+- Prefer a single continuous suite run: a long-lived executor deep into a
+  suite is not in the same state as a freshly started one.
+- Report `FAIL` for a query that ran but did not produce an answer, and
+  `OOM` when the failure is a known memory exhaustion. See the tracker for
+  open issues found by benchmarking: [#1359][aqe],
+  [#2025][q18], [#2063][aqe-hang].
+
+[aqe]: https://github.com/apache/datafusion-ballista/issues/1359
+[q18]: https://github.com/apache/datafusion-ballista/issues/2025
+[aqe-hang]: https://github.com/apache/datafusion-ballista/issues/2063
diff --git a/contributors-guide/benchmarking.html 
b/contributors-guide/benchmarking.html
index 384390bfe..d3f16c2b0 100644
--- a/contributors-guide/benchmarking.html
+++ b/contributors-guide/benchmarking.html
@@ -390,519 +390,314 @@
 -->
 <section id="benchmarking">
 <h1>Benchmarking<a class="headerlink" href="#benchmarking" title="Link to this 
heading">#</a></h1>
-<p>This page describes how Ballista is benchmarked at scale, and records the 
current
-results. It is aimed at contributors who want to reproduce a number, 
understand why
-a comparison is set up the way it is, or add a result of their own.</p>
-<p>The benchmark used here is derived from TPC-H. For running TPC-H locally at 
small
-scale factors, see <a class="reference external" 
href="https://github.com/apache/datafusion-ballista/blob/main/benchmarks/README.md";><code
 class="docutils literal notranslate"><span 
class="pre">benchmarks/README.md</span></code></a> in the repository; this
-page covers the multi-node, large-scale-factor setup and the cross-engine
-comparisons.</p>
-<section id="what-we-measure-and-why">
-<h2>What we measure, and why<a class="headerlink" 
href="#what-we-measure-and-why" title="Link to this heading">#</a></h2>
-<p>Ballista’s central performance question is <strong>how evenly work is 
spread across
-executor tasks</strong>. A distributed query is only as fast as its slowest 
stage, and a
-stage is only as fast as its slowest task, so a query whose work concentrates 
onto
-a few partitions will underperform no matter how fast the underlying operators 
are.
-Benchmarks are therefore run at a scale where that imbalance actually shows 
up.</p>
-<p>These benchmarks are run with <strong>adaptive query execution (AQE) 
enabled</strong>
-(<code class="docutils literal notranslate"><span 
class="pre">ballista.planner.adaptive.enabled=true</span></code>). This is the 
configuration Ballista is
-being actively developed against, so it is the one measured here.</p>
-<p>AQE selects the adaptive planner, which can re-plan stages using runtime 
statistics —
-coalescing partitions, re-optimising joins, and promoting a small join side to 
a
-broadcast at runtime. The alternative, AQE off, selects the static
-<code class="docutils literal notranslate"><span 
class="pre">DefaultDistributedPlanner</span></code>; it is a different planner 
with materially different join
-behaviour, and it is no longer benchmarked on this page.</p>
+<p>Current TPC-H <strong>SF1000</strong> results for Ballista, compared 
against a vanilla
+<strong>Spark 3.4</strong> baseline running on the same cluster shape.</p>
+<section id="versions-under-test">
+<h2>Versions under test<a class="headerlink" href="#versions-under-test" 
title="Link to this heading">#</a></h2>
+<div class="pst-scrollable-table-container"><table class="table">
+<thead>
+<tr class="row-odd"><th class="head"><p>Engine</p></th>
+<th class="head"><p>Version</p></th>
+</tr>
+</thead>
+<tbody>
+<tr class="row-even"><td><p>Ballista</p></td>
+<td><p><a class="reference external" 
href="https://github.com/apache/datafusion-ballista/commit/696ca29b3c1fe3013238822b7f5d8cdb918fdaa3";><code
 class="docutils literal notranslate"><span 
class="pre">696ca29b</span></code></a> (<code class="docutils literal 
notranslate"><span class="pre">main</span></code>, 2026-07-23), Cargo pkg <code 
class="docutils literal notranslate"><span class="pre">54.0.0</span></code>, 
DataFusion <code class="docutils literal notranslate"><span class="pre">54.1.0 
[...]
+</tr>
+<tr class="row-odd"><td><p>Spark</p></td>
+<td><p>3.4 (vanilla, no acceleration plugin)</p></td>
+</tr>
+</tbody>
+</table>
+</div>
 </section>
 <section id="environment">
 <h2>Environment<a class="headerlink" href="#environment" title="Link to this 
heading">#</a></h2>
-<p>The results on this page currently come from a <strong>small homelab 
cluster</strong>: two
-bare-metal nodes running Kubernetes, with the TPC-H data staged on node-local 
disk.
-This is a deliberate starting point rather than a final destination. A two-node
-cluster with local disk removes as many confounds as possible — no object-store
-latency, no cloud network variance, no noisy neighbours — so that when a query 
is
-slow, the cause is Ballista and not the environment.</p>
-<p>The intent is to <strong>move these benchmarks to AWS with data in 
S3</strong> once the results
-here are good. That environment is the one users actually run, and it exercises
-things a homelab cannot: object-store reads instead of local disk, higher and 
more
-variable network latency between executors, and larger executor counts. Some of
-Ballista’s behaviour is expected to change there — a shuffle that is cheap 
over a
-local link is not cheap over a cloud network, and object-store reads make scan
-parallelism matter differently.</p>
-<p>So results on this page should be read as <strong>relative comparisons on 
controlled
-hardware</strong>, useful for “did this change help”, not as absolute 
throughput numbers
-for a cloud deployment.</p>
+<ul class="simple">
+<li><p><strong>Cluster:</strong> Kubernetes on AWS (<code class="docutils 
literal notranslate"><span class="pre">us-west-2</span></code>); one 
driver/scheduler pod and
+32 executor pods for each engine, launched on the same node pool.</p></li>
+<li><p><strong>Executor pod (Ballista):</strong> x86_64, 8 vCPU, 64 GiB 
memory.</p></li>
+<li><p><strong>Executor pod (Spark):</strong> x86_64, 8 vCPU, 64 GiB + 10 GiB 
overhead.</p></li>
+<li><p><strong>Client pod (Ballista):</strong> the Python benchmark runner 
submits SQL to
+Ballista via <code class="docutils literal notranslate"><span 
class="pre">BallistaSessionContext</span></code> and collects results locally.
+Requires 64 GiB memory limit to complete the full 22-query suite —
+smaller limits (16 GiB) OOM the client partway through, even though the
+executor cluster is healthy.</p></li>
+<li><p><strong>Data:</strong> TPC-H SF1000 Parquet on S3 (<code 
class="docutils literal notranslate"><span 
class="pre">us-west-2</span></code>), ZSTD compression,
+~512 MiB row groups, one directory per table.</p></li>
+</ul>
 </section>
-<section id="reference-cluster">
-<h2>Reference cluster<a class="headerlink" href="#reference-cluster" 
title="Link to this heading">#</a></h2>
-<p>All results on this page use the following shape. It is a reference point, 
not a
-requirement — the commands below work on any cluster, but numbers are only
-comparable when the shape matches.</p>
+<section id="ballista-configuration">
+<h2>Ballista configuration<a class="headerlink" href="#ballista-configuration" 
title="Link to this heading">#</a></h2>
 <div class="pst-scrollable-table-container"><table class="table">
 <thead>
-<tr class="row-odd"><th class="head"><p></p></th>
-<th class="head"><p></p></th>
+<tr class="row-odd"><th class="head"><p>Flag / config key</p></th>
+<th class="head"><p>Value</p></th>
 </tr>
 </thead>
 <tbody>
-<tr class="row-even"><td><p>Executors</p></td>
-<td><p>2, one per physical node</p></td>
+<tr class="row-even"><td><p><code class="docutils literal notranslate"><span 
class="pre">--concurrent-tasks</span></code></p></td>
+<td><p><code class="docutils literal notranslate"><span 
class="pre">8</span></code></p></td>
+</tr>
+<tr class="row-odd"><td><p><code class="docutils literal notranslate"><span 
class="pre">--memory-pool-size</span></code> (bytes; ≈70 % of the 64 GiB 
container)</p></td>
+<td><p><code class="docutils literal notranslate"><span 
class="pre">48103633715</span></code></p></td>
 </tr>
-<tr class="row-odd"><td><p>Per executor</p></td>
-<td><p>16 cores, 56 GiB, <code class="docutils literal notranslate"><span 
class="pre">--memory-pool-size=48GB</span></code></p></td>
+<tr class="row-even"><td><p><code class="docutils literal notranslate"><span 
class="pre">datafusion.execution.target_partitions</span></code></p></td>
+<td><p><code class="docutils literal notranslate"><span 
class="pre">256</span></code></p></td>
 </tr>
-<tr class="row-even"><td><p>Per task slot</p></td>
-<td><p>16 concurrent tasks → 3 GB pool each</p></td>
+<tr class="row-odd"><td><p><code class="docutils literal notranslate"><span 
class="pre">datafusion.execution.collect_statistics</span></code></p></td>
+<td><p><code class="docutils literal notranslate"><span 
class="pre">true</span></code></p></td>
 </tr>
-<tr class="row-odd"><td><p>Scheduler</p></td>
-<td><p>1</p></td>
+<tr class="row-even"><td><p><code class="docutils literal notranslate"><span 
class="pre">datafusion.execution.listing_table_factory_infer_partitions</span></code></p></td>
+<td><p><code class="docutils literal notranslate"><span 
class="pre">false</span></code></p></td>
 </tr>
-<tr class="row-even"><td><p>Data</p></td>
-<td><p>TPC-H SF1000 Parquet, node-local disk</p></td>
+<tr class="row-odd"><td><p><code class="docutils literal notranslate"><span 
class="pre">datafusion.catalog.information_schema</span></code></p></td>
+<td><p><code class="docutils literal notranslate"><span 
class="pre">true</span></code></p></td>
 </tr>
-<tr class="row-odd"><td><p><code class="docutils literal notranslate"><span 
class="pre">target_partitions</span></code></p></td>
-<td><p>64</p></td>
+<tr class="row-even"><td><p><code class="docutils literal notranslate"><span 
class="pre">ballista.planner.adaptive.enabled</span></code></p></td>
+<td><p><code class="docutils literal notranslate"><span 
class="pre">true</span></code> (AQE)</p></td>
+</tr>
+<tr class="row-odd"><td><p><code class="docutils literal notranslate"><span 
class="pre">ballista.shuffle.sort_based.memory_limit_per_task_bytes</span></code></p></td>
+<td><p><code class="docutils literal notranslate"><span 
class="pre">0</span></code></p></td>
 </tr>
 </tbody>
 </table>
 </div>
-<p>Two details matter more than they look:</p>
-<ul class="simple">
-<li><p><strong>Executors are spread one per node.</strong> Packing two 
executors onto one node makes
-them contend for the same disk and memory bandwidth, which is measuring the 
host,
-not the engine.</p></li>
-<li><p><strong>Data is on node-local disk, not object storage.</strong> With a 
modest network between
-nodes, reading from object storage makes the interconnect the bottleneck and 
the
-engine comparison becomes an I/O comparison.</p></li>
-</ul>
-<p>The memory pool is deliberately set below the container limit. Ballista 
splits
-<code class="docutils literal notranslate"><span 
class="pre">--memory-pool-size</span></code> into one <code class="docutils 
literal notranslate"><span class="pre">FairSpillPool</span></code> per task 
slot, and that accounting
-does not cover every allocation, so leaving headroom between the pool and the
-container limit avoids the container being killed outright instead of 
reporting a
-graceful resource error.</p>
-</section>
-<section id="queries">
-<h2>Queries<a class="headerlink" href="#queries" title="Link to this 
heading">#</a></h2>
-<p>All engines run the <strong>same SQL</strong>, which is what makes a 
cross-engine comparison
-apples-to-apples.</p>
-<p>The shared set is the TPC-H queries from <a class="reference external" 
href="https://github.com/apache/datafusion-benchmarks";>apache/datafusion-benchmarks</a>
-(SQLBench-H). Ballista’s bundled queries in <code class="docutils literal 
notranslate"><span class="pre">benchmarks/queries/</span></code> are the classic
-TPC-H phrasing and differ in places, so the shared set is overlaid over them 
for a
-comparison run.</p>
-<p>Spark and Comet are driven through Comet’s benchmark harness, which reads 
its own
-bundled copy of the queries (labelled CometBench-H). That copy is textually
-identical to the SQLBench-H set — all 22 queries match once the licence header
-comment is ignored — so the engines are executing the same statements even 
though
-they load them from different paths. Worth re-checking if either set is ever
-regenerated.</p>
-</section>
-<section id="running-the-benchmark">
-<h2>Running the benchmark<a class="headerlink" href="#running-the-benchmark" 
title="Link to this heading">#</a></h2>
-<section id="ballista">
-<h3>Ballista<a class="headerlink" href="#ballista" title="Link to this 
heading">#</a></h3>
-<p>Start a scheduler and one executor per node:</p>
-<div class="highlight-sh notranslate"><div 
class="highlight"><pre><span></span><span class="c1"># scheduler</span>
-ballista-scheduler<span class="w"> </span>--bind-host<span class="w"> 
</span><span class="m">0</span>.0.0.0<span class="w"> </span>--bind-port<span 
class="w"> </span><span class="m">50050</span>
-
-<span class="c1"># executor (one per node; --concurrent-tasks defaults to the 
detected core count)</span>
-ballista-executor<span class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--bind-host<span class="w"> </span><span 
class="m">0</span>.0.0.0<span class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--scheduler-host<span class="w"> 
</span>&lt;scheduler&gt;<span class="w"> </span>--scheduler-port<span 
class="w"> </span><span class="m">50050</span><span class="w"> </span><span 
class="se">\</span>
-<span class="w">  </span>--memory-pool-size<span class="o">=</span>48GB<span 
class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--work-dir<span class="w"> </span>/work<span 
class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--client-ttl<span class="o">=</span><span 
class="m">60</span>
-</pre></div>
-</div>
-<p><code class="docutils literal notranslate"><span 
class="pre">--client-ttl=60</span></code> enables shuffle-client connection 
caching. Without it every
-shuffle fetch opens a new connection, and a high <code class="docutils literal 
notranslate"><span class="pre">target_partitions</span></code> can exhaust
-ephemeral ports.</p>
-<p>Run a query:</p>
-<div class="highlight-sh notranslate"><div 
class="highlight"><pre><span></span>tpch<span class="w"> </span>benchmark<span 
class="w"> </span>ballista<span class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--host<span class="w"> </span>&lt;scheduler&gt;<span 
class="w"> </span>--port<span class="w"> </span><span 
class="m">50050</span><span class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--query<span class="w"> </span><span 
class="m">18</span><span class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--path<span class="w"> 
</span>/mnt/bigdata/tpch/sf1000<span class="w"> </span>--format<span class="w"> 
</span>parquet<span class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--partitions<span class="w"> </span><span 
class="m">64</span><span class="w"> </span>--iterations<span class="w"> 
</span><span class="m">1</span><span class="w"> </span><span class="se">\</span>
-<span class="w">  </span>-c<span class="w"> 
</span>ballista.planner.adaptive.enabled<span class="o">=</span><span 
class="nb">true</span>
-</pre></div>
-</div>
-<p>Omit <code class="docutils literal notranslate"><span 
class="pre">--query</span></code> to run all 22, and add <code class="docutils 
literal notranslate"><span class="pre">--skip</span> <span 
class="pre">&lt;n&gt;</span></code> (repeatable) to leave individual
-queries out of that run — <code class="docutils literal notranslate"><span 
class="pre">--skip</span> <span class="pre">18</span></code> is what produced 
the Ballista column below.
-<code class="docutils literal notranslate"><span 
class="pre">ballista.planner.adaptive.enabled=true</span></code> is the AQE-on 
configuration these results
-use.</p>
-<p>Note that <code class="docutils literal notranslate"><span 
class="pre">datafusion.optimizer.prefer_hash_join</span></code> is deliberately 
left at its default.
-Under AQE it does not select the join strategy: <code class="docutils literal 
notranslate"><span class="pre">DelayJoinSelectionRule</span></code> folds both
-<code class="docutils literal notranslate"><span 
class="pre">HashJoinExec</span></code> and <code class="docutils literal 
notranslate"><span class="pre">SortMergeJoinExec</span></code> into a single 
<code class="docutils literal notranslate"><span 
class="pre">DynamicJoinSelectionExec</span></code>, and
-the strategy is then chosen from runtime statistics — the broadcast 
thresholds, and
-<code class="docutils literal notranslate"><span 
class="pre">ballista.optimizer.hash_join_max_build_partition_bytes</span></code>
 (64 MiB by default), which
-lowers a Partitioned hash join to the spillable <code class="docutils literal 
notranslate"><span class="pre">SortMergeJoin</span></code> when the build side’s
-largest partition would not fit. Setting <code class="docutils literal 
notranslate"><span class="pre">prefer_hash_join=false</span></code> does not 
force
-sort-merge; it only changes which conversion path the plan takes, and the
-<code class="docutils literal notranslate"><span 
class="pre">SortMergeJoinExec</span></code> path discards the join’s 
projection, so it measures a slower plan
-rather than a different join strategy.</p>
-</section>
-<section id="spark">
-<h3>Spark<a class="headerlink" href="#spark" title="Link to this 
heading">#</a></h3>
-<p>Spark runs the same queries via <code class="docutils literal 
notranslate"><span class="pre">tpcbench.py</span></code> from
-<a class="reference external" 
href="https://github.com/apache/datafusion-benchmarks";>apache/datafusion-benchmarks</a>:</p>
-<div class="highlight-sh notranslate"><div 
class="highlight"><pre><span></span>spark-submit<span class="w"> </span><span 
class="se">\</span>
-<span class="w">  </span>--master<span class="w"> </span>&lt;master&gt;<span 
class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--conf<span class="w"> 
</span>spark.executor.instances<span class="o">=</span><span 
class="m">2</span><span class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--conf<span class="w"> 
</span>spark.executor.cores<span class="o">=</span><span 
class="m">16</span><span class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--conf<span class="w"> 
</span>spark.executor.memory<span class="o">=</span>32G<span class="w"> 
</span><span class="se">\</span>
-<span class="w">  </span>--conf<span class="w"> 
</span>spark.executor.memoryOverhead<span class="o">=</span>8G<span class="w"> 
</span><span class="se">\</span>
-<span class="w">  </span>--conf<span class="w"> 
</span>spark.memory.offHeap.enabled<span class="o">=</span><span 
class="nb">true</span><span class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--conf<span class="w"> 
</span>spark.memory.offHeap.size<span class="o">=</span>16g<span class="w"> 
</span><span class="se">\</span>
-<span class="w">  </span>--conf<span class="w"> 
</span>spark.comet.enabled<span class="o">=</span><span 
class="nb">false</span><span class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--conf<span class="w"> 
</span>spark.shuffle.manager<span 
class="o">=</span>org.apache.spark.shuffle.sort.SortShuffleManager<span 
class="w"> </span><span class="se">\</span>
-<span class="w">  </span>tpcbench.py<span class="w"> </span><span 
class="se">\</span>
-<span class="w">    </span>--benchmark<span class="w"> </span>tpch<span 
class="w"> </span><span class="se">\</span>
-<span class="w">    </span>--data<span class="w"> 
</span>/mnt/bigdata/tpch/sf1000<span class="w"> </span><span class="se">\</span>
-<span class="w">    </span>--format<span class="w"> </span>parquet<span 
class="w"> </span><span class="se">\</span>
-<span class="w">    </span>--iterations<span class="w"> </span><span 
class="m">1</span><span class="w"> </span><span class="se">\</span>
-<span class="w">    </span>--query<span class="w"> </span><span 
class="m">18</span>
-</pre></div>
-</div>
-<p><code class="docutils literal notranslate"><span 
class="pre">spark.comet.enabled=false</span></code> and the stock <code 
class="docutils literal notranslate"><span 
class="pre">SortShuffleManager</span></code> are what make this a
-<strong>vanilla</strong> Spark baseline, in case the image being used ships 
Comet.</p>
-</section>
-<section id="comet">
-<h3>Comet<a class="headerlink" href="#comet" title="Link to this 
heading">#</a></h3>
-<p><a class="reference external" 
href="https://datafusion.apache.org/comet/";>Apache DataFusion Comet</a> 
accelerates Spark by translating supported
-operators to DataFusion. Same queries, same sizing; the difference is the 
plugin,
-the Comet shuffle manager, and the jar on the classpath:</p>
-<div class="highlight-sh notranslate"><div 
class="highlight"><pre><span></span>spark-submit<span class="w"> </span><span 
class="se">\</span>
-<span class="w">  </span>--master<span class="w"> </span>&lt;master&gt;<span 
class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--jars<span class="w"> </span><span 
class="nv">$COMET_JAR</span><span class="w"> </span>--driver-class-path<span 
class="w"> </span><span class="nv">$COMET_JAR</span><span class="w"> 
</span><span class="se">\</span>
-<span class="w">  </span>--conf<span class="w"> 
</span>spark.executor.extraClassPath<span class="o">=</span><span 
class="nv">$COMET_JAR</span><span class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--conf<span class="w"> </span>spark.plugins<span 
class="o">=</span>org.apache.spark.CometPlugin<span class="w"> </span><span 
class="se">\</span>
-<span class="w">  </span>--conf<span class="w"> 
</span>spark.shuffle.manager<span 
class="o">=</span>org.apache.spark.sql.comet.execution.shuffle.CometShuffleManager<span
 class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--conf<span class="w"> 
</span>spark.comet.exec.replaceSortMergeJoin<span class="o">=</span><span 
class="nb">false</span><span class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--conf<span class="w"> 
</span>spark.comet.exec.memoryPool<span class="o">=</span>fair_unified<span 
class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--conf<span class="w"> 
</span>spark.comet.exec.memoryPool.fraction<span class="o">=</span><span 
class="m">0</span>.8<span class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--conf<span class="w"> 
</span>spark.executor.instances<span class="o">=</span><span 
class="m">2</span><span class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--conf<span class="w"> 
</span>spark.executor.cores<span class="o">=</span><span 
class="m">16</span><span class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--conf<span class="w"> 
</span>spark.executor.memory<span class="o">=</span>32G<span class="w"> 
</span><span class="se">\</span>
-<span class="w">  </span>--conf<span class="w"> 
</span>spark.executor.memoryOverhead<span class="o">=</span>8G<span class="w"> 
</span><span class="se">\</span>
-<span class="w">  </span>--conf<span class="w"> 
</span>spark.memory.offHeap.enabled<span class="o">=</span><span 
class="nb">true</span><span class="w"> </span><span class="se">\</span>
-<span class="w">  </span>--conf<span class="w"> 
</span>spark.memory.offHeap.size<span class="o">=</span>16g<span class="w"> 
</span><span class="se">\</span>
-<span class="w">  </span>tpcbench.py<span class="w"> </span><span 
class="se">\</span>
-<span class="w">    </span>--benchmark<span class="w"> </span>tpch<span 
class="w"> </span><span class="se">\</span>
-<span class="w">    </span>--data<span class="w"> 
</span>/mnt/bigdata/tpch/sf1000<span class="w"> </span><span class="se">\</span>
-<span class="w">    </span>--format<span class="w"> </span>parquet<span 
class="w"> </span><span class="se">\</span>
-<span class="w">    </span>--iterations<span class="w"> </span><span 
class="m">1</span><span class="w"> </span><span class="se">\</span>
-<span class="w">    </span>--query<span class="w"> </span><span 
class="m">18</span>
-</pre></div>
-</div>
-<p><code class="docutils literal notranslate"><span 
class="pre">spark.comet.exec.replaceSortMergeJoin=false</span></code> keeps 
Comet on Spark’s <code class="docutils literal notranslate"><span 
class="pre">SortMergeJoin</span></code>
-instead of converting it to a shuffled hash join.</p>
-<p>This no longer pins the two engines to the same join strategy. Ballista 
under AQE
-chooses per join at runtime — broadcast, Partitioned hash, or sort-merge — and 
there
-is no supported way to force it to sort-merge throughout (see the note in the 
Ballista
-section above). So a Ballista-vs-Comet gap may partly reflect different join
-strategies, not only different execution. Read the comparison with that in 
mind.</p>
-</section>
-</section>
-<section id="why-compare-against-comet">
-<h2>Why compare against Comet<a class="headerlink" 
href="#why-compare-against-comet" title="Link to this heading">#</a></h2>
-<p>Comet is the most informative comparison available to Ballista, because 
<strong>Comet and
-Ballista execute DataFusion physical plans using the same DataFusion 
operators</strong>.
-The scan, filter, join, and aggregate implementations doing the work are 
largely
-shared code. When Ballista and Comet diverge on a query, the difference 
therefore
-points at what is <em>not</em> shared — how work is distributed, scheduled, 
shuffled, and
-bounded by memory — rather than at the speed of the operators themselves. That 
is
-precisely the surface Ballista is trying to improve, which makes the comparison
-diagnostic rather than merely competitive.</p>
-<p>That said, <strong>the two do not necessarily run the same plan 
shape</strong>, and the numbers
-should not be read as an operator-level A/B:</p>
-<ul class="simple">
-<li><p><strong>Different planners produce the plan.</strong> Comet accelerates 
a plan that <em>Spark’s</em>
-optimizer produced: Spark chooses the join order and join strategies, and
-<strong>Spark’s AQE</strong> coalesces shuffle partitions, converts joins, and 
splits skewed
-partitions at runtime. Ballista plans with DataFusion’s optimizer and, when
-enabled, its own experimental AQE. Spark’s AQE is a mature implementation and
-Ballista’s is not, so the same SQL can arrive at execution with materially
-different plans.</p></li>
-<li><p><strong>Comet falls back to Spark.</strong> Operators and expressions 
Comet does not support
-stay on the JVM, so a Comet run is generally a mix of DataFusion and Spark
-execution rather than an all-DataFusion one.</p></li>
-<li><p><strong>The distribution models differ.</strong> Comet executes within 
Spark’s task model and
-shuffle service; Ballista has its own scheduler, stage/task model, and 
shuffle.</p></li>
-</ul>
-<p>So a Comet-vs-Ballista gap is best read as a question — <em>what is Spark’s 
planner or
-execution model doing here that Ballista’s is not?</em> — rather than as a 
verdict on
-DataFusion. Vanilla Spark is included as the third data point, since it 
isolates
-how much of any Comet result comes from DataFusion acceleration versus from 
Spark’s
-planner.</p>
+<p><code class="docutils literal notranslate"><span 
class="pre">datafusion.optimizer.prefer_hash_join</span></code> is left at its 
default; under AQE the
+join strategy is selected at runtime by <code class="docutils literal 
notranslate"><span class="pre">DelayJoinSelectionRule</span></code> /
+<code class="docutils literal notranslate"><span 
class="pre">DynamicJoinSelectionExec</span></code> from runtime statistics and 
the broadcast /
+<code class="docutils literal notranslate"><span 
class="pre">ballista.optimizer.hash_join_max_build_partition_bytes</span></code>
 thresholds.</p>
 </section>
-<section id="results">
-<h2>Results<a class="headerlink" href="#results" title="Link to this 
heading">#</a></h2>
-<p>TPC-H <strong>SF1000</strong>, reference cluster above, <strong>AQE 
on</strong>, 1 iteration,
-<code class="docutils literal notranslate"><span 
class="pre">target_partitions=64</span></code>, all Ballista join settings at 
their defaults. Ballista picks
-each join’s strategy at runtime; Spark and Comet are held to <code 
class="docutils literal notranslate"><span 
class="pre">SortMergeJoin</span></code>. Times in
-seconds; lower is better.</p>
-<p>Versions under test:</p>
+<section id="spark-configuration-highlights">
+<h2>Spark configuration (highlights)<a class="headerlink" 
href="#spark-configuration-highlights" title="Link to this heading">#</a></h2>
+<p>Vanilla Spark 3.4 — no Comet plugin, stock <code class="docutils literal 
notranslate"><span class="pre">SortShuffleManager</span></code>.</p>
 <div class="pst-scrollable-table-container"><table class="table">
 <thead>
-<tr class="row-odd"><th class="head"><p>Engine</p></th>
-<th class="head"><p>Version</p></th>
+<tr class="row-odd"><th class="head"><p>Key</p></th>
+<th class="head"><p>Value</p></th>
 </tr>
 </thead>
 <tbody>
-<tr class="row-even"><td><p>Ballista</p></td>
-<td><p><code class="docutils literal notranslate"><span 
class="pre">#2124</span></code> &#64; <code class="docutils literal 
notranslate"><span class="pre">5290436d</span></code></p></td>
+<tr class="row-even"><td><p><code class="docutils literal notranslate"><span 
class="pre">spark.executor.instances</span></code></p></td>
+<td><p><code class="docutils literal notranslate"><span 
class="pre">32</span></code></p></td>
 </tr>
-<tr class="row-odd"><td><p>Spark</p></td>
-<td><p>3.5.3 (vanilla, Comet disabled)</p></td>
+<tr class="row-odd"><td><p><code class="docutils literal notranslate"><span 
class="pre">spark.executor.cores</span></code></p></td>
+<td><p><code class="docutils literal notranslate"><span 
class="pre">16</span></code> (task parallelism per executor)</p></td>
+</tr>
+<tr class="row-even"><td><p><code class="docutils literal notranslate"><span 
class="pre">spark.kubernetes.executor.limit.cores</span></code> / <code 
class="docutils literal notranslate"><span 
class="pre">.request.cores</span></code></p></td>
+<td><p><code class="docutils literal notranslate"><span 
class="pre">8</span></code> (physical vCPU)</p></td>
+</tr>
+<tr class="row-odd"><td><p><code class="docutils literal notranslate"><span 
class="pre">spark.executor.memory</span></code></p></td>
+<td><p><code class="docutils literal notranslate"><span 
class="pre">64G</span></code></p></td>
 </tr>
-<tr class="row-even"><td><p>Comet</p></td>
-<td><p><code class="docutils literal notranslate"><span 
class="pre">main</span></code> &#64; <code class="docutils literal 
notranslate"><span class="pre">b0165552</span></code> (1.0.0-SNAPSHOT, Spark 
3.5)</p></td>
+<tr class="row-even"><td><p><code class="docutils literal notranslate"><span 
class="pre">spark.executor.memoryOverhead</span></code></p></td>
+<td><p><code class="docutils literal notranslate"><span 
class="pre">10G</span></code></p></td>
+</tr>
+<tr class="row-odd"><td><p><code class="docutils literal notranslate"><span 
class="pre">spark.memory.fraction</span></code></p></td>
+<td><p><code class="docutils literal notranslate"><span 
class="pre">0.6</span></code></p></td>
+</tr>
+<tr class="row-even"><td><p><code class="docutils literal notranslate"><span 
class="pre">spark.memory.storageFraction</span></code></p></td>
+<td><p><code class="docutils literal notranslate"><span 
class="pre">0.2</span></code></p></td>
+</tr>
+<tr class="row-odd"><td><p><code class="docutils literal notranslate"><span 
class="pre">spark.sql.shuffle.partitions</span></code></p></td>
+<td><p><code class="docutils literal notranslate"><span 
class="pre">512</span></code></p></td>
+</tr>
+<tr class="row-even"><td><p><code class="docutils literal notranslate"><span 
class="pre">spark.sql.broadcastTimeout</span></code></p></td>
+<td><p><code class="docutils literal notranslate"><span 
class="pre">900</span></code></p></td>
+</tr>
+<tr class="row-odd"><td><p><code class="docutils literal notranslate"><span 
class="pre">spark.serializer</span></code></p></td>
+<td><p><code class="docutils literal notranslate"><span 
class="pre">KryoSerializer</span></code></p></td>
+</tr>
+<tr class="row-even"><td><p><code class="docutils literal notranslate"><span 
class="pre">spark.io.compression.codec</span></code></p></td>
+<td><p><code class="docutils literal notranslate"><span 
class="pre">zstd</span></code></p></td>
 </tr>
 </tbody>
 </table>
 </div>
-<p>Pin the <strong>exact commit</strong> the numbers came from, not “main”: 
<code class="docutils literal notranslate"><span class="pre">main</span></code> 
moves, and a row
-that mixes numbers from different commits silently misattributes a 
regression.</p>
-<p>The Ballista column comes from a <strong>single suite run of 21 
queries</strong> on freshly
-started executors, using <code class="docutils literal notranslate"><span 
class="pre">--skip</span> <span class="pre">18</span></code> to leave out the 
one query that exhausts the
-memory pool and OOM-kills the executors (see below), which would otherwise 
wedge
-the rest of the suite. Comet and Spark each completed the full suite in one 
run.</p>
-<p>Keep the whole column to one continuous run wherever possible. Driving 
queries as
-individual single-query jobs measures something different: each job starts 
against
-a cold page cache, which on this cluster cost the two join-free queries (Q1, 
Q6)
-roughly 20 s and 20 s respectively when the two methods were compared 
directly. Use
-<code class="docutils literal notranslate"><span 
class="pre">--skip</span></code> rather than a loop of <code class="docutils 
literal notranslate"><span class="pre">--query</span></code> runs.</p>
-<p>Single iteration means <strong>run-to-run variance is not quantified 
here</strong>. Comparing the
-same commit across the two methods above showed spreads of up to ~20% on 
individual
-queries, so treat a per-query difference under about 20% as noise and read the 
total
-row instead.</p>
+<p>Spark AQE is left at its Spark 3.4 defaults. Shuffle spills to a <code 
class="docutils literal notranslate"><span class="pre">gp3</span></code>-backed
+per-executor volume (<code class="docutils literal notranslate"><span 
class="pre">spark.kubernetes.executor.volumes...spark-local-dir-1</span></code>).</p>
+<p>Note that <code class="docutils literal notranslate"><span 
class="pre">spark.executor.cores=16</span></code> is Spark’s <strong>task 
parallelism</strong> setting,
+not a CPU allocation — each executor pod is given only <strong>8 physical 
vCPU</strong>
+via <code class="docutils literal notranslate"><span 
class="pre">spark.kubernetes.executor.limit.cores</span></code> / <code 
class="docutils literal notranslate"><span 
class="pre">.request.cores</span></code>, so Spark
+schedules 16 concurrent tasks onto 8 physical cores (2× oversubscription).
+The matching Ballista executor runs <code class="docutils literal 
notranslate"><span class="pre">--concurrent-tasks=8</span></code> on the same
+8 physical vCPU (1:1).</p>
+</section>
+<section id="queries">
+<h2>Queries<a class="headerlink" href="#queries" title="Link to this 
heading">#</a></h2>
+<p>The SQLBench-H phrasing of the 22 TPC-H queries from
+<a class="reference external" 
href="https://github.com/apache/datafusion-benchmarks";>apache/datafusion-benchmarks</a>.</p>
+</section>
+<section id="results">
+<h2>Results<a class="headerlink" href="#results" title="Link to this 
heading">#</a></h2>
+<p><strong>Times in seconds; lower is better.</strong> Ballista: single 
iteration. Spark:
+mean of 3 iterations (cold iteration dropped by the harness).</p>
 <div class="pst-scrollable-table-container"><table class="table">
 <thead>
 <tr class="row-odd"><th class="head text-right"><p>Query</p></th>
-<th class="head text-right"><p>Spark</p></th>
-<th class="head text-right"><p>Comet</p></th>
-<th class="head text-right"><p>Ballista (AQE on)</p></th>
-<th class="head text-right"><p>Rows</p></th>
+<th class="head text-right"><p>Ballista (s)</p></th>
+<th class="head text-right"><p>Spark 3.4 (s)</p></th>
 </tr>
 </thead>
 <tbody>
 <tr class="row-even"><td class="text-right"><p>1</p></td>
-<td class="text-right"><p>427.1</p></td>
-<td class="text-right"><p>46.6</p></td>
-<td class="text-right"><p>54.2</p></td>
-<td class="text-right"><p>4</p></td>
+<td class="text-right"><p>21.16</p></td>
+<td class="text-right"><p>67.58</p></td>
 </tr>
 <tr class="row-odd"><td class="text-right"><p>2</p></td>
-<td class="text-right"><p>75.9</p></td>
-<td class="text-right"><p>39.2</p></td>
-<td class="text-right"><p>60.3</p></td>
-<td class="text-right"><p>100</p></td>
+<td class="text-right"><p>32.37</p></td>
+<td class="text-right"><p>29.80</p></td>
 </tr>
 <tr class="row-even"><td class="text-right"><p>3</p></td>
-<td class="text-right"><p>154.1</p></td>
-<td class="text-right"><p>195.4</p></td>
-<td class="text-right"><p>140.0</p></td>
-<td class="text-right"><p>10</p></td>
+<td class="text-right"><p>32.69</p></td>
+<td class="text-right"><p>25.13</p></td>
 </tr>
 <tr class="row-odd"><td class="text-right"><p>4</p></td>
-<td class="text-right"><p>82.0</p></td>
-<td class="text-right"><p>42.6</p></td>
-<td class="text-right"><p>46.3</p></td>
-<td class="text-right"><p>5</p></td>
+<td class="text-right"><p>27.75</p></td>
+<td class="text-right"><p>21.19</p></td>
 </tr>
 <tr class="row-even"><td class="text-right"><p>5</p></td>
-<td class="text-right"><p>336.4</p></td>
-<td class="text-right"><p>493.4</p></td>
-<td class="text-right"><p>327.2</p></td>
-<td class="text-right"><p>5</p></td>
+<td class="text-right"><p>49.37</p></td>
+<td class="text-right"><p>54.12</p></td>
 </tr>
 <tr class="row-odd"><td class="text-right"><p>6</p></td>
-<td class="text-right"><p>28.9</p></td>
-<td class="text-right"><p>14.6</p></td>
-<td class="text-right"><p>27.2</p></td>
-<td class="text-right"><p>1</p></td>
+<td class="text-right"><p>16.17</p></td>
+<td class="text-right"><p>1.23</p></td>
 </tr>
 <tr class="row-even"><td class="text-right"><p>7</p></td>
-<td class="text-right"><p>180.6</p></td>
-<td class="text-right"><p>149.5</p></td>
-<td class="text-right"><p>364.9</p></td>
-<td class="text-right"><p>4</p></td>
+<td class="text-right"><p>48.24</p></td>
+<td class="text-right"><p>19.57</p></td>
 </tr>
 <tr class="row-odd"><td class="text-right"><p>8</p></td>
-<td class="text-right"><p>391.8</p></td>
-<td class="text-right"><p>642.1</p></td>
-<td class="text-right"><p>481.1</p></td>
-<td class="text-right"><p>2</p></td>
+<td class="text-right"><p>189.30</p></td>
+<td class="text-right"><p>48.60</p></td>
 </tr>
 <tr class="row-even"><td class="text-right"><p>9</p></td>
-<td class="text-right"><p>509.8</p></td>
-<td class="text-right"><p>843.5</p></td>
-<td class="text-right"><p>606.4</p></td>
-<td class="text-right"><p>175</p></td>
+<td class="text-right"><p>154.88</p></td>
+<td class="text-right"><p>69.38</p></td>
 </tr>
 <tr class="row-odd"><td class="text-right"><p>10</p></td>
-<td class="text-right"><p>151.1</p></td>
-<td class="text-right"><p>104.8</p></td>
-<td class="text-right"><p>148.4</p></td>
-<td class="text-right"><p>20</p></td>
+<td class="text-right"><p>67.72</p></td>
+<td class="text-right"><p>35.92</p></td>
 </tr>
 <tr class="row-even"><td class="text-right"><p>11</p></td>
-<td class="text-right"><p>44.7</p></td>
-<td class="text-right"><p>44.1</p></td>
-<td class="text-right"><p>62.0</p></td>
-<td class="text-right"><p>0 [1]</p></td>
+<td class="text-right"><p>26.72</p></td>
+<td class="text-right"><p>30.88</p></td>
 </tr>
 <tr class="row-odd"><td class="text-right"><p>12</p></td>
-<td class="text-right"><p>74.1</p></td>
-<td class="text-right"><p>49.9</p></td>
-<td class="text-right"><p>58.3</p></td>
-<td class="text-right"><p>2</p></td>
+<td class="text-right"><p>24.46</p></td>
+<td class="text-right"><p>10.78</p></td>
 </tr>
 <tr class="row-even"><td class="text-right"><p>13</p></td>
-<td class="text-right"><p>98.7</p></td>
-<td class="text-right"><p>58.3</p></td>
-<td class="text-right"><p>100.4</p></td>
-<td class="text-right"><p>30</p></td>
+<td class="text-right"><p>18.36</p></td>
+<td class="text-right"><p>20.45</p></td>
 </tr>
 <tr class="row-odd"><td class="text-right"><p>14</p></td>
-<td class="text-right"><p>43.0</p></td>
-<td class="text-right"><p>27.4</p></td>
-<td class="text-right"><p>71.1</p></td>
-<td class="text-right"><p>1</p></td>
+<td class="text-right"><p>19.93</p></td>
+<td class="text-right"><p>7.00</p></td>
 </tr>
 <tr class="row-even"><td class="text-right"><p>15</p></td>
-<td class="text-right"><p>121.5</p></td>
-<td class="text-right"><p>65.6</p></td>
-<td class="text-right"><p>53.8</p></td>
-<td class="text-right"><p>1 [2]</p></td>
+<td class="text-right"><p>27.56</p></td>
+<td class="text-right"><p>23.75</p></td>
 </tr>
 <tr class="row-odd"><td class="text-right"><p>16</p></td>
-<td class="text-right"><p>24.5</p></td>
-<td class="text-right"><p>17.5</p></td>
-<td class="text-right"><p>34.3</p></td>
-<td class="text-right"><p>27840</p></td>
+<td class="text-right"><p>18.03</p></td>
+<td class="text-right"><p>23.41</p></td>
 </tr>
 <tr class="row-even"><td class="text-right"><p>17</p></td>
-<td class="text-right"><p>406.7</p></td>
-<td class="text-right"><p>285.8</p></td>
-<td class="text-right"><p>417.8</p></td>
-<td class="text-right"><p>1</p></td>
+<td class="text-right"><p>60.46</p></td>
+<td class="text-right"><p>82.30</p></td>
 </tr>
 <tr class="row-odd"><td class="text-right"><p>18</p></td>
-<td class="text-right"><p>428.8</p></td>
-<td class="text-right"><p>370.5</p></td>
-<td class="text-right"><p>TBD</p></td>
-<td class="text-right"><p>100</p></td>
+<td class="text-right"><p>282.79</p></td>
+<td class="text-right"><p>129.40</p></td>
 </tr>
 <tr class="row-even"><td class="text-right"><p>19</p></td>
-<td class="text-right"><p>58.9</p></td>
-<td class="text-right"><p>35.5</p></td>
-<td class="text-right"><p>68.5</p></td>
-<td class="text-right"><p>1</p></td>
+<td class="text-right"><p>23.28</p></td>
+<td class="text-right"><p>11.26</p></td>
 </tr>
 <tr class="row-odd"><td class="text-right"><p>20</p></td>
-<td class="text-right"><p>105.9</p></td>
-<td class="text-right"><p>67.6</p></td>
-<td class="text-right"><p>93.7</p></td>
-<td class="text-right"><p>110759</p></td>
+<td class="text-right"><p>88.93</p></td>
+<td class="text-right"><p>19.22</p></td>
 </tr>
 <tr class="row-even"><td class="text-right"><p>21</p></td>
-<td class="text-right"><p>562.2</p></td>
-<td class="text-right"><p>460.1</p></td>
-<td class="text-right"><p>698.0</p></td>
-<td class="text-right"><p>100</p></td>
+<td class="text-right"><p>110.87</p></td>
+<td class="text-right"><p>101.53</p></td>
 </tr>
 <tr class="row-odd"><td class="text-right"><p>22</p></td>
-<td class="text-right"><p>36.8</p></td>
-<td class="text-right"><p>21.5</p></td>
-<td class="text-right"><p>22.9</p></td>
-<td class="text-right"><p>7</p></td>
+<td class="text-right"><p>12.17</p></td>
+<td class="text-right"><p>12.71</p></td>
 </tr>
-<tr class="row-even"><td class="text-right"><p><strong>Total (excl. 
Q18)</strong></p></td>
-<td class="text-right"><p><strong>3914.7</strong></p></td>
-<td class="text-right"><p><strong>3705.0</strong></p></td>
-<td class="text-right"><p><strong>3936.8</strong></p></td>
-<td class="text-right"><p></p></td>
+<tr class="row-even"><td class="text-right"><p><strong>Total 
(Q1–Q22)</strong></p></td>
+<td class="text-right"><p><strong>1353.21</strong></p></td>
+<td class="text-right"><p><strong>845.21</strong></p></td>
 </tr>
 </tbody>
 </table>
 </div>
-<p>The <strong>Total</strong> row sums the 21 queries <strong>excluding 
Q18</strong>, because Q18 has no Ballista
-figure at this commit (below), so a 22-query total would not be comparable 
across
-engines. Spark’s and Comet’s Q18 times are in its row.</p>
-<p>Row counts agree across all three engines on every query for which Ballista 
produced
-a result.</p>
-<p>The <strong>AQE-off</strong> Ballista column has been removed pending a 
re-run at a matched core
-count; the numbers above for AQE off were taken at a different executor size 
and are
-not comparable to a fresh AQE-off run, so they were dropped rather than left 
stale.</p>
-<p>[1] Q11 returns 0 rows for every engine at this scale factor: the query’s 
threshold
-constant is tuned for SF1.</p>
-<p>[2] Q15 is a multi-statement query (<code class="docutils literal 
notranslate"><span class="pre">CREATE</span> <span 
class="pre">VIEW</span></code> / <code class="docutils literal 
notranslate"><span class="pre">SELECT</span></code> / <code class="docutils 
literal notranslate"><span class="pre">DROP</span> <span 
class="pre">VIEW</span></code>); all three
-engines report 1 row here. (A previous result set saw Spark report 0 for Q15 
depending
-on which statement the harness took as the result; it does not recur in this 
run.)</p>
-<p><strong>Q18 is not measured at this commit.</strong> Q18’s hash-join build 
side is <code class="docutils literal notranslate"><span 
class="pre">Partitioned</span></code>
-and does not spill; with 16 task slots sharing the 48 GB pool (3 GB per slot), 
the
-per-task build side exceeds the container limit and the executor is OOM-killed
-(<a class="reference external" 
href="https://github.com/apache/datafusion-ballista/issues/2025";>#2025</a>). At 
the previous
-8-slot sizing (6 GB per slot) Q18 completed, so this is a direct consequence 
of the
-denser packing.</p>
-<p>It was excluded from the suite run above with <code class="docutils literal 
notranslate"><span class="pre">--skip</span> <span class="pre">18</span></code> 
so that one query could not
-wedge the other 21, and it has not since been run to completion at this commit 
— hence
-<code class="docutils literal notranslate"><span class="pre">TBD</span></code> 
rather than <code class="docutils literal notranslate"><span 
class="pre">OOM</span></code>. An earlier attempt on this branch did OOM-kill 
an executor,
-but under a different configuration (<code class="docutils literal 
notranslate"><span class="pre">prefer_hash_join=false</span></code>, which 
sends the plan
-down a conversion path that discards the join’s projection and so materialises 
more
-columns than the configuration documented above). That result does not 
transfer, so it
-is not recorded here.</p>
-<p>The <code class="docutils literal notranslate"><span 
class="pre">Rows</span></code> column is the row count the query returned, 
recorded so a time is never
-read without the answer it produced.</p>
-<p>This table records <strong>one current result set</strong>. When results 
are refreshed, the
-table and the pinned versions above are replaced together — a row must never 
mix
-numbers from different commits, because a stale row silently misattributes a
-regression.</p>
-<p><code class="docutils literal notranslate"><span 
class="pre">TBD</span></code> means not yet measured on this cluster at this 
commit; a query that ran but
-did not produce an answer is recorded as <code class="docutils literal 
notranslate"><span class="pre">FAIL</span></code>, or <code class="docutils 
literal notranslate"><span class="pre">OOM</span></code> where the failure is a
-known memory exhaustion.</p>
-<section id="recording-a-result">
-<h3>Recording a result<a class="headerlink" href="#recording-a-result" 
title="Link to this heading">#</a></h3>
-<ul class="simple">
-<li><p>Pin the <strong>exact commit</strong> the numbers came from, not a 
branch name.</p></li>
-<li><p>Report the <strong>AQE-on</strong> number (<code class="docutils 
literal notranslate"><span 
class="pre">ballista.planner.adaptive.enabled=true</span></code>) — the
-configuration this page measures — from the same commit and cluster.</p></li>
-<li><p>Prefer the figure from a <strong>full suite run</strong>, not a 
standalone single-query run: a
-long-lived executor deep into a suite is not in the same state as a freshly 
started
-one, and a single-query job pays a cold-cache penalty that is large enough to 
swamp
-the effect being measured. When a query cannot complete in-suite (e.g. Q18’s 
OOM
-currently wedges the run), exclude it with <code class="docutils literal 
notranslate"><span class="pre">--skip</span> <span 
class="pre">&lt;n&gt;</span></code> so the rest of the column
-still comes from one continuous run, and measure that query 
separately.</p></li>
-<li><p>Note the <strong>row count</strong> each query returned. A fast wrong 
answer is not a result,
-and distributed execution has produced silently wrong row counts 
before.</p></li>
-<li><p>Flag any stage whose runtime is dominated by a few partitions — that is 
the
-imbalance this page exists to surface.</p></li>
-</ul>
+<p>Row counts agree across engines for every query. Ballista completed the
+full 22-query suite in a single continuous run.</p>
 </section>
+<section id="reproducing">
+<h2>Reproducing<a class="headerlink" href="#reproducing" title="Link to this 
heading">#</a></h2>
+<section id="ballista">
+<h3>Ballista<a class="headerlink" href="#ballista" title="Link to this 
heading">#</a></h3>
+<p>Bring up the cluster (one scheduler, N executors), then run the suite from a
+client. Executor sizing on each node:</p>
+<div class="highlight-sh notranslate"><div 
class="highlight"><pre><span></span>ballista-executor<span class="w"> 
</span><span class="se">\</span>
+<span class="w">  </span>--bind-host<span class="w"> </span><span 
class="m">0</span>.0.0.0<span class="w"> </span>--bind-port<span class="w"> 
</span><span class="m">50051</span><span class="w"> </span><span 
class="se">\</span>
+<span class="w">  </span>--scheduler-host<span class="w"> 
</span>&lt;scheduler&gt;<span class="w"> </span>--scheduler-port<span 
class="w"> </span><span class="m">50050</span><span class="w"> </span><span 
class="se">\</span>
+<span class="w">  </span>--concurrent-tasks<span class="w"> </span><span 
class="m">8</span><span class="w"> </span><span class="se">\</span>
+<span class="w">  </span>--memory-pool-size<span class="w"> </span><span 
class="m">48103633715</span>
+</pre></div>
+</div>
+<p>Run all 22 queries:</p>
+<div class="highlight-sh notranslate"><div 
class="highlight"><pre><span></span>tpch<span class="w"> </span>benchmark<span 
class="w"> </span>ballista<span class="w"> </span><span class="se">\</span>
+<span class="w">  </span>--host<span class="w"> </span>&lt;scheduler&gt;<span 
class="w"> </span>--port<span class="w"> </span><span 
class="m">50050</span><span class="w"> </span><span class="se">\</span>
+<span class="w">  </span>--path<span class="w"> 
</span>s3://&lt;bucket&gt;/tpch/sf1000<span class="w"> </span>--format<span 
class="w"> </span>parquet<span class="w"> </span><span class="se">\</span>
+<span class="w">  </span>--partitions<span class="w"> </span><span 
class="m">256</span><span class="w"> </span>--iterations<span class="w"> 
</span><span class="m">2</span><span class="w"> </span><span class="se">\</span>
+<span class="w">  </span>-c<span class="w"> 
</span>ballista.planner.adaptive.enabled<span class="o">=</span><span 
class="nb">true</span><span class="w"> </span><span class="se">\</span>
+<span class="w">  </span>-c<span class="w"> 
</span>datafusion.execution.collect_statistics<span class="o">=</span><span 
class="nb">true</span><span class="w"> </span><span class="se">\</span>
+<span class="w">  </span>-c<span class="w"> 
</span>ballista.shuffle.sort_based.memory_limit_per_task_bytes<span 
class="o">=</span><span class="m">0</span>
+</pre></div>
+</div>
 </section>
-<section id="known-issues-found-by-benchmarking">
-<h2>Known issues found by benchmarking<a class="headerlink" 
href="#known-issues-found-by-benchmarking" title="Link to this 
heading">#</a></h2>
-<p>Benchmarking at SF1000 is how most of the following were found. They are 
worth
-knowing about before interpreting a number:</p>
-<div class="pst-scrollable-table-container"><table class="table">
-<thead>
-<tr class="row-odd"><th class="head"><p>Issue</p></th>
-<th class="head"><p>Summary</p></th>
-</tr>
-</thead>
-<tbody>
-<tr class="row-even"><td><p><a class="reference external" 
href="https://github.com/apache/datafusion-ballista/issues/1359";>#1359</a></p></td>
-<td><p>Umbrella issue for adaptive (AQE) query execution.</p></td>
-</tr>
-<tr class="row-odd"><td><p><a class="reference external" 
href="https://github.com/apache/datafusion-ballista/issues/2025";>#2025</a></p></td>
-<td><p>Q18’s hash-join build side exhausts the memory pool at SF1000. 
DataFusion’s hash-join build side does not spill, so a per-partition build side 
larger than one task slot’s pool fails the task outright.</p></td>
-</tr>
-<tr class="row-even"><td><p><a class="reference external" 
href="https://github.com/apache/datafusion-ballista/issues/2063";>#2063</a></p></td>
-<td><p>AQE can hang when a re-plan cancels an in-flight stage; the job never 
reports completion.</p></td>
-</tr>
-</tbody>
-</table>
+<section id="spark">
+<h3>Spark<a class="headerlink" href="#spark" title="Link to this 
heading">#</a></h3>
+<p>Runs the same queries via <code class="docutils literal notranslate"><span 
class="pre">tpcbench.py</span></code> from
+<a class="reference external" 
href="https://github.com/apache/datafusion-benchmarks";>apache/datafusion-benchmarks</a>,
+with the highlights above and stock Spark 3.4 defaults for everything else:</p>
+<div class="highlight-sh notranslate"><div 
class="highlight"><pre><span></span>spark-submit<span class="w"> </span><span 
class="se">\</span>
+<span class="w">  </span>--master<span class="w"> </span>&lt;master&gt;<span 
class="w"> </span><span class="se">\</span>
+<span class="w">  </span>--conf<span class="w"> 
</span>spark.executor.instances<span class="o">=</span><span 
class="m">32</span><span class="w"> </span><span class="se">\</span>
+<span class="w">  </span>--conf<span class="w"> 
</span>spark.executor.cores<span class="o">=</span><span 
class="m">16</span><span class="w"> </span><span class="se">\</span>
+<span class="w">  </span>--conf<span class="w"> 
</span>spark.executor.memory<span class="o">=</span>64G<span class="w"> 
</span><span class="se">\</span>
+<span class="w">  </span>--conf<span class="w"> 
</span>spark.executor.memoryOverhead<span class="o">=</span>10G<span class="w"> 
</span><span class="se">\</span>
+<span class="w">  </span>--conf<span class="w"> 
</span>spark.sql.shuffle.partitions<span class="o">=</span><span 
class="m">512</span><span class="w"> </span><span class="se">\</span>
+<span class="w">  </span>tpcbench.py<span class="w"> </span><span 
class="se">\</span>
+<span class="w">    </span>--benchmark<span class="w"> </span>tpch<span 
class="w"> </span><span class="se">\</span>
+<span class="w">    </span>--data<span class="w"> 
</span>s3a://&lt;bucket&gt;/tpch/sf1000<span class="w"> </span><span 
class="se">\</span>
+<span class="w">    </span>--format<span class="w"> </span>parquet<span 
class="w"> </span><span class="se">\</span>
+<span class="w">    </span>--iterations<span class="w"> </span><span 
class="m">3</span>
+</pre></div>
 </div>
 </section>
+</section>
+<section id="recording-a-new-result-set">
+<h2>Recording a new result set<a class="headerlink" 
href="#recording-a-new-result-set" title="Link to this heading">#</a></h2>
+<ul class="simple">
+<li><p>Pin the <strong>exact commit</strong> the numbers came from, not a 
branch name.</p></li>
+<li><p>Replace the version, environment, config, and results tables together — 
a
+row that mixes numbers from different commits silently misattributes a
+regression.</p></li>
+<li><p>Prefer a single continuous suite run: a long-lived executor deep into a
+suite is not in the same state as a freshly started one.</p></li>
+<li><p>Report <code class="docutils literal notranslate"><span 
class="pre">FAIL</span></code> for a query that ran but did not produce an 
answer, and
+<code class="docutils literal notranslate"><span class="pre">OOM</span></code> 
when the failure is a known memory exhaustion. See the tracker for
+open issues found by benchmarking: <a class="reference external" 
href="https://github.com/apache/datafusion-ballista/issues/1359";>#1359</a>,
+<a class="reference external" 
href="https://github.com/apache/datafusion-ballista/issues/2025";>#2025</a>, <a 
class="reference external" 
href="https://github.com/apache/datafusion-ballista/issues/2063";>#2063</a>.</p></li>
+</ul>
+</section>
 </section>
 
 
@@ -952,22 +747,18 @@ knowing about before interpreting a number:</p>
   </div>
   <nav id="pst-page-toc-nav" class="page-toc" 
aria-labelledby="pst-page-navigation-heading-2">
     <ul class="pst-show_toc_level nav section-nav flex-column">
-<li class="toc-h2 nav-item toc-entry"><a class="reference internal nav-link" 
href="#what-we-measure-and-why">What we measure, and why</a></li>
+<li class="toc-h2 nav-item toc-entry"><a class="reference internal nav-link" 
href="#versions-under-test">Versions under test</a></li>
 <li class="toc-h2 nav-item toc-entry"><a class="reference internal nav-link" 
href="#environment">Environment</a></li>
-<li class="toc-h2 nav-item toc-entry"><a class="reference internal nav-link" 
href="#reference-cluster">Reference cluster</a></li>
+<li class="toc-h2 nav-item toc-entry"><a class="reference internal nav-link" 
href="#ballista-configuration">Ballista configuration</a></li>
+<li class="toc-h2 nav-item toc-entry"><a class="reference internal nav-link" 
href="#spark-configuration-highlights">Spark configuration (highlights)</a></li>
 <li class="toc-h2 nav-item toc-entry"><a class="reference internal nav-link" 
href="#queries">Queries</a></li>
-<li class="toc-h2 nav-item toc-entry"><a class="reference internal nav-link" 
href="#running-the-benchmark">Running the benchmark</a><ul class="nav 
section-nav flex-column">
+<li class="toc-h2 nav-item toc-entry"><a class="reference internal nav-link" 
href="#results">Results</a></li>
+<li class="toc-h2 nav-item toc-entry"><a class="reference internal nav-link" 
href="#reproducing">Reproducing</a><ul class="nav section-nav flex-column">
 <li class="toc-h3 nav-item toc-entry"><a class="reference internal nav-link" 
href="#ballista">Ballista</a></li>
 <li class="toc-h3 nav-item toc-entry"><a class="reference internal nav-link" 
href="#spark">Spark</a></li>
-<li class="toc-h3 nav-item toc-entry"><a class="reference internal nav-link" 
href="#comet">Comet</a></li>
-</ul>
-</li>
-<li class="toc-h2 nav-item toc-entry"><a class="reference internal nav-link" 
href="#why-compare-against-comet">Why compare against Comet</a></li>
-<li class="toc-h2 nav-item toc-entry"><a class="reference internal nav-link" 
href="#results">Results</a><ul class="nav section-nav flex-column">
-<li class="toc-h3 nav-item toc-entry"><a class="reference internal nav-link" 
href="#recording-a-result">Recording a result</a></li>
 </ul>
 </li>
-<li class="toc-h2 nav-item toc-entry"><a class="reference internal nav-link" 
href="#known-issues-found-by-benchmarking">Known issues found by 
benchmarking</a></li>
+<li class="toc-h2 nav-item toc-entry"><a class="reference internal nav-link" 
href="#recording-a-new-result-set">Recording a new result set</a></li>
 </ul>
   </nav></div>
 
diff --git a/searchindex.js b/searchindex.js
index 9c8447e7b..188213351 100644
--- a/searchindex.js
+++ b/searchindex.js
@@ -1 +1 @@
-Search.setIndex({"alltitles":{"AQE join behavior 
(opt-in)":[[27,"aqe-join-behavior-opt-in"]],"Adaptive Query Execution 
(AQE)":[[21,"adaptive-query-execution-aqe"]],"Adaptive Query Execution 
(Experimental)":[[50,"adaptive-query-execution-experimental"]],"Adding a 
persona":[[25,"adding-a-persona"]],"Aggregate 
Functions":[[49,"aggregate-functions"]],"Apache DataFusion 
Ballista":[[26,null]],"Apache DataFusion Ballista 0.10.0 
Changelog":[[0,null]],"Apache DataFusion Ballista 0.11.0 Changelog" [...]
\ No newline at end of file
+Search.setIndex({"alltitles":{"AQE join behavior 
(opt-in)":[[27,"aqe-join-behavior-opt-in"]],"Adaptive Query Execution 
(AQE)":[[21,"adaptive-query-execution-aqe"]],"Adaptive Query Execution 
(Experimental)":[[50,"adaptive-query-execution-experimental"]],"Adding a 
persona":[[25,"adding-a-persona"]],"Aggregate 
Functions":[[49,"aggregate-functions"]],"Apache DataFusion 
Ballista":[[26,null]],"Apache DataFusion Ballista 0.10.0 
Changelog":[[0,null]],"Apache DataFusion Ballista 0.11.0 Changelog" [...]
\ No newline at end of file


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


Reply via email to