1- # Instruction-count (Callgrind) perf-regression gate against a COMMITTED baseline. No CodSpeed account/token/runner:
2- # compare_baseline.py parses raw callgrind dumps and diffs each benchmark against benchmarks/baseline.json. Counts
3- # are near-deterministic with PYTHONHASHSEED pinned (~0.1% noise), so the 5% gate threshold sits far above it.
4- # Details + rationale: benchmarks/README.md and benchmarks/PLAN.md.
5- #
6- # Triggers: nightly schedule + manual workflow_dispatch (no pull_request/push). A dispatch on a feature branch
7- # compares that branch's counts vs the baseline.json committed on it, answering "did my branch regress vs main".
8- #
9- # Modes (workflow_dispatch input `regen`):
10- # regen=false (default) -> COMPARE + report. Report-only for now (never fails); flip to --enforce once trusted.
11- # regen=true -> write a fresh baseline.json + upload as an artifact to commit deliberately. Bump
12- # requirements-bench.txt FIRST (separate commit) if the pins should change.
13- #
14- # The concurrency module is excluded from the sweep (Callgrind serializes threads, so its signal is meaningless).
15- # Memory mode (a second sweep for produce peak-RSS) is deferred (see PLAN.md).
1+ # Instruction-count (Callgrind) perf gate vs a committed benchmarks/baseline.json; no CodSpeed account needed
2+ # (compare_baseline.py parses the raw callgrind dumps). Rationale in benchmarks/README.md and benchmarks/PLAN.md.
3+ # workflow_dispatch input `regen`: false compares (report only), true writes baseline.json and uploads it.
164
175name : Benchmarks
186
@@ -33,24 +21,23 @@ concurrency:
3321jobs :
3422 benchmarks :
3523 runs-on : ubuntu-latest
36- timeout-minutes : 90 # ~25 min sweep at BENCH_SCALE=10 (12-core Linux ) + ~10 min cold build; margin for CI
24+ timeout-minutes : 90 # ~25 min sweep ( BENCH_SCALE=10) + ~10 min cold build, plus margin
3725 permissions :
3826 contents : read
3927 env :
40- PYTHONHASHSEED : " 0" # stable instruction counts for dict/struct paths
41- CODSPEED_ENV : " 1" # activates pytest-codspeed's instrument hooks
42- # shrink the O(rows) benches so the sweep fits under timeout-minutes. Local runs leave this unset -> full N.
43- # Recorded in baseline.json meta.bench_scale; a baseline only compares to a run at the SAME scale.
28+ PYTHONHASHSEED : " 0" # deterministic instruction counts
29+ CODSPEED_ENV : " 1" # enable pytest-codspeed callgrind hooks
30+ # shrink O(rows) benches to fit the timeout; a baseline only compares to a run at the SAME scale (meta.bench_scale)
4431 BENCH_SCALE : " 10"
4532 steps :
4633 - uses : actions/checkout@v4
4734 with :
48- submodules : recursive # the DuckDB engine submodule is needed to build
49- fetch-depth : 0 # setuptools_scm needs history for version detection
35+ submodules : recursive
36+ fetch-depth : 0 # setuptools_scm needs history
5037
5138 - name : Resolve DuckDB submodule SHA
5239 id : duckdb_sha
53- # used for the sccache key AND passed to compare_baseline.py for the engine-bump guard
40+ # sccache key + compare_baseline.py engine-bump guard
5441 run : echo "sha=$(git rev-parse HEAD:external/duckdb)" >> "$GITHUB_OUTPUT"
5542
5643 - name : Install uv
@@ -78,29 +65,29 @@ jobs:
7865 CMAKE_C_COMPILER_LAUNCHER : sccache
7966 CMAKE_CXX_COMPILER_LAUNCHER : sccache
8067 run : |
81- # step 1: build deps only (needed for --no-build-isolation), no project
8268 uv sync --only-group build --no-install-project -p 3.13
83- # step 2: the frozen bench pins (exact ==), so the only cross-run delta is the binding. MUST precede the
84- # build: numpy>=2.0 is a [build-system].requires (numpy C API headers), which --no-build-isolation does
85- # not auto-install and which is not in the `build` group, so CMake's find_package(... NumPy) fails first.
69+ # pins BEFORE the build: numpy>=2.0 is a build-system.requires (C API headers), absent under
70+ # --no-build-isolation and not in the `build` group, so find_package(NumPy) fails otherwise.
8671 uv pip install -r benchmarks/requirements-bench.txt
87- # step 3: build+install the project (release), no default `dev` group (torch/tensorflow/pyspark). uv pip
88- # install is additive; uv sync here would prune numpy back out before the build and re-break the config.
72+ # uv pip is additive; uv sync would prune numpy back out before the build. release, no dev group.
8973 uv pip install --no-build-isolation --no-deps --reinstall -C cmake.build-type=Release .
9074
9175 - name : Collect gate node-ids
92- # the gate/informational marker split; regen uses it to classify each benchmark
93- run : uv run --no-sync pytest benchmarks/ -m gate --collect-only -q -o addopts= -p no:cacheprovider \
94- | grep '::' > gate_list.txt || true
76+ # regen uses these to classify each benchmark. Block scalar so `\` is a real shell continuation; a plain
77+ # scalar folds it to a literal `\ `, mangling pytest args into an empty (all-informational) gate list.
78+ run : |
79+ uv run --no-sync pytest benchmarks/ -m gate --collect-only -q -o addopts= -p no:cacheprovider \
80+ | grep '::' > gate_list.txt || true
81+ # an empty gate list yields a 0-gate baseline that never catches a regression
82+ test -s gate_list.txt || { echo "ERROR: gate_list.txt empty"; exit 1; }
83+ echo "collected $(wc -l < gate_list.txt) gate node-ids"
9584
9685 - name : Run benchmarks under Callgrind (per-benchmark instruction counts)
97- # ONE sweep over gate+informational EXCEPT the concurrency module (thread-serialized, expensive). Each
98- # benchmark emits a callgrind dump keyed by its uri.
86+ # one sweep; concurrency module excluded (Callgrind serializes threads).
9987 run : |
10088 mkdir -p profiles
101- # Invoke the venv python DIRECTLY under valgrind, not `uv run`: uv run forks pytest as a child, and
102- # valgrind without --trace-children only instruments uv, so the child runs un-instrumented (native
103- # speed) and pytest-codspeed's callgrind_dump_stats_at calls no-op -> zero dumps.
89+ # run the venv python directly, NOT `uv run`: valgrind without --trace-children only instruments uv,
90+ # leaving pytest un-instrumented (native speed, zero dumps).
10491 CODSPEED_PROFILE_FOLDER="$PWD/profiles" valgrind --tool=callgrind --instr-atstart=no \
10592 --callgrind-out-file="$PWD/profiles/cg.%p.%n" \
10693 "$VIRTUAL_ENV/bin/python" -m pytest benchmarks/ \
10996
11097 - name : Compare against committed baseline (report-only)
11198 if : ${{ !inputs.regen }}
112- # report-only: prints the delta table , never fails the job. Add --enforce once trusted.
99+ # report only , never fails; add --enforce once trusted
113100 run : |
114101 uv run --no-sync python benchmarks/compare_baseline.py compare \
115102 --profiles profiles --baseline benchmarks/baseline.json \
0 commit comments