Skip to content

Commit 3694485

Browse files
Dandandanclaude
andcommitted
ci: trim the benchmark comments and README
Most of the prose was two or three times longer than the point it made. Cut to a sentence or two each, keeping the facts that are not visible from the code: why the sides are interleaved, the three bars a query clears to fail the gate, and that fork pull requests cannot reach the larger runner. 107 lines lighter, with no behaviour change -- `compare.py` gives the same verdict on the last run's rounds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 5eef2cd commit 3694485

3 files changed

Lines changed: 107 additions & 214 deletions

File tree

‎.github/workflows/benchmark.yml‎

Lines changed: 37 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -15,40 +15,25 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18-
# Catches performance regressions by running TPC-H SF10 twice -- once for the
19-
# candidate commit (`head`) and once for the commit it sits on (`base`) -- then
20-
# failing when `head` is slower than the configured limits allow. SF10 rather
21-
# than SF1 because SF1 queries finish in milliseconds, where runner noise is a
22-
# large fraction of the measurement.
23-
#
24-
# The two binaries are built by two jobs, on a runner each, and both are then
25-
# measured by a third job on one machine: building is embarrassingly parallel,
26-
# while comparing timings taken on different machines is meaningless.
27-
#
28-
# The two sides are measured interleaved -- a full pass of one, then a full
29-
# pass of the other, several times, alternating which goes first -- rather than
30-
# all of one side and then all of the other. Measuring in two blocks makes any
31-
# drift between them look exactly like a code change: a runner that gets slower
32-
# halfway through, or a process that happened to get an unlucky heap, shifts one
33-
# side only. Interleaving spreads that over both sides and turns it into
34-
# round-to-round spread, which `compare.py` can see and discount.
18+
# Catches performance regressions: runs TPC-H SF10 for a candidate commit
19+
# (`head`) and for the commit it sits on (`base`), on one machine, and fails
20+
# when `head` is slower than the limits allow. The sides are measured
21+
# interleaved, a pass each per round, because measuring one after the other
22+
# lets machine drift read as a code change.
3523

3624
name: Benchmarks
3725

3826
concurrency:
3927
group: ${{ github.repository }}-${{ github.head_ref || github.sha }}-${{ github.workflow }}
4028
cancel-in-progress: true
4129

42-
# On `main` this runs on every push, so each merge is measured against the
43-
# commit it landed on and a regression that no PR run caught is still pinned to
44-
# one merge. On a PR it is opt-in, because the builds and the benchmark runs add
45-
# up to roughly half an hour of runner time: add the `performance` label, or
46-
# start it by hand from the Actions tab.
30+
# Every push to `main`, so an unlabelled regression is still pinned to one
31+
# merge. Opt-in on a PR, at half an hour of runner time: add the `performance`
32+
# label, or start it from the Actions tab.
4733
on:
4834
push:
4935
branches:
50-
# The default branch upstream. A fork that calls it something else has to
51-
# add that name here for its own merges to be measured.
36+
# A fork whose default branch is named differently has to add it here.
5237
- main
5338
paths-ignore:
5439
- "docs/**"
@@ -98,28 +83,21 @@ permissions:
9883
contents: read
9984

