Skip to content

perf: parallel cold checks that actually scale + flat LSP latency - #4100

Closed
hellovai wants to merge 1 commit into
hellovai/compiler-perf-fixesfrom
hellovai/compiler-parallel-load-lsp
Closed

hellovai wants to merge 1 commit into
hellovai/compiler-perf-fixesfrom
hellovai/compiler-parallel-load-lsp

Conversation

@hellovai

Copy link
Copy Markdown
Contributor

Stacked on #4098. Fixes the "12 threads = 1.18× speedup" wall, diagnosed by comparing our salsa usage against ty/ruff's (our driver pattern was already identical to theirs — the problem was one query shape).

What

  1. Prime wave: every file's PPIR semantic index is computed across workers before the per-file check fan-out. Whole-package aggregates (package_items/namespace_items) fold over every file's index; on a cold db the first worker to claim that memo computed parse+lower+index for the entire project inline on one thread while all other workers parked on the memo's sync slot. Also retires the old first-file warm-up, which warmed the builtin package, not the user package.
  2. Bulk project load: add_or_update_files writes the project file list once (the per-file path re-cloned the whole Vec and bumped the salsa revision per file — O(files²) during load); source reads are parallel with discovery order preserved (FileId assignment and diagnostic ordering unchanged).
  3. LSP: the diagnostics candidate checks files through the shared order-preserving parallel checker under the held source guard, so every per-edit path gets the parallelism + prime wave.

End-user impact (measured)

before after
cold pure check, 295k LOC (12 threads) 7.4 s (~1.2 cores) 2.8 s (2.4+ cores)
cold check+seed, 295k LOC 10.5 s 4.4 s
LSP edit→diagnostics, 250 files 464 ms 172 ms
LSP edit→diagnostics, 500 files 1.7 s 176 ms
LSP edit→diagnostics, 1000 files 5.5 s 209 ms

Editor latency is now ~flat in project size.

Verification

  • Diagnostics byte-identical to the previous binary on the error-corpus (cold + warm).
  • 198 tests pass across baml_project / bex_project / baml_cli.
  • LSP load test: 8+ runs at 250/1000 files with 50-edit multi-file burst storms and 20 s of ~17 edits/s churn — zero crashes, stable RSS (~810 MB peak @1000), post-storm convergence every time. Disclosure: the first two 1000-file runs (only, immediately after a large build finished) showed a warm-up stall that has not reproduced in 8 subsequent attempts including exact-condition replays; the stress harness now captures server log traces to identify the stage if it recurs.

Not in this PR: ty-style snapshot+cancellation for the LSP (blocked on the panic = "abort" release profile — cancellation requires unwinding; needs a product decision), and the warm-edit floor (throws-gate + unit loads; separate reviewed design).

🤖 Generated with Claude Code

https://claude.ai/code/session_01V4oee6wrCHzXH6mM2h8eJo

@vercel

vercel Bot commented Jul 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, Comment Jul 22, 2026 3:39am
promptfiddle Ready Ready Preview, Comment Jul 22, 2026 3:39am
promptfiddle2 Ready Ready Preview, Comment Jul 22, 2026 3:39am

Request Review

@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.

@vercel
vercel Bot temporarily deployed to Preview – beps July 21, 2026 07:19 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 July 21, 2026 07:26 Inactive
@github-actions

github-actions Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

Binary size checks passed

✅ 7 passed

Artifact Platform File Gzip Gated on Baseline Delta Status
✅ baml-cli Linux 🔒 25.6 MB 10.9 MB file 25.3 MB +335.0 KB (+1.3%) OK
✅ packed-program Linux 🔒 17.3 MB 7.1 MB file 17.0 MB +218.6 KB (+1.3%) OK
✅ baml-cli macOS 🔒 19.9 MB 9.5 MB file 19.6 MB +281.3 KB (+1.4%) OK
✅ packed-program macOS 🔒 13.5 MB 6.2 MB file 13.2 MB +231.5 KB (+1.8%) OK
✅ baml-cli Windows 🔒 21.4 MB 9.7 MB file 21.1 MB +278.0 KB (+1.3%) OK
✅ packed-program Windows 🔒 14.4 MB 6.3 MB file 14.2 MB +200.7 KB (+1.4%) OK
✅ bridge_wasm WASM 16.3 MB 🔒 4.4 MB gzip 4.4 MB +23.6 KB (+0.5%) 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

…roject load; parallel LSP sweep

Three fixes for the "12 threads = 1.18x" cold-check scaling wall,
diagnosed by comparing against ty/ruff's salsa usage (whose driver
pattern ours already matches):

