Skip to content

perf(engine): cut interpreter hot-path overheads (permit churn, VC-key hashing, futures-map rehash) - #4553

Open
aaronvg wants to merge 1 commit into
canaryfrom
aaron/engine-perf
Open

perf(engine): cut interpreter hot-path overheads (permit churn, VC-key hashing, futures-map rehash)#4553
aaronvg wants to merge 1 commit into
canaryfrom
aaron/engine-perf

Conversation

@aaronvg

@aaronvg aaronvg commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Issue Reference

Changes

Profiling a release-grade corpus run (3,448 tests, sample-based, thread-state census + symbolized frame buckets) showed the engine averaging ~1 core on a 12-core machine, with three concrete taxes. This PR removes them; all changes are behavior-preserving.

1. Heap-permit fast path (bex_heap). Every VM task resume paid a full fair-queue acquire on the global heap-permit semaphore — 17 of 39 sampled threads sat inside InactiveHeapPermit::acquire / batch_semaphore::poll machinery. acquire now tries try_acquire_owned first. Fairness only matters once a GC park's acquire_many(MAX_PERMITS) is pending — and in exactly that case zero permits are available, the try fails, and acquisition falls through to the fair wait, so GC parks cannot be starved by the bypass.

2. Primitive receiver keys for the virtual-call cache (bex_vm). Every virtual call on a primitive receiver — string/int methods, the hottest calls in any workload — built a RealizedTy and deep-hashed it under SipHash to key the static virtual-call cache (in an earlier debug-profile these hash/clone frames were ~65% of VM samples). StaticVirtualReceiverKey gains a Primitive(u8) variant: bare discriminants for the argument-less primitives, nothing built or hashed. Receivers whose identity carries type arguments (instances, containers, enums) keep the structural keys. The cache also switches SipHash → FxHash (in-process cache keyed by program structure; DoS resistance irrelevant).

3. Futures-map presizing (bex_engine). The FutureManager's active_futures map rehashed under the manager's single mutex mid-run — sampled as one thread in hashbrown::reserve_rehash while every other worker parked. Pre-sized to 4096 so corpus-scale future churn never rehashes inside the lock.

Measurements

Back-to-back A/B on the full corpus, same machine, fasttest profile (release-grade codegen, debug assertions on):

wall user CPU sys CPU total CPU
canary 69.0s 71.2s 17.0s 88.2s
this PR 67.1s 62.2s 1.9s 64.1s

Total CPU −27%; sys time −89% (the semaphore's kernel wake/park traffic — the permit fast path); user time −13% (hashing). Wall moves little because the corpus floor is timer-bound (sleep- and timeout-shaped tests), not CPU-bound.

Testing

  • cargo nextest run -p bex_vm -p bex_heap -p bex_engine — 580/580
  • Full corpus via baml-cli test — 3,447/3,448 (the one failure is the known contention-flaky vertex_stream_time_to_first_token_timeout_ms, which fails identically on baseline runs)
  • clippy/fmt clean on the three crates
  • Manual: profiled before/after; the permit-acquire and rehash frames are gone from the after-sample

Reviewer notes — what this deliberately does NOT do

The remaining parallelism ceiling is structural, and these are documented for follow-up rather than attempted here:

  • FutureManager sharding: one tokio::Mutex still guards the futures map + TLAB; every spawn/settle serializes through it. Sharding or a lock-free map is the next real win.
  • Permit-holding across resumes: resumes still round-trip the (now cheap) semaphore; holding permits across iterations needs care against the GC-park protocol.
  • Monomorphic inline caches: the VC cache key still includes caller_function_addr + call_pc — a per-PC IC comparing receiver tags would skip the map entirely.
  • Test-runner concurrency bounding (TaskGroup gating in testing/registry.baml): the corpus spawns all ~3,400 tests at once; bounding admission fixes fd storms, tight-timeout flakiness, and makes per-test durations meaningful. Separate stdlib PR.

Raw sample profiles are available on request.

🤖 Generated with Claude Code


Note

Medium Risk
Touches GC-permit fairness and virtual-dispatch cache identity. Logic is intended to be equivalent, but a keying or try-acquire mistake would mis-dispatch methods or starve GC parks.

Overview
Behavior-preserving hot-path speedups from corpus profiling: less semaphore churn on every VM resume, cheaper virtual-call cache keys, and no futures-map rehash under the manager mutex.

Heap permits: InactiveHeapPermit::acquire now try_acquire_owned first and only fair-waits when a GC park has drained the semaphore, so parks cannot be starved.

Virtual-call cache: primitive receivers (int/string/etc.) key as a Primitive(u8) discriminant instead of hashing a RealizedTy. The cache uses FxHashMap instead of SipHash.

Futures: active_futures is pre-sized to 4096 so corpus-scale churn does not rehash while holding the manager lock.

Reviewed by Cursor Bugbot for commit a5f0104. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Performance Improvements
    • Improved runtime efficiency when managing large numbers of concurrent operations.
    • Reduced overhead for common virtual calls involving primitive values.
    • Improved memory and lookup performance for virtual-call caching.
    • Optimized resource acquisition to respond immediately when capacity is available.

…iling

Sampling a release-grade corpus run (3,448 tests) showed the engine averaging
~1 core on a 12-core machine with three concrete taxes:

1. every VM task resume paid a full fair-queue acquire on the global heap
   permit semaphore (17 of 39 sampled threads sat inside
   `InactiveHeapPermit::acquire` / `batch_semaphore::poll`),
2. every virtual call on a primitive receiver (string/int methods — the
   hottest calls in practice) built and deep-hashed a `RealizedTy` under
   SipHash for the static virtual-call cache key,
3. the `FutureManager`'s `active_futures` map rehashed under the manager's
   mutex mid-run — sampled as one thread in `reserve_rehash` while every
   other worker parked.

Fixes, all behavior-preserving:

- `InactiveHeapPermit::acquire`: `try_acquire_owned` fast path. Fairness only
  matters once a GC park's `acquire_many(MAX_PERMITS)` is pending — and then
  zero permits are available, the try fails, and acquisition falls through to
  the fair wait, so parks cannot be starved.
- `StaticVirtualReceiverKey::Primitive(u8)`: bare discriminants for the
  argument-less primitive receivers; no realized type built or hashed.
  Receivers whose identity carries type arguments keep the structural keys.
- the static virtual-call cache switches SipHash for FxHash (in-process
  cache keyed by program structure; DoS resistance is irrelevant).
- `active_futures` pre-sized to 4096 so corpus-scale future churn does not
  rehash inside the manager lock.

Back-to-back A/B on the corpus (same machine, fasttest profile):
wall 69.0s -> 67.1s (timer-bound floor), total CPU 88.2s -> 64.1s (-27%),
sys time 17.0s -> 1.9s (-89%, the semaphore kernel traffic). All 580
bex_vm/bex_heap/bex_engine tests pass.

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

vercel Bot commented Aug 21, 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 Aug 21, 2026 7:11am
promptfiddle2 Error Error Aug 21, 2026 7:11am

Request Review

@cursor

cursor Bot commented Aug 21, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_4fd667d0-0eba-4e42-9564-e9307c3d422c)

