Skip to content

perf(esp32): compile the sketch concurrently with the framework core - #1545

Merged
zackees merged 3 commits into
mainfrom
perf/esp32-sketch-concurrency
Sep 28, 2026
Merged

zackees merged 3 commits into
mainfrom
perf/esp32-sketch-concurrency

Conversation

@zackees

@zackees zackees commented Sep 28, 2026

Copy link
Copy Markdown
Member

Stacked on #1543 (include fan-out) and #1544 (canonicalize memo). Merge those first, then rebase — or merge this last. The numbers below are measured with both applied, since that is the state this change ships in.

Problem

Part of #1537 (cause 2). The ESP32 orchestrator ran the compile phases strictly sequentially:

compile-core-variant -> core-cache-store -> compile-sketch -> link

The sketch TU therefore had its own slot on the critical path with nothing to overlap with, so a cold build paid its full duration as pure added wall time — compile-sketch measures 374 ms, matching the compile-sketch: 589ms in the issue. PlatformIO compiles the sketch alongside the core files and pays nothing extra.

Fix

Compile both against one shared job gate. compile_sources_parallel_shared already exists for exactly this (#1468); it just was not used here.

The sketch future is submitted first and the gate has far more permits than the first wave needs, so its single task starts alongside the core fan-out instead of being scheduled after it.

Sharing one semaphore is also what keeps the two phases from multiplying the job budget: the region still runs at most jobs compilers, where giving each phase its own pool would permit two.

One PhaseGuard covers the region both phases now share, so compile-core-variant spans the concurrent region and compile-sketch is recorded separately as the sketch's own span rather than as a serial wall-clock phase. (The two spans deliberately overlap, so Σphases now exceeds total_ms for these two entries.)

On the oversubscription claim in the issue

Investigated and did not change the ncpu * 2 default, because it is not causing oversubscription on this workload:

  • peak concurrent compilers is 10–12 even at -j 32 (tight 10 ms sampler over the whole build)
  • -j 6 / 12 / 16 / 32 cold medians: 4.26 / 3.63 / 3.56 / 3.61 s — a plateau from 12 up, with the default already on it
  • -j 32 is not a regression, so lowering it would only risk regressing larger sketches that use the extra permits to hide zccache I/O

The shared gate in this PR is the part of "oversubscription" that actually needed fixing: it is what prevents the newly-concurrent phases from compounding the budget.

Verification

5 interleaved cold trials each, median, round-robined, daemon torn down per trial, and confirmed identical framework-core cache keys (45cae7382e31) on both sides so the comparison is apples-to-apples:

wall compile-core-variant compile-sketch
#1543 + #1544 3.66s 1551 ms 374 ms
+ this PR 3.38s 1593 ms (now spans both) 542 ms (overlaps)

firmware.bin / firmware.elf are byte-identical to the pre-change build (abe3b6ae285fe8f6 / 1a1e79faf9b22fbb) — this moves work, it does not change what is compiled.

  • soldr cargo test -p fbuild-build-esp — 133 passed
  • soldr cargo clippy -p fbuild-build-esp --all-targets clean
  • soldr cargo fmt --all applied

Refs #1537

@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 7 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: FastLED/fbuild/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 5dd31f8c-bd87-4603-9246-7275f9b48dbd

📥 Commits

Reviewing files that changed from the base of the PR and between 601f327 and 3b0e51a.

📒 Files selected for processing (3)
  • crates/fbuild-build-esp/src/esp32/orchestrator/build.rs
  • crates/fbuild-build-esp/src/esp32/orchestrator/compile_phases.rs
  • crates/fbuild-build-esp/src/esp32/orchestrator/mod.rs

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.

The ESP32 orchestrator ran the compile phases strictly sequentially:
compile-core-variant -> core-cache-store -> compile-sketch -> link. The
sketch translation unit therefore had its own slot on the critical path and
nothing else to overlap with, so a cold build paid its full duration as pure
added wall time. PlatformIO compiles the sketch together with the core files
and pays nothing extra for it.

Compile both against one shared job gate. The sketch is submitted first and
the gate has far more permits than the first wave needs, so its single task
starts alongside the core fan-out instead of being scheduled after it.

Sharing one semaphore is also what keeps the two phases from multiplying the
job budget: the region still runs at most `jobs` compilers, where giving each
phase its own pool would permit two. That budget is not the binding
constraint today - measured on 16 cores, this build peaks at 10-12 concurrent
compilers even at the default `jobs = ncpu * 2`, and `-j 16` vs `-j 32` is a
wash (3.56s vs 3.61s median over 3 interleaved cold trials) - but it is what
keeps it that way now that the phases overlap.

One `PhaseGuard` covers the region both phases share, so `compile-core-variant`
now spans the concurrent region and `compile-sketch` is recorded separately as
the sketch's own span rather than as a serial wall-clock phase.

Measured on bench/blink -e esp32s3 (16-core Linux), 5 interleaved cold trials,
median, every trial a genuine framework-core cache miss with the same cache
key on both sides:

  wall  3.66s -> 3.38s   (-7.7%)

`firmware.bin` / `firmware.elf` are byte-identical to the pre-change build -
this moves work, it does not change what is compiled.

Refs #1537
…dule

`build.rs` sits at exactly the 1000-LOC gate on main, so the previous commit
pushed it over and failed `Reject .rs files over 1000 LOC`. Extract the
core+sketch concurrent-compile block into `compile_phases.rs`, which is what
the directory layout is already for ("the orchestrator is split across sibling
files in this directory to keep each one under the 1000-LOC gate").

Pure move: same shared gate, same submit order, same phase guards. build.rs
goes 1030 -> 993 lines.
…d_pathbuf

`dylints/ban_std_pathbuf` denies naming `std::path::PathBuf` in a new file,
and `compile_phases.rs` is new. The compile engine's API takes exactly that
type, so the paths are now materialised inside the two async blocks, where the
element type is inferred from the engine call, and the boundary is generic
over `AsRef<Path>` so callers pass their existing vectors unchanged.

Verified with the same command CI runs:
`env -u RUSTUP_TOOLCHAIN soldr dylint --all -- --package fbuild-build-esp
--all-targets --target x86_64-unknown-linux-gnu` exits 0.
@zackees
zackees force-pushed the perf/esp32-sketch-concurrency branch from 1df08e9 to 3b0e51a Compare September 28, 2026 08:23
@zackees

zackees commented Sep 28, 2026

Copy link
Copy Markdown
Member Author

Rebased onto main now that #1543 and #1544 have merged — the branch is a clean 3 commits on top of main, no longer stacked.

Also added a commit fixing the Dylint Full failure: dylints/ban_std_pathbuf denies naming std::path::PathBuf in a new file, and compile_phases.rs is new. The paths are now materialised inside the two async blocks (element type inferred from the engine call) and the boundary is generic over AsRef<Path>, so callers pass their existing vectors unchanged. Verified with the same command CI runs:

env -u RUSTUP_TOOLCHAIN soldr dylint --all -- --package fbuild-build-esp \
  --all-targets --target x86_64-unknown-linux-gnu   # exit 0

Re-verified after the rebase + refactor: cold build succeeds and firmware.bin/firmware.elf still hash to abe3b6ae285fe8f6/1a1e79faf9b22fbb.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

1 participant