- Prime wave: compute every file's PPIR semantic index across workers
  BEFORE fanning out per-file checks. Whole-package aggregate queries
  (package_items / namespace_items) fold over every file's index, and
  every file's check demands them - on a cold database the first
  worker to claim the aggregate memo computed parse+lowering+indexing
  for the entire project inline on one thread while all other workers
  parked on that memo's sync slot. With per-file indexes primed the
  aggregate fold is cheap and workers stay busy. Also replaces the
  old first-file serial warm-up, which warmed the builtin package
  (compiler2_all_files orders builtins first), not the user package.

- Bulk project load: add_or_update_files registers all sources with
  ONE project-file-list write; the per-file path cloned and re-set
  the whole files Vec and bumped the salsa revision once per file
  (O(files^2) copies during initial load). Source reads now happen
  across worker threads, preserving discovery order so FileId
  assignment and diagnostic ordering are unchanged.

- The LSP diagnostics candidate now checks files through the shared
  order-preserving parallel checker (under the held source guard -
  the same exclusion the serial loop relied on), so every per-edit
  path inherits the parallelism and the prime wave.

check_files_parallel returns per-file results in input order so the
LSP can zip them back to documents; the flat collector delegates.

Measured (Apple Silicon, 12 cores):
- cold pure check, 295k LOC: 7.4s -> 2.8s wall (1.2 -> 2.4+ cores)
- cold check+seed, 295k LOC: 10.5s -> 4.4s; 59k LOC: ~0.9s
- LSP edit->diagnostics: 250 files 464 -> 172ms; 500 files 1.7s ->
  176ms; 1000 files 5.5s -> 209ms (now ~flat in project size)
- LSP load test: burst storms + sustained churn at 250/1000 files,
  zero crashes, stable RSS, convergence after every storm

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01V4oee6wrCHzXH6mM2h8eJo
@hellovai
hellovai force-pushed the hellovai/compiler-parallel-load-lsp branch from c2da018 to f214619 Compare July 22, 2026 03:17
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: da4b74e8-33a7-4fac-bb55-f17e5b0a5cf3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hellovai/compiler-parallel-load-lsp

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.

@vercel
vercel Bot temporarily deployed to Preview – beps July 22, 2026 03:18 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 July 22, 2026 03:26 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle July 22, 2026 03:39 Inactive
@hellovai

Copy link
Copy Markdown
Contributor Author

Consolidated into #4105 (the full stack, rebased onto current canary, all CI green there). This PR's commits are contained in #4105 verbatim — closing in favor of one merge.

@hellovai hellovai closed this Jul 23, 2026
meefs pushed a commit to meefs/baml that referenced this pull request Jul 23, 2026
…itor latency (BoundaryML#4105)

Consolidated stack (supersedes BoundaryML#4098, BoundaryML#4100, BoundaryML#4101 — their commits are
all contained here, rebased onto current canary). Seven commits,
reviewable independently, in dependency order.

## What this does

A ground-up performance pass on the compiler driven by head-to-head
benchmarking against tsgo (native TypeScript) and `go build` on
semantically identical generated corpora (1.5k–295k LOC), followed by
root-cause profiling of every gap found.

### 1. `perf(compiler2)`: kill the two cold-compile quadratics;
parallelize the cache-path check
- The package-wide Jinja type environment was rebuilt inside every
`check_file` call (O(N²)) — and twice per compile via `get_bytecode`'s
error gate. Now a memoized salsa query (`jinja_base_types`) + skipped
entirely for files with no prompts.
- `ProjectDatabase` handle clones deep-copied two N-entry PathBuf maps;
the parallel drivers clone ~1.55N handles per compile (quadratic CPU +
peak RSS). Now Arc-wrapped, O(1).
- `baml check`/`run` routed through a serial diagnostics collector while
the parallel one sat unreachable next to it. Now shared.
- The LSP proves the project error-free once (`get_bytecode_unchecked`),
and its diagnostics candidate now includes package-level diagnostics —
cross-file name conflicts previously **never reached the editor at
all**.

### 2. `feat(cli)`: `baml pack` uses the bytecode cache; `baml check`
seeds it
Pack was fully cold on every invocation (no cache participation at all);
check reused but never wrote, so check-only workflows never warmed. Now
one warm cache serves check → run → test → pack.

### 3–4. `build` + `fix(ci)`: hardware SHA-2 (`sha2/asm`, gated off
MSVC), doc-link fix
The warm path hashes ~hundreds of MB of unit payloads per run; it was on
the software SHA fallback.

### 5. `perf(compiler2)`: prime wave, bulk project load, parallel LSP
sweep
Cold parallel check scaled 1.18× on 12 cores: every file's check demands
whole-package aggregates, and the first worker to claim that memo
computed parse+index for the entire project inline while all other
workers parked on the sync slot (diagnosed against ty/ruff's salsa
usage; interventionally verified). Per-file indexes are now primed
across workers before fan-out. Project load stops re-writing the file
list per file (O(files²)) and reads sources in parallel (FileId order
preserved). The LSP candidate uses the shared order-preserving parallel
checker.