@github-actions

Copy link
Copy Markdown

⏭️ Performance benchmarks were skipped

Perf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to canary/main.

To run them on this PR, do any of the following, then push a commit (or re-run CI):

  • Add RUN_CODSPEED=1 to the PR description, or
  • Include run-perf or /perf in the PR title or any commit message.

@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bf97f114-601d-41f8-a109-d0ab20eecf0f

📥 Commits

Reviewing files that changed from the base of the PR and between f7bd01e and a5f0104.

📒 Files selected for processing (4)
  • baml_language/crates/bex_engine/src/future.rs
  • baml_language/crates/bex_heap/src/heap_guard.rs
  • baml_language/crates/bex_vm/Cargo.toml
  • baml_language/crates/bex_vm/src/vm.rs

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The runtime now pre-allocates active futures, adds a fast path for inactive heap permits, and uses compact primitive keys with FxHashMap for static virtual-call caching.

Changes

Runtime optimizations

Layer / File(s) Summary
Virtual-call receiver cache optimization
baml_language/crates/bex_vm/Cargo.toml, baml_language/crates/bex_vm/src/vm.rs
The VM adds compact primitive receiver keys, switches the static virtual-call cache to FxHashMap, and updates both VM constructors.
Heap permit acquisition fast path
baml_language/crates/bex_heap/src/heap_guard.rs
InactiveHeapPermit::acquire tries immediate semaphore acquisition before awaiting fair acquisition.
Future map pre-allocation
baml_language/crates/bex_engine/src/future.rs
active_futures is initialized with capacity for 4096 entries.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: 🟡 Moderate · up to a5f01

This PR reduces interpreter CPU and synchronization overhead, but merge readiness is not yet complete because each changed Rust crate still needs its cargo test --lib run completed after the earlier timeout.

Suggested reviewers: 2kai2kai2, antoniosarosi, codeshaunted

Poem

A rabbit found a cache so spry,
With tiny keys that quickly fly.
Permits hop through the fast lane,
Futures grow without rehash pain.
“Less waiting!” thumps my carrot heart—
Runtime paths now make a smart start.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 3 files. (1 skipped: 1 unsupported.) Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the PR's main performance improvements in permit acquisition, virtual-call key hashing, and futures-map rehashing.
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.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch aaron/engine-perf

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.

@github-actions

Copy link
Copy Markdown

Binary size checks passed

7 passed

Artifact Platform File Gzip Gated on Baseline Delta Status
baml-cli Linux 🔒 32.3 MB 12.8 MB file 32.1 MB +214.3 KB (+0.7%) OK
packed-program Linux 🔒 25.8 MB 9.4 MB file 25.5 MB +367.1 KB (+1.4%) OK
baml-cli macOS 🔒 26.0 MB 11.3 MB file 25.8 MB +230.2 KB (+0.9%) OK
packed-program macOS 🔒 21.6 MB 8.4 MB file 21.2 MB +371.3 KB (+1.8%) OK
baml-cli Windows 🔒 27.8 MB 11.5 MB file 27.6 MB +236.3 KB (+0.9%) OK
packed-program Windows 🔒 22.7 MB 8.5 MB file 22.2 MB +428.3 KB (+1.9%) OK
bridge_wasm WASM 21.7 MB 🔒 5.5 MB gzip 5.5 MB +9.2 KB (+0.2%) 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

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