10085
env:
101-
# `release-nonlto` is `release` with `lto = false` and 16 codegen units. Fat
102-
# LTO with a single codegen unit roughly doubles the build, and both sides are
103-
# built identically, so the ratio the gate looks at still holds. Dispatch with
104-
# `release` when a change is expected to interact with cross-crate inlining,
105-
# or to get numbers comparable with locally posted `bench.sh` results.
86+
# `release-nonlto` is `release` without fat LTO, which roughly halves the
87+
# build. Dispatch with `release` for cross-crate inlining effects, or for
88+
# numbers comparable with locally posted `bench.sh` results.
10689
CARGO_PROFILE: ${{ inputs.profile || 'release-nonlto' }}
10790
SCALE_FACTOR: ${{ inputs.scale_factor || '10' }}
108-
# Even, for two reasons: each side then leads the same number of rounds, and
109-
# the median of an even number of ratios averages the two middle rounds
110-
# instead of resting on one.
91+
# Even, so each side leads the same number of rounds.
11192
ROUNDS: ${{ inputs.rounds || '6' }}
11293
ITERATIONS: ${{ inputs.iterations || '1' }}
11394
QUERY_REGRESSION: ${{ inputs.query_regression || '1.20' }}
11495
TOTAL_REGRESSION: ${{ inputs.total_regression || '1.05' }}
115-
# A 1.20x swing on a query that runs for 20ms is 4ms, which this kind of
116-
# runner cannot resolve. Regressions have to cost real time to count.
96+
# A 1.20x swing on a 20ms query is 4ms, below what a shared runner resolves.
11797
MIN_DELTA_MS: ${{ inputs.min_delta_ms || '25' }}
11898
# `benchmark_runner` finds `sql_benchmarks` through the CARGO_MANIFEST_DIR
119-
# baked into it at compile time, so a binary built in one job only works in
120-
# another if its tree sits at the same absolute path there. Every job below
121-
# puts the two trees under this root, which is why it is a fixed path rather
122-
# than something derived from the workspace or the runner.
99+
# baked in at compile time, so its tree has to sit at the same absolute path
100+
# in the job that builds it and the job that runs it -- hence a fixed root.
123101
BENCH_ROOT: /tmp/df-bench
124102
# Same cargo network settings as .github/actions/setup-rust-runtime, without
125103
# its RUSTFLAGS: benchmark binaries are built with the defaults.
@@ -141,9 +119,7 @@ jobs:
141119
- uses: runs-on/action@46910bf61b41721b0579f237e186afb35477007a # v2.3.0
142120
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
143121
with:
144-
# For `pull_request` this is the PR already merged into the base
145-
# branch, for `push` the new branch tip; depth 2 is enough to also
146-
# reach the first parent in both cases.
122+
# Depth 2 reaches the first parent, which is the base in both cases.
147123
fetch-depth: 2
148124
- name: Resolve base commit
149125
id: base
@@ -152,16 +128,13 @@ jobs:
152128
run: |
153129
set -euo pipefail
154130
if [ "$GITHUB_EVENT_NAME" = "push" ]; then
155-
# A merge that just landed on the default branch. Its first parent
156-
# is the branch as it was before, whether the merge was squashed
157-
# into one commit or kept as a merge commit, so the difference is
158-
# attributable to this one merge.
131+
# A merge that just landed: its first parent is the branch as it
132+
# was before, whether the merge was squashed or not.
159133
base_sha=$(git rev-parse "HEAD^1")
160134
echo "push to $GITHUB_REF_NAME, comparing its tip against the parent"
161135
elif [ "$(git rev-list --parents -n 1 HEAD | wc -w)" -ge 3 ]; then
162136
# HEAD is the PR merged into the base branch, so its first parent
163-
# is the base branch tip that the merge used -- the commit this PR
164-
# would actually land on.
137+
# is the commit this PR would land on.
165138
base_sha=$(git rev-parse "HEAD^1")
166139
else
167140
# Manual run on a branch: compare it as-is against the base tip.
@@ -190,10 +163,8 @@ jobs:
190163
with:
191164
fetch-depth: 2
192165
- name: Free Disk Space (Ubuntu)
193-
# Two minutes of deleting Android SDKs, which a release build of the
194-
# workspace genuinely needs on `ubuntu-latest`'s 14GB. The RunsOn
195-
# runner above asks for `disk=large` and has no such problem, so only
196-
# the fallback pays for this.
166+
# A release build needs this on `ubuntu-latest`'s 14GB, but not on the
167+
# RunsOn runner's `disk=large`, where it is two wasted minutes.
197168
if: vars.USE_RUNS_ON != 'true'
198169
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
199170
- name: Install Rust
@@ -216,15 +187,8 @@ jobs:
216187
git worktree add --detach "$BENCH_ROOT/$SIDE" HEAD
217188
fi
218189
- name: Cache the dependency build
219-
# A cold build of this is fourteen minutes, most of it dependencies
220-
# that neither side changed. The action drops workspace crates from
221-
# what it saves, so a hit rebuilds DataFusion and reuses the rest.
222-
#
223-
# `workspaces` because the build happens in a worktree outside the
224-
# checkout, and one shared key for both sides because they are a commit
225-
# or two apart and share a dependency graph. Only a push to `main`
226-
# writes the cache: pull request runs would each save a near-identical
227-
# copy of it.
190+
# A cold build is fourteen minutes, mostly dependencies neither side
191+
# changed. One shared key covers both sides; only pushes write it.
228192
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
229193
with:
230194
workspaces: ${{ env.BENCH_ROOT }}/${{ matrix.side }}
@@ -244,7 +208,7 @@ jobs:
244208
path: ${{ runner.temp }}/benchmark_runner
245209
retention-days: 1
246210