### 6. `perf(cache)`: warm no-op checks stop re-proving the cache
A warm no-op check cost the same as a full recompute: the throws-gate
safety compare re-parsed every file, and seeding read+hashed 78MB of
unit payloads for metadata. Now: the gate is skipped when the dirty
partition is provably empty (a value compared against itself), throws
fragments live verbatim in the manifest so a no-op check opens **zero**
unit payloads, and units load in parallel (manifest order preserved)
only when a delta means relink will need them. Both verify oracles
(`BAML_CACHE_VERIFY` full compare + always-on 1-in-32 sampled re-derive)
now check the manifest copy — exactly what is served. Design was
adversarially reviewed; the unsound variant (fact-space throws compare)
was rejected and every real edit still takes the full runtime-space
gate.

### 7. `refactor(cli)`: one shared `ProjectSession` for every
project-loading command
check/run/test/pack/describe/grep/generate each hand-rolled a drifting
subset of the same preamble — the root cause of "pack forgot the cache"
and "describe never primes." One implementation; commands declare
`CacheUse::{ReadWrite, ReadWriteTests, ReadOnly, Off}`. Introspection
installs seeds only on a no-delta plan (provable tautology) and derives
honestly otherwise.

## Measured impact (Apple Silicon, 12 cores, hyperfine)

| | before | after |
|---|--:|--:|
| cold check, 295k LOC | 21–46 s (126 s CPU, 5.3 GB RSS) | **6.6 s**
(~13 s CPU, 2.7 GB) |
| cold check, 59k LOC | 3.3 s | **1.24 s** |
| warm no-op check, 295k / 59k | 7.2 s / 3.2 s | **2.0–2.6 s / 0.4–0.6
s** |
| `run` after check (59k / 295k) | full compile | **0.12 s / 0.65 s**
(cache hit) |
| `pack` after check (59k / 295k) | 1.7 s / 40.5 s (always cold) |
**0.19 s / 0.87 s** (cache hit) |
| LSP edit→diagnostics (250/500/1000 files) | 0.46 / 1.7 / 5.5 s | **179
/ 192 / 216 ms** (flat) |
| `describe`, 295k / 59k LOC | 6.3 s / 716 ms | **2.6 s / 402 ms** |

Cold compile scaling is now ~linear in project size (was quadratic: 5×
code cost 33× CPU), verified to 8000 modules.

## Verification

- Diagnostics byte-identical to the pre-change binary (error-heavy
corpus, cold + warm) at each stage.
- Cache soundness: `BAML_CACHE_VERIFY=1` honest-recompile compare (no-op
+ after-edit) and forced `BAML_CACHE_SAMPLED_VERIFY=1` across edit
sequences — all pass; 43 bytecode-cache oracle tests updated and green.
- LSP load-tested: 50-edit multi-file burst storms + sustained ~17
edits/s churn at 250/1000 files — zero crashes, stable RSS, convergence
after every storm.
- Full stack green in CI (doc, Windows incl. MSVC sha2 gating, size
gate, all SDK matrices).

## Deliberate trade-offs

- Cold `check` at mid-size projects (~15–30k LOC) is ~10% slower than
before: it now emits + stores once to warm the cache; every later
check/run/pack is served warm.
- A warm *edit* check re-emits and stores (that is what buys the instant
run/pack); at 295k LOC the edit path remains ~6–9 s — the known
remaining floor, tracked separately.
- A unit file deleted out from under a warm cache is not detected on a
no-op run; the next compile that needs it falls back to a byte-identical
full compile.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01V4oee6wrCHzXH6mM2h8eJo

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* CLI commands now share a consistent, cache-aware project session
workflow, including lenient read-only sessions for inspection when
configuration is missing.
* **Performance**
* Faster project loading and diagnostics checking via parallel per-file
processing.
* Improved bytecode-cache reuse by avoiding unnecessary payload
hashing/loading, including a fast “no-delta” warm path.
* **Bug Fixes**
* Fixed callable-throws fragment seeding and verification for warm
“no-delta” runs.
* Improved package-level diagnostics handling during compilation and
error-gating behavior.
* **Chores**
* Improved build robustness for cryptography acceleration on MSVC
targets.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Fable 5 <[email protected]>

This branch was previously deployed

3 inactive deployments
Preview – promptfiddle — f2146194 Deployed Jul 22, 2026 by vercel[bot]
Preview – promptfiddle2 — f2146194 Deployed Jul 22, 2026 by vercel[bot]
Preview – beps — f2146194 Deployed Jul 22, 2026 by vercel[bot]
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