Skip to content

ci: split benchmark build (Blacksmith ARM64) from CodSpeed walltime run - #3601

Merged
hellovai merged 8 commits into
canaryfrom
hellovai/fix-benchmarks
May 30, 2026
Merged

ci: split benchmark build (Blacksmith ARM64) from CodSpeed walltime run#3601
hellovai merged 8 commits into
canaryfrom
hellovai/fix-benchmarks

Conversation

@hellovai

@hellovai hellovai commented May 29, 2026

Copy link
Copy Markdown
Contributor

What

Splits the benchmark CI into two jobs so the slow compile and the measurement happen on the right machines:

Job Runner Does
benchmarks-build blacksmith-8vcpu-ubuntu-2204-arm cargo codspeed build -p baml_tests -m walltime → uploads prebuilt binaries
benchmarks-run codspeed-macro downloads them, cargo codspeed run -m walltime via CodSpeedHQ/action@v4

Why

The previous single job built and ran on one small runner, and cargo codspeed build was timing out.

Key correctness points

  • No flag mismatch / double-build: cargo codspeed run only executes the prebuilt binaries (verified against the cargo-codspeed source) — it never invokes the compiler. If the artifact is missing it errors loudly instead of measuring the wrong binary.
  • ABI match: codspeed-macro is ARM64, so we build on ARM64 too, on Ubuntu 22.04 (older glibc) so binaries stay forward-compatible with whatever glibc the macro runner ships.
  • chmod +x after download — upload-artifact strips the executable bit.
  • cargo-codspeed@4.7.0 pinned to match codspeed-divan-compat.
  • continue-on-error on the run + kept out of the ci-failure-alert gate, so a measurement blip never blocks a merge.

Caching

Dedicated bench-only linux-arm-bench lane (canary-only saver, PRs read-only). It can't reuse the x86 linux-cargo lane (rustc host triple is baked into the key) and is too thin a seed (only baml_tests benches) to share with other ARM jobs. Documented in the saver/reader map.

Triggers

Runs automatically on any change under baml_language/ (determine_changes.code), plus push to main/canary and RUN_CODSPEED=1 opt-in for unrelated PRs.

⚠️ To verify by running this PR

  1. codspeed-macro is enabled for the org (separate CodSpeed feature).
  2. blacksmith-8vcpu-ubuntu-2204-arm is provisioned — only 4vcpu-arm is attested in the repo today; drop to 4vcpu if 8 isn't available.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores
    • Updated cache docs to describe a new ARM bench cache entry and its bench-specific refresh/aging behavior.
  • New Features
    • Added an automated two-phase ARM64 benchmarking pipeline for the BAML language: separate build and run stages, artifact handoff, and optional CodSpeed-based performance runs.
  • Tests
    • Refactored runtime benchmarks to use a shared helper for consistent, focused VM performance measurements.

Review Change Stack

The single `benchmarks-instrumented` job built and ran on one small runner,
and the `cargo codspeed build` step was timing out. Split it in two:

- `benchmarks-build` (blacksmith-8vcpu-ubuntu-2204-arm): `cargo codspeed
  build -p baml_tests -m walltime`, uploads the prebuilt binaries.
- `benchmarks-run` (codspeed-macro): downloads them and `cargo codspeed run`.

`cargo codspeed run` only executes the prebuilt binaries (it never invokes
the compiler), so the run job can't accidentally rebuild with default flags.
Build is on ARM64 to match the codspeed-macro arch, on Ubuntu 22.04 (older
glibc) for forward-compatibility, and restores the executable bit that
upload-artifact strips.

Caching uses a dedicated, bench-only `linux-arm-bench` lane (canary-only
saver) — it can't reuse the x86 `linux-cargo` lane (rustc host triple is in
the key) and is too thin a seed to share with other ARM jobs.

Runs automatically on any change under baml_language/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented May 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment May 30, 2026 6:40am
promptfiddle Ready Ready Preview, Comment May 30, 2026 6:40am
promptfiddle2 Ready Ready Preview, Comment May 30, 2026 6:40am

Request Review

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a two-phase CodSpeed bench pipeline (ARM64 prebuild + run), documents a linux-arm-bench cache lane, adds bench_vm_main, and refactors multiple VM benchmarks to use that helper.

Changes

CodSpeed benchmarking with ARM64 build artifacts