247-
# Both sides are measured here, on this one machine, back to back.
211+
# Both sides are measured here, interleaved, on this one machine.
248212
benchmark:
249213
name: TPC-H (head vs base)
250214
needs: [resolve, build]
@@ -280,8 +244,7 @@ jobs:
280244
- name: Check both runners see their queries
281245
run: |
282246
# Artifacts do not carry the executable bit, and a binary whose tree
283-
# is missing at the path baked into it would discover no benchmark at
284-
# all -- fail here rather than three steps later.
247+
# is missing would discover no benchmark at all -- fail here.
285248
for side in base head; do
286249
binary="$RUNNER_TEMP/bin/$side/benchmark_runner"
287250
chmod +x "$binary"
@@ -295,16 +258,9 @@ jobs:
295258
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
296259
- name: Generate TPC-H data
297260
# Same generator settings as `bench.sh data tpch`, inlined because that
298-
# path also downloads the expected answers through `docker run -it`,
299-
# which needs a TTY and is only used for result validation.
300-
#
301-
# tpchgen-cli comes from its PyPI wheel: the same 3.0.0 release as the
302-
# crate, but a ~4MB download instead of a build from source, since the
303-
# project attaches no binaries to its GitHub releases for a prebuilt
304-
# fetch to find. It also means this job needs no Rust toolchain at all.
305-
#
306-
# `parquet` as a subcommand rather than `--format parquet`: 3.0.0
307-
# deprecated the flag form and warns that it goes away in 4.0.0.
261+
# path also pulls the expected answers through `docker run -it`, which
262+
# has no TTY here. From the PyPI wheel, so no Rust toolchain is needed;
263+
# `parquet` is a subcommand because 3.0.0 deprecated `--format`.
308264
run: |
309265
mkdir -p "$DATA_DIR/tpch_sf$SCALE_FACTOR" "$RESULTS_DIR/base" "$RESULTS_DIR/head"
310266
uv tool run --from 'tpchgen-cli==3.0.0' tpchgen-cli parquet \
@@ -315,20 +271,15 @@ jobs:
315271
du -sh "$DATA_DIR/tpch_sf$SCALE_FACTOR"
316272
df -h "$DATA_DIR"
317273
- name: Describe the machine
318-
# A comparison is only as good as the machine under it, and this does
319-
# not always land on the machine the `runs-on` line above asks for.
320-
# GitHub withholds `vars` from workflows triggered by a pull request
321-
# from a fork, so `vars.USE_RUNS_ON` reads as empty there however it is
322-
# set on the repository, and every job falls back to a shared 4-vCPU
323-
# `ubuntu-latest` -- which is where this workflow's own false Q02
324-
# regressions were measured. Pushes to `main` and manual dispatches run
325-
# in the repository's own context and do get the 16-vCPU runner.
274+
# GitHub withholds `vars` from fork pull requests, so `USE_RUNS_ON`
275+
# reads as empty and they fall back to a 4-vCPU `ubuntu-latest`. Pushes
276+
# to `main` and manual dispatches do get the 16-vCPU runner.
326277
run: |
327278
echo "cpus: $(nproc)"
328279
lscpu | grep -E '^(Model name|Socket|Core|Thread|CPU\(s\)):' || true
329280
free -h
330281
if [ "$(nproc)" -lt 8 ]; then
331-
echo "::warning::running on $(nproc) CPUs, not the runner this workflow asks for; expect a high noise floor. Fork pull requests cannot reach the larger runner -- the run on \`main\` after the merge is the authoritative one."
282+
echo "::warning::running on $(nproc) CPUs, not the runner asked for; expect a high noise floor. Fork pull requests cannot reach the larger one -- the run on \`main\` after the merge is authoritative."
332283
fi
333284
# Each side runs from its own tree and reads the one generated dataset.
334285
- name: Benchmark both sides, interleaved
@@ -344,18 +295,14 @@ jobs:
344295
--path "$DATA_DIR" \
345296
--output "$output"
346297
}
347-
# Read the data once first, so no measured round is the only one
348-
# paying to pull the parquet files into the page cache. Reading the
349-
# files is all a warmup can carry between rounds -- each round is a
350-
# fresh process -- and it takes seconds where a discarded pass of the
351-
# suite took as long as a round.
298+
# Pull the data into the page cache, which is all a warmup can carry
299+
# between rounds -- each round is a fresh process.
352300
echo "::group::warm the page cache"
353301
find "$DATA_DIR" -type f -exec cat {} + > /dev/null
354302
echo "::endgroup::"
355303
for round in $(seq 1 "$ROUNDS"); do
356-
# Alternate which side goes first, so with an even ROUNDS
357-
# whatever the first position costs -- or saves -- is paid by each
358-
# side the same number of times.
304+
# Alternate the leader, so each side pays the first-position cost
305+
# the same number of times.
359306
if [ $((round % 2)) -eq 1 ]; then order="base head"; else order="head base"; fi
360307
for side in $order; do
361308
echo "::group::round $round: $side"

0 commit comments

Comments
 (0)