Layer / File(s) Summary
Cache strategy documentation for ARM benchmarks
.github/workflows/cargo-tests.reusable.yaml
Expanded the inline cache strategy header comment to document a new linux-arm-bench shared-key entry and its bench-only seeding behavior, excluding it from the daily canary cache purge.
CodSpeed benchmarking build and run jobs
.github/workflows/ci.yaml
Added benchmarks-build (ARM64 build with cargo-codspeed@4.7.0, bench-specific Rust cache key, build walltime benchmarks, upload short-retention artifact) and benchmarks-run (depends on build, download artifacts, restore permissions, run CodSpeed walltime subset with continue-on-error), wired to existing gating and opt-in.
Benchmark helper: bench_vm_main
baml_language/crates/baml_tests/benches/runtime_benchmark.rs
Adds bench_vm_main to compile BAML and create a Tokio runtime and BexEngine outside the timed region, timing only engine.call_function("main", ...).
Refactor benchmarks to use bench_vm_main
baml_language/crates/baml_tests/benches/runtime_benchmark.rs
Refactors multiple #[divan::bench] VM benchmarks (fib, loops, string concat, arrays, classes, call-chains, closures, etc.) to delegate to bench_vm_main with inlined or generated BAML sources.

Sequence Diagram(s)

sequenceDiagram
  participant WorkflowTrigger as Workflow Trigger
  participant BuildJob as benchmarks-build (ARM64 runner)
  participant ArtifactStorage as Artifact Storage
  participant RunJob as benchmarks-run
  participant CodSpeedAction as CodSpeedHQ/action

  WorkflowTrigger->>BuildJob: trigger (baml_language/** changes or RUN_CODSPEED=1)
  BuildJob->>BuildJob: install Rust + cargo-codspeed@4.7.0
  BuildJob->>BuildJob: cargo codspeed build -p baml_tests --bench runtime_benchmark -m walltime
  BuildJob->>ArtifactStorage: upload walltime binaries
  ArtifactStorage->>RunJob: download prebuilt walltime binaries
  RunJob->>RunJob: restore executable permissions
  RunJob->>CodSpeedAction: cargo codspeed run -m walltime 'vm_|engine_init' (continue-on-error)
  CodSpeedAction-->>RunJob: benchmark results
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • BoundaryML/baml#3575: Overlaps in CodSpeed benchmarking workflow changes (removal/adjustment of prior benchmark jobs).
  • BoundaryML/baml#3498: Related to cache strategy and canary cache purge behavior for ARM benchmark cache keys.

Poem

🐰 I hopped through CI with curious cheer,
Built ARM binaries for benches near,
Artifacts packed, then fetched to run,
Timed main calls hum — a tiny drum,
A rabbit cheers for measured fun!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title concisely and accurately describes the main change: splitting benchmark CI into separate build and run jobs on appropriate hardware (ARM64 for build, CodSpeed macro for run).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hellovai/fix-benchmarks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yaml:
- Around line 495-615: The CI failure-alert job is not depending on the
benchmarks-build job, so a failing benchmarks-build is invisible to the `CI-v2
Failure Alert`; update the workflow so the failure-alert job's needs/fan-in
includes `benchmarks-build` (add the `benchmarks-build` job name to the `needs:`
array that currently feeds `ci-failure-alert`), leaving `benchmarks-run` as
advisory/continue-on-error if you want measurement blips non-blocking; this
ensures `ci-failure-alert` sees benchmark build failures.
- Around line 514-557: Pin the floating action refs to immutable commit SHAs by
replacing uses: entries for actions/checkout@v6, Swatinem/rust-cache@v2,
taiki-e/install-action@v2, actions/upload-artifact@v7,
actions/download-artifact@v7 and CodSpeedHQ/action@v4 with their corresponding
full commit SHAs (locate the uses: lines in the new benchmark job steps such as
the "Checkout Branch", rust-cache, "Install cargo-codspeed", "Upload prebuilt
benchmark binaries" and download/upload steps) and update the workflow so the
job dependency graph includes the benchmark jobs (add the benchmark job names,
e.g. benchmarks-build and benchmarks-run, to the ci-failure-alert.needs array)
so ci-failure-alert will run and reflect benchmark failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 03af2929-6a76-4230-b374-b818248d5866

📥 Commits

Reviewing files that changed from the base of the PR and between ddd0ff1 and 9fa8d07.

📒 Files selected for processing (2)
  • .github/workflows/cargo-tests.reusable.yaml
  • .github/workflows/ci.yaml

Comment thread .github/workflows/ci.yaml
Comment thread .github/workflows/ci.yaml
@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown

Binary size checks passed

7 passed

Artifact Platform File Gzip Gated on Baseline Delta Status
baml-cli Linux 🔒 19.4 MB 8.1 MB file 19.4 MB +9.4 KB (+0.0%) OK
packed-program Linux 🔒 14.7 MB 6.2 MB file 14.7 MB +4.1 KB (+0.0%) OK
baml-cli macOS 🔒 14.6 MB 7.0 MB file 14.6 MB +16.5 KB (+0.1%) OK
packed-program macOS 🔒 11.2 MB 5.4 MB file 11.2 MB +0 B (+0.0%) OK
baml-cli Windows 🔒 15.8 MB 7.1 MB file 15.8 MB +1.0 KB (+0.0%) OK
packed-program Windows 🔒 11.9 MB 5.4 MB file 11.9 MB +1.5 KB (+0.0%) OK
bridge_wasm WASM 12.8 MB 🔒 3.7 MB gzip 3.8 MB -66.9 KB (-1.8%) OK

🔒 = the size this artifact is GATED on (ceiling + delta). Binaries gate on file size (installed binary); WASM gates on gzip (download size). The other size is shown for information only.


Generated by cargo size-gate · workflow run

The vm_* benches recompiled the BAML project on every divan sample (via
with_inputs) and built a fresh tokio runtime inside the measured region on
every call_main(). The recompile burned ~208s of wall-clock per pass (untimed
but real), and the per-call runtime creation polluted the measurement: ~8ms of
each "VM" number was just tokio::Runtime::new(), not VM execution.

BexEngine::call_function takes &Arc<Self> and builds a fresh call context per
call, so one compiled engine safely serves every sample. Factor the vm_* benches
through a bench_vm_main() helper that compiles + builds the runtime once and
measures only the call. Effect (100 samples, local): runtime-only suite 255s ->
36s; e.g. vm_fib_20 9.7ms -> 1.8ms, vm_string_concat_5k 9.5ms -> 0.81ms.

CI (benchmarks-run):
- Filter to `vm_|engine_init` so we measure only runtime, not the compile-bound
  e2e_/compile_to_engine/startup benches.
- Drop the DIVAN_SAMPLE_COUNT band-aid: at ~2 min/pass on the macro runner the
  full 100-sample suite fits the timeout comfortably.
- Build only runtime_benchmark (cache_profile is a macOS/kperf no-op on Linux).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
baml_language/crates/baml_tests/benches/runtime_benchmark.rs (1)

51-60: 💤 Low value

Optional: call_main still spins up a fresh Tokio runtime on every invocation.

For the bench-based callers (startup_empty_expression, e2e_*) this happens inside the measured closure on every sample, so runtime-construction cost is folded into those measurements. If that overhead is intentional for the e2e/startup numbers, ignore this; otherwise consider the same hoisting pattern as bench_vm_main.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/baml_tests/benches/runtime_benchmark.rs` around lines 51
- 60, call_main currently builds a new Tokio runtime for every call (fn
call_main), which makes per-sample benchmarks include runtime construction cost;
instead hoist runtime creation into the benchmark setup and make call_main
accept and use the existing runtime/handle (e.g., pass &tokio::runtime::Runtime
or &tokio::runtime::Handle or an Arc<Runtime>) or use a once-created static
runtime used by bench_vm_main and the e2e/startup benchmarks; update call_main
signature and all callers (including bench_vm_main and e2e_* benches) to reuse
that runtime so samples only measure engine.call_function execution.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@baml_language/crates/baml_tests/benches/runtime_benchmark.rs`:
- Around line 51-60: call_main currently builds a new Tokio runtime for every
call (fn call_main), which makes per-sample benchmarks include runtime
construction cost; instead hoist runtime creation into the benchmark setup and
make call_main accept and use the existing runtime/handle (e.g., pass
&tokio::runtime::Runtime or &tokio::runtime::Handle or an Arc<Runtime>) or use a
once-created static runtime used by bench_vm_main and the e2e/startup
benchmarks; update call_main signature and all callers (including bench_vm_main
and e2e_* benches) to reuse that runtime so samples only measure
engine.call_function execution.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0fbad646-1b69-41e9-91bb-59e2730b4406

📥 Commits

Reviewing files that changed from the base of the PR and between 9fa8d07 and cf3c725.

📒 Files selected for processing (2)
  • .github/workflows/ci.yaml
  • baml_language/crates/baml_tests/benches/runtime_benchmark.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/ci.yaml

@codspeed-hq

codspeed-hq Bot commented May 29, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 52.73%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 11 (👁 11) regressed benchmarks
🆕 1 new benchmark
⏩ 7 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
👁 WallTime vm_array_push_50k 8.9 ms 23.5 ms -61.88%
WallTime vm_string_concat_5k 27.1 ms 4.1 ms ×6.7
👁 WallTime vm_closure_call_50k 8.4 ms 22.7 ms -63.14%
👁 WallTime vm_class_create_50k 12.5 ms 38.9 ms -67.92%
👁 WallTime vm_fib_20 5.7 ms 8.2 ms -30.65%
🆕 WallTime vm_wide_nested_class_create_50k N/A 300.9 ms N/A
👁 WallTime vm_array_iter_10k 5.2 ms 7.8 ms -33.49%
👁 WallTime vm_mixed_ops 5.9 ms 10.7 ms -45.23%
👁 WallTime vm_nested_loop 4.9 ms 9.9 ms -50.11%
👁 WallTime vm_field_access_50k 6.7 ms 17.1 ms -60.69%
👁 WallTime vm_call_chain_100_x_5k 35.1 ms 142.9 ms -75.47%
👁 WallTime vm_loop_500k 21.1 ms 117.7 ms -82.08%
👁 WallTime engine_init_cost 3.3 ms 17.3 ms -81.03%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing hellovai/fix-benchmarks (16475d4) with canary (d393c6f)2

Open in CodSpeed

Footnotes

  1. 7 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on canary (c3b383c) during the generation of this report, so d393c6f was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

The run job's 30-min near-timeout was NOT a rebuild (cargo codspeed run only
executes prebuilt binaries in 4.7.0). It was `cargo codspeed run`'s implicit
`cargo metadata` re-cloning every git dependency (minijinja, the aws-sdk-rust
fork, …) on the bare codspeed-macro runner, which has no cargo cache — ~24 min
of silence before the first bench. Evidence: the build job spent 13.5 min in
`cargo metadata` before its first `Compiling` line, and a cold-CARGO_HOME
`cargo metadata` locally sits on "Updating git repository" for many minutes.

Add a read-only Swatinem/rust-cache restore (shared-key linux-arm-bench, same
aarch64 triple as benchmarks-build) so the run job reuses the cargo home the
build job saves on canary. benchmarks-build stays the sole (canary) saver.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Temporary: flip benchmarks-build save-if to true so the cargo-home cache is
seeded during this PR run and benchmarks-run can restore it in the same
workflow (proving the cargo metadata fix before canary seeds the lane).
Revert save-if to canary-only before merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Swatinem-based restore missed: rust-cache folds a per-runner environment
hash into its key, and `rustup toolchain list` errors on codspeed-macro, so the
build (7aa5a20b) and run (63ff4892) keys never matched — the run job restored
nothing and stalled ~24 min in cold `cargo metadata` again.

Switch to a plain actions/cache keyed on hashFiles(Cargo.lock), byte-identical
on both runners: benchmarks-build saves ~/.cargo/{registry,git}, benchmarks-run
restores it, and CARGO_NET_OFFLINE=true makes `cargo metadata` use only the
local cache (no index update, no git fetch). Revert the temporary Swatinem
save-if hack back to canary-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ers)

actions/cache addresses entries by (key, version); the version depends on the
runner's compression tooling, so the cargo-home cache saved on the blacksmith
build runner is invisible to the codspeed-macro run job even with a byte-
identical key (verified: same key 4f01266e…, "Cache not found"). The offline
guard then correctly failed in 2s instead of stalling 24 min — proving the
diagnosis but not yet the fix.

Switch to the mechanism we already use for the binary: tar ~/.cargo/{registry,
git} into an artifact in benchmarks-build, download + untar it in benchmarks-run.
Artifacts are content-addressed by name and cross runners cleanly. With
CARGO_NET_OFFLINE=true the run job's cargo metadata then resolves entirely from
the local cargo home — no cold git re-clone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hellovai
hellovai enabled auto-merge May 30, 2026 06:18
The run job only does offline cargo metadata (needs the registry index + git
checkouts), so drop registry/src — the extracted dep sources that dominate the
3.4GB artifact — to cut the run job's download/untar setup time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant