| CRITICAL |
crates/fbuild-build/src/zccache_embedded.rs:198-224 (FbuildZccacheService::compile_blocking) |
runtime.block_on(async move { inner.compile(req).await }) from a sync caller. The underlying ZccacheService::compile is already async on the daemon's runtime. |
Delete compile_blocking; expose pub async fn compile(...) -> Result<EmbeddedCompileOutcome, _> that just .awaits inner.compile(req). |
Cascades into compiler::compile_source (the sole call site) and from there into the Compiler::compile_one trait. With BuildOrchestrator::build going async too (see below), the whole stack collapses to native .await. Removes the only block_on in fbuild-build and the only one in compile_backend.rs (runtime field becomes dead). |
| CRITICAL |
crates/fbuild-build/src/compiler.rs:573-685 (pub fn compile_source) |
Sync pub fn. Calls svc.compile_blocking(runtime, ...). Runs once per TU on every parallel-compile worker, so on a 100-TU project it holds N tokio workers blocked for the entire build if hosted on the runtime. Currently runs on std::thread::scope workers in parallel.rs, so it doesn't block tokio today — but it can't be moved onto the runtime until this is async. |
pub async fn compile_source(...) -> Result<CompileResult>. Replace compile_blocking call with .await. std::fs::create_dir_all + std::fs::write (lines 599, 675) → tokio::fs::*. |
This is the cascade root for the whole pipeline. Every Compiler::compile_one impl in avr/, esp32/, esp8266/, rp2040/, stm32/, teensy/, nrf52/, apollo3/, ch32v/, nxplpc/, renesas/, sam/, silabs/, generic_arm/ must become async fn. Need async-trait (Rust 1.94.1 native AFITs work but with dyn limitations — Compiler is used as &dyn Compiler widely, so async-trait is the pragmatic choice). |
| CRITICAL |
crates/fbuild-build/src/parallel.rs:80-143 (compile_sources_parallel) |
std::thread::scope spawning N workers, each calling compiler.compile(...) sync. Uses std::sync::Mutex<work_iter>, Mutex<Option<String>>, Mutex<Vec<String>>, AtomicUsize. Held across sync compiler.compile (which itself goes through block_on today). |
Replace thread::scope with futures::stream::FuturesUnordered driven by tokio::task::spawn. Mutex<work_iter> → cheaper to drain into a Vec upfront and index with AtomicUsize. Per-task compiler.compile(...).await. Bound concurrency with a tokio::sync::Semaphore(jobs) instead of fixed worker count. |
Hot-path fan-out: every pipeline::compile_sources / compile_local_libraries / ESP32 build calls this. Cascades from compile_source async conversion. Once both flip, the entire per-build compile graph is visible to tokio-console. |
| CRITICAL |
crates/fbuild-build/src/lib.rs:201-204 (BuildOrchestrator trait) |
fn build(&self, params: &BuildParams) -> Result<BuildResult> — sync. 14 impls. |
async fn build(...) via #[async_trait] (used as Box<dyn BuildOrchestrator> in get_orchestrator, so AFITs are awkward). |
Cascades into all 14 per-platform orchestrators: apollo3/, avr/, ch32v/, esp32/orchestrator/build.rs, esp8266/, nrf52/, nxplpc/, rp2040/, sam/, silabs/, stm32/orchestrator/mod.rs, teensy/, renesas/, plus the trait's caller in fbuild-daemon. See the dedicated subsection below for the design call. |
| HIGH |
crates/fbuild-build/src/compile_many.rs:665-736 (run_stage2) |
std::thread::scope fanning out per-sketch builds across N workers. Each worker calls builder.build(SketchBuildInputs) → get_orchestrator(platform).build(¶ms) (sync chain). Uses AtomicUsize queue + Mutex<Option<SketchResult>> per slot. let _ = h.join(); (line 734). |
If BuildOrchestrator::build goes async: replace with FuturesUnordered + per-task orchestrator.build(...).await. Mutex<Option<SketchResult>> per slot becomes a plain Vec<Option<SketchResult>> populated by index from a JoinSet<(usize, SketchResult)>. |
Primary stage-2 fan-out for fbuild compile-many (#238 / #335). Tokio-console gains per-sketch task visibility. Cascades from BuildOrchestrator::build async conversion. |
| HIGH |
crates/fbuild-build/src/compiler.rs:438 (compiler_version → subprocess::run_command(["gcc", "-dumpversion"], …)) |
Sync subprocess via fbuild_core::subprocess::run_command. Cached in COMPILER_IDENTITY_CACHE: OnceLock<Mutex<HashMap<...>>>. Runs once per distinct compiler path. |
subprocess::run_command_async(...).await. Cache → tokio::sync::OnceCell per compiler path or keep OnceLock<Mutex<HashMap>> because the lock is only held for the cache lookup (not across await — fine to keep). |
Setup-ish, but called from inside compile_source (signature derivation), so on the hot path. Cascades into fbuild-core::subprocess (separate audit). |
| HIGH |
crates/fbuild-build/src/pipeline/compile.rs:167-178 (log_toolchain_version) |
Sync subprocess::run_command(["gcc", "-dumpversion"], …). |
.await. |
Per-build (not per-TU). Same cascade through fbuild-core::subprocess. |
| HIGH |
crates/fbuild-build/src/linker.rs:309, 351, 398, 432 (LinkerBase::archive / report_size / analyze_symbols / objcopy_firmware) |
Sync subprocess::run_command(args, None, None, None). |
.await. |
Linker hot path, one invocation per build per archive/size/objcopy step. Cascades into every per-platform *_linker.rs. |
| HIGH |
crates/fbuild-build/src/{avr,esp32,esp8266,ch32v,sam,silabs,teensy,renesas,nrf52,generic_arm}/[*_]linker.rs |
All share the same shape: use fbuild_core::subprocess::run_command; … run_command(&args_ref, None, None, None)?; for the link driver invocation. ~10 files, each one call. |
.await on run_command_async. |
Cascades from subprocess::run_command async conversion. Linker invocations are ~1s+ on ESP32; making them async lets tokio-console show the "linking" task. |
| HIGH |
crates/fbuild-build/src/esp32/orchestrator/embed.rs:26-80 (process_embed_files) |
Sync subprocess::run_command([objcopy, …], project_dir, …) in a for file in embed_files loop. Each file is one objcopy invocation. |
.await per file, or FuturesUnordered for parallel embed if file count grows. |
Cascades from subprocess::run_command async. ESP32-only, but real per-build cost on FS / partition / OTA embeds. |
| HIGH |
crates/fbuild-build/src/esp32/orchestrator/boot_artifacts.rs:90, 141 |
Sync subprocess::run_command(...) for esptool elf2image + bootloader merging. |
.await. |
Cascades from subprocess::run_command async. ESP32-only, once per build. |
| HIGH |
crates/fbuild-build/src/stm32/orchestrator/arduino_mbed.rs:272 |
Sync subprocess::run_command(...) (memap or Mbed-specific tool). |
.await. |
Cascades from subprocess::run_command async. STM32-only. |
| HIGH |
crates/fbuild-build/src/script_runtime.rs:111, 297 |
Sync subprocess::run_command([python, harness, input], …) to evaluate PlatformIO extra_scripts. |
.await. |
Cascades from subprocess::run_command async. Per-build (once). |
| HIGH |
crates/fbuild-build/src/symbol_analyzer/mod.rs:15, 161, 216, 359 |
Sync subprocess::run_command_with_stdin + run_command for nm / c++filt. |
.await. |
Cascades from subprocess async. Off the hot per-TU path but still per-build. |
| HIGH |
crates/fbuild-build/src/shrink/probe.rs:33, 348 |
Sync subprocess::run_command_with_stdin + run_command for preprocessor probes (#493, #502). |
.await. |
Cascades from subprocess async. Per-build (once per shrink decision). |
| MEDIUM |
crates/fbuild-build/src/compile_many.rs:121-171 (seed_stage2_core_from_stage1) |
std::fs::read_dir + std::fs::hard_link / std::fs::copy loop on every stage-2 worker. |
tokio::fs::read_dir + tokio::fs::hard_link / tokio::fs::copy. |
Cascades from run_stage2 async conversion. Per stage-2 sketch, so up to N times per compile-many. Hardlink is fast but on cold-cache CI runs the byte-copy fallback is real I/O. |
| MEDIUM |
crates/fbuild-build/src/framework_core_cache.rs:124-176 (hydrate / store / copy_artifact_files) |
std::fs::read, std::fs::create_dir_all, std::fs::read_dir, std::fs::copy, std::fs::remove_file, std::fs::File::options().open(). |
tokio::fs::*. |
Per-build (hydrate + store calls bracket the framework compile step). The set_modified call on a tokio::fs::File needs the std handle — into_std().await then sync metadata write is OK to keep sync inside a spawn_blocking. |
| MEDIUM |
crates/fbuild-build/src/pipeline/compile.rs:61-114 (compile_local_libraries) |
std::fs::read_dir(&local_lib_dir) + std::fs::create_dir_all(&lib_build_dir) inside per-library loop. |
tokio::fs::*. |
Cascades from the compile-pipeline async conversion. Per-build (once per library). |
| MEDIUM |
crates/fbuild-build/src/pipeline/sequential.rs:116, 229 (build_log_mutex) |
let build_log_mutex = std::sync::Mutex::new(ctx.build_log); shared across parallel compile workers. Mutex::lock calls inside worker bodies (collect_warnings path in compile.rs). |
Once workers are tokio tasks, switch to tokio::sync::Mutex<BuildLog> — but only if the lock would be held across .await. The current usage holds the lock only for log.push / collect_warnings, both sync. Safer: keep std::sync::Mutex and assert no .await while held. |
Held only for log.push(...) / BuildLog::collect_warnings(...). If we keep these sync inside the lock scope, std::sync::Mutex is fine in async code per the tokio guidelines. Document the contract. |
| MEDIUM |
crates/fbuild-build/src/esp32/orchestrator/build.rs:595 |
Same std::sync::Mutex<BuildLog> pattern as sequential.rs. |
Same: keep std::sync::Mutex but document the no-await-while-held contract. |
Same as above. |
| MEDIUM |
crates/fbuild-build/src/build_output.rs:13, 22, 330 and lib.rs:164 (BuildParams::log_sender: Option<std::sync::mpsc::Sender<String>>) |
std::sync::mpsc::Sender carrying log lines from build workers back to the caller. |
If the receiver is on the runtime (the daemon's WebSocket task pumps build output), switch to tokio::sync::mpsc to avoid the receiver doing a recv() that blocks a tokio worker. If senders run in spawn_blocking and the receiver is a sync thread, std::sync::mpsc is fine. |
The receiver side lives in fbuild-daemon (build endpoint streams). Decision belongs to the partner audit but flagged here because BuildParams carries the type. |
| MEDIUM |
crates/fbuild-build/src/source_scanner.rs:270, 483, 584, 589 |
std::fs::read_to_string + std::fs::write during source discovery (per-build, not per-TU). |
tokio::fs::*. |
Per-build setup (once); lower priority than compile-time fs ops. |
| MEDIUM |
crates/fbuild-build/src/zccache_embedded.rs:282-309 (check_fingerprint_embedded, mark_fingerprint_success_embedded) |
std::fs::create_dir_all + sync TwoLayerCache::check (rayon under the hood). |
tokio::fs::create_dir_all. Wrap TwoLayerCache::check in spawn_blocking since it spins rayon workers. |
Per-build (once before each compile phase). |
| LOW |
crates/fbuild-build/src/compiler.rs:416 (COMPILER_IDENTITY_CACHE: OnceLock<Mutex<HashMap<PathBuf, String>>>) |
Sync Mutex held only for HashMap insert/lookup. |
Keep as std::sync::Mutex — never held across .await. Document. |
No change needed; documenting the contract is enough. |
| LOW |
crates/fbuild-build/src/parallel.rs:16-25 (default_jobs, effective_jobs) |
std::thread::available_parallelism(). |
Keep sync. |
Pure computation; no I/O. |
Intro
This audits
crates/fbuild-build/for synchronous code that could run on the daemon's tokio runtime instead ofstd::thread/ synchronous-subprocess pools.fbuild-buildis the highest-leverage crate for the meta migration in #813 because:compile_source→FbuildZccacheService::compile_blocking→runtime.block_on(svc.compile(req))). Thatblock_onis the most costly anti-pattern in the daemon today: every TU compile blocks one tokio worker for the full gcc duration plus the embeddedZccacheService::compileround-trip, even though the embedded service is already async-native on the same runtime.parallel.rs+compile_many.rsstage-2 workers) onstd::thread::scope, which tokio-console cannot see.BuildOrchestratorsurface — 14 sync orchestrators. Whether the trait method becomesasync fnis the single biggest design question for audit: go fully async — whole-app tokio runtime sharing for tokio-console (meta) #813's end-state.The good news:
fbuild-buildnever reaches for rawstd::process::Commandorstd::thread::spawndirectly — both are routed through wrappers (fbuild_core::subprocess::run_command,std::thread::scope) that are easy to replace centrally. The cascade is bounded; convertingsubprocess::run_commandto async plus anasync fn buildonBuildOrchestratorwould mechanically cascade through every finding below.Findings
crates/fbuild-build/src/zccache_embedded.rs:198-224(FbuildZccacheService::compile_blocking)runtime.block_on(async move { inner.compile(req).await })from a sync caller. The underlyingZccacheService::compileis already async on the daemon's runtime.compile_blocking; exposepub async fn compile(...) -> Result<EmbeddedCompileOutcome, _>that just.awaitsinner.compile(req).compiler::compile_source(the sole call site) and from there into theCompiler::compile_onetrait. WithBuildOrchestrator::buildgoing async too (see below), the whole stack collapses to native.await. Removes the onlyblock_oninfbuild-buildand the only one incompile_backend.rs(runtimefield becomes dead).crates/fbuild-build/src/compiler.rs:573-685(pub fn compile_source)pub fn. Callssvc.compile_blocking(runtime, ...). Runs once per TU on every parallel-compile worker, so on a 100-TU project it holds N tokio workers blocked for the entire build if hosted on the runtime. Currently runs onstd::thread::scopeworkers inparallel.rs, so it doesn't block tokio today — but it can't be moved onto the runtime until this is async.pub async fn compile_source(...) -> Result<CompileResult>. Replacecompile_blockingcall with.await.std::fs::create_dir_all+std::fs::write(lines 599, 675) →tokio::fs::*.Compiler::compile_oneimpl inavr/,esp32/,esp8266/,rp2040/,stm32/,teensy/,nrf52/,apollo3/,ch32v/,nxplpc/,renesas/,sam/,silabs/,generic_arm/must becomeasync fn. Needasync-trait(Rust 1.94.1 native AFITs work but withdynlimitations —Compileris used as&dyn Compilerwidely, soasync-traitis the pragmatic choice).crates/fbuild-build/src/parallel.rs:80-143(compile_sources_parallel)std::thread::scopespawning N workers, each callingcompiler.compile(...)sync. Usesstd::sync::Mutex<work_iter>,Mutex<Option<String>>,Mutex<Vec<String>>,AtomicUsize. Held across synccompiler.compile(which itself goes throughblock_ontoday).thread::scopewithfutures::stream::FuturesUnordereddriven bytokio::task::spawn.Mutex<work_iter>→ cheaper to drain into aVecupfront and index withAtomicUsize. Per-taskcompiler.compile(...).await. Bound concurrency with atokio::sync::Semaphore(jobs)instead of fixed worker count.pipeline::compile_sources/compile_local_libraries/ ESP32 build calls this. Cascades fromcompile_sourceasync conversion. Once both flip, the entire per-build compile graph is visible to tokio-console.crates/fbuild-build/src/lib.rs:201-204(BuildOrchestratortrait)fn build(&self, params: &BuildParams) -> Result<BuildResult>— sync. 14 impls.async fn build(...)via#[async_trait](used asBox<dyn BuildOrchestrator>inget_orchestrator, so AFITs are awkward).apollo3/,avr/,ch32v/,esp32/orchestrator/build.rs,esp8266/,nrf52/,nxplpc/,rp2040/,sam/,silabs/,stm32/orchestrator/mod.rs,teensy/,renesas/, plus the trait's caller infbuild-daemon. See the dedicated subsection below for the design call.crates/fbuild-build/src/compile_many.rs:665-736(run_stage2)std::thread::scopefanning out per-sketch builds across N workers. Each worker callsbuilder.build(SketchBuildInputs)→get_orchestrator(platform).build(¶ms)(sync chain). UsesAtomicUsizequeue +Mutex<Option<SketchResult>>per slot.let _ = h.join();(line 734).BuildOrchestrator::buildgoes async: replace withFuturesUnordered+ per-taskorchestrator.build(...).await.Mutex<Option<SketchResult>>per slot becomes a plainVec<Option<SketchResult>>populated by index from aJoinSet<(usize, SketchResult)>.fbuild compile-many(#238 / #335). Tokio-console gains per-sketch task visibility. Cascades fromBuildOrchestrator::buildasync conversion.crates/fbuild-build/src/compiler.rs:438(compiler_version→subprocess::run_command(["gcc", "-dumpversion"], …))fbuild_core::subprocess::run_command. Cached inCOMPILER_IDENTITY_CACHE: OnceLock<Mutex<HashMap<...>>>. Runs once per distinct compiler path.subprocess::run_command_async(...).await. Cache →tokio::sync::OnceCellper compiler path or keepOnceLock<Mutex<HashMap>>because the lock is only held for the cache lookup (not across await — fine to keep).compile_source(signature derivation), so on the hot path. Cascades intofbuild-core::subprocess(separate audit).crates/fbuild-build/src/pipeline/compile.rs:167-178(log_toolchain_version)subprocess::run_command(["gcc", "-dumpversion"], …)..await.fbuild-core::subprocess.crates/fbuild-build/src/linker.rs:309, 351, 398, 432(LinkerBase::archive/report_size/analyze_symbols/objcopy_firmware)subprocess::run_command(args, None, None, None)..await.*_linker.rs.crates/fbuild-build/src/{avr,esp32,esp8266,ch32v,sam,silabs,teensy,renesas,nrf52,generic_arm}/[*_]linker.rsuse fbuild_core::subprocess::run_command; … run_command(&args_ref, None, None, None)?;for the link driver invocation. ~10 files, each one call..awaitonrun_command_async.subprocess::run_commandasync conversion. Linker invocations are ~1s+ on ESP32; making them async lets tokio-console show the "linking" task.crates/fbuild-build/src/esp32/orchestrator/embed.rs:26-80(process_embed_files)subprocess::run_command([objcopy, …], project_dir, …)in afor file in embed_filesloop. Each file is oneobjcopyinvocation..awaitper file, orFuturesUnorderedfor parallel embed if file count grows.subprocess::run_commandasync. ESP32-only, but real per-build cost on FS / partition / OTA embeds.crates/fbuild-build/src/esp32/orchestrator/boot_artifacts.rs:90, 141subprocess::run_command(...)foresptool elf2image+ bootloader merging..await.subprocess::run_commandasync. ESP32-only, once per build.crates/fbuild-build/src/stm32/orchestrator/arduino_mbed.rs:272subprocess::run_command(...)(memap or Mbed-specific tool)..await.subprocess::run_commandasync. STM32-only.crates/fbuild-build/src/script_runtime.rs:111, 297subprocess::run_command([python, harness, input], …)to evaluate PlatformIOextra_scripts..await.subprocess::run_commandasync. Per-build (once).crates/fbuild-build/src/symbol_analyzer/mod.rs:15, 161, 216, 359subprocess::run_command_with_stdin+run_commandfornm/c++filt..await.subprocessasync. Off the hot per-TU path but still per-build.crates/fbuild-build/src/shrink/probe.rs:33, 348subprocess::run_command_with_stdin+run_commandfor preprocessor probes (#493, #502)..await.subprocessasync. Per-build (once per shrink decision).crates/fbuild-build/src/compile_many.rs:121-171(seed_stage2_core_from_stage1)std::fs::read_dir+std::fs::hard_link/std::fs::copyloop on every stage-2 worker.tokio::fs::read_dir+tokio::fs::hard_link/tokio::fs::copy.run_stage2async conversion. Per stage-2 sketch, so up to N times percompile-many. Hardlink is fast but on cold-cache CI runs the byte-copy fallback is real I/O.crates/fbuild-build/src/framework_core_cache.rs:124-176(hydrate/store/copy_artifact_files)std::fs::read,std::fs::create_dir_all,std::fs::read_dir,std::fs::copy,std::fs::remove_file,std::fs::File::options().open().tokio::fs::*.tokio::fs::Fileneeds the std handle —into_std().awaitthen sync metadata write is OK to keep sync inside aspawn_blocking.crates/fbuild-build/src/pipeline/compile.rs:61-114(compile_local_libraries)std::fs::read_dir(&local_lib_dir)+std::fs::create_dir_all(&lib_build_dir)inside per-library loop.tokio::fs::*.crates/fbuild-build/src/pipeline/sequential.rs:116, 229(build_log_mutex)let build_log_mutex = std::sync::Mutex::new(ctx.build_log);shared across parallel compile workers.Mutex::lockcalls inside worker bodies (collect_warnings path incompile.rs).tokio::sync::Mutex<BuildLog>— but only if the lock would be held across.await. The current usage holds the lock only forlog.push/collect_warnings, both sync. Safer: keepstd::sync::Mutexand assert no.awaitwhile held.log.push(...)/BuildLog::collect_warnings(...). If we keep these sync inside the lock scope,std::sync::Mutexis fine in async code per the tokio guidelines. Document the contract.crates/fbuild-build/src/esp32/orchestrator/build.rs:595std::sync::Mutex<BuildLog>pattern assequential.rs.std::sync::Mutexbut document the no-await-while-held contract.crates/fbuild-build/src/build_output.rs:13, 22, 330andlib.rs:164(BuildParams::log_sender: Option<std::sync::mpsc::Sender<String>>)std::sync::mpsc::Sendercarrying log lines from build workers back to the caller.tokio::sync::mpscto avoid the receiver doing arecv()that blocks a tokio worker. If senders run inspawn_blockingand the receiver is a sync thread,std::sync::mpscis fine.fbuild-daemon(build endpoint streams). Decision belongs to the partner audit but flagged here becauseBuildParamscarries the type.crates/fbuild-build/src/source_scanner.rs:270, 483, 584, 589std::fs::read_to_string+std::fs::writeduring source discovery (per-build, not per-TU).tokio::fs::*.crates/fbuild-build/src/zccache_embedded.rs:282-309(check_fingerprint_embedded,mark_fingerprint_success_embedded)std::fs::create_dir_all+ syncTwoLayerCache::check(rayon under the hood).tokio::fs::create_dir_all. WrapTwoLayerCache::checkinspawn_blockingsince it spins rayon workers.crates/fbuild-build/src/compiler.rs:416(COMPILER_IDENTITY_CACHE: OnceLock<Mutex<HashMap<PathBuf, String>>>)std::sync::Mutex— never held across.await. Document.crates/fbuild-build/src/parallel.rs:16-25(default_jobs,effective_jobs)std::thread::available_parallelism().The
BuildOrchestratortrait questionShould
BuildOrchestrator::buildbecomeasync fn build(...)?Pros (in order of leverage)
fbuild-daemoncallsorchestrator.build(¶ms)from inside an axum HTTP handler. That handler is on the tokio runtime; the call intobuildis sync and blocks the worker for the entire compile (seconds to minutes). Makingbuildasync lets the worker yield naturally at every.awaitpoint — the dominant pattern in audit: go fully async — whole-app tokio runtime sharing for tokio-console (meta) #813's motivation.tokio::spawninside an asyncbuild(parallel compiles, linker, esptool) shows up in the console. Today the console sees the HTTP request task and then a silent block.tokio::sync::watchshutdown signal cantokio::select!against the build future and abort all in-flight subprocesses transitively. Syncbuildcan only check a flag between phases.tokio::time::timeout(...)around build calls. That only works on an async future.compile_sourceandsubprocess::run_commandare async, every per-platformbuildimpl is mostly.awaitinsertions — no logic changes.Cons (and mitigations)
generic_armpath. Each is hundreds of lines of orchestration. Mitigation: the actual changes are mechanical —async fn build+.awaiton everycompile_*/link_*/subprocess::*call. No business-logic changes. Land it as one PR per orchestrator behind a feature gate.async fnin traits —dynsupport. Rust 1.94.1 supports nativeasync fnin traits, butBox<dyn BuildOrchestrator>doesn't compose cleanly with native AFITs (the returned future isn'tSend-bounded by default; you'd needreturn_type_notationor a typed-future return). Mitigation: use#[async_trait](theasync-traitcrate). It already works with&dyn/Box<dyn>. The runtime overhead (boxed future per call) is trivial vs the build time itself.PlatformSupport::install_depsandCompiler::compile_oneneed the same treatment. Both are sync traits called from insidebuild. Mitigation: sameasync_trait, same mechanical cascade.compile_manyorchestration code lives infbuild-buildand callsget_orchestrator(platform).build(...)from a sync thread-pool worker today. Mitigation:compile_manyitself becomes async (see CRITICAL row forrun_stage2). The stage-2 fan-out becomesFuturesUnordered<async_compute>instead ofthread::scope.Recommendation
Convert
BuildOrchestrator::buildtoasync fnvia#[async_trait]. The mechanical cascade is bounded, the wins are load-bearing for #813's end-state, and the only "real" design question —async_traitvs native AFITs — has an obvious near-term answer (async_traitwhiledynsupport for native AFITs is still rough). Revisit native AFITs in a follow-up oncereturn_type_notationstabilises and Rust'sdyn AsyncTraitergonomics catch up.What was searched
Patterns (via Grep on
crates/fbuild-build/src/):std::thread::spawn|thread::spawn— 0 hits (all parallelism goes throughstd::thread::scope).JoinHandle|\.join\(\)— 2 hits, boththread::scopejoins inparallel.rs+compile_many.rs.std::thread::scope|scope.spawn— 2 sites:parallel.rs:80-143,compile_many.rs:665-736.rayon|par_iter|ThreadPool— 0 direct uses (rayon shows up only in upstreamzccache::fingerprint, which is documented as intentionally sync-on-CPU-pool inzccache_embedded.rs:274).Command::new|process::Command— 0 direct uses; all subprocess routing goes throughfbuild_core::subprocess::run_command(_*).subprocess::run_command(the wrapper) — 28 call sites acrosscompiler.rs,pipeline/compile.rs,linker.rs, every*_linker.rs,esp32/orchestrator/embed.rs,esp32/orchestrator/boot_artifacts.rs,stm32/orchestrator/arduino_mbed.rs,script_runtime.rs,symbol_analyzer/mod.rs,shrink/probe.rs.Mutex::new|RwLock::new— 7 hits. None held across.awaittoday (no.awaitin the crate). The 3 that would matter post-conversion are listed under MEDIUM findings.std::sync::mpsc|crossbeam—build_output.rsexposesstd::sync::mpsc::Sender<String>onBuildParams.log_sender. Cross-crate API; flagged for the partner audit.std::fs::— 329 occurrences across 50 files. The ones on the per-compile / per-build hot path are listed (compile_many seed/copy, framework_core_cache hydrate/store, pipeline compile_local_libraries, compiler.rs output writes).block_on— 1 real call site:zccache_embedded.rs:216insidecompile_blocking. (Plus four docstring references and one incompile_backend.rsdocumenting the runtime handle.)impl BuildOrchestrator for|fn build\(— confirmed 14 syncfn buildimpls across the platform tree, plus the trait itself inlib.rs:201-204.Files specifically read end-to-end:
compile_many.rs,parallel.rs,compiler.rs(compile_source + trait),pipeline/mod.rs,pipeline/compile.rs,pipeline/sequential.rs,zccache_embedded.rs,compile_backend.rs,linker.rs(subprocess sites),avr/avr_linker.rs(representative per-platform linker),esp32/orchestrator/build.rs(representative orchestrator top-level),esp32/orchestrator/embed.rs,build_output.rs,framework_core_cache.rs.Out of scope (deliberately left alone)
zccache::fingerprint's rayon parallelism — documented inzccache_embedded.rs:274as intentionally sync-on-CPU-pool. Genuinely CPU-bound (BLAKE3 + path walks). Keep on rayon; only wrap callers inspawn_blockingwhen they're invoked from the runtime.std::thread::available_parallelism()inparallel.rs:17,compile_many.rs:71/83,stage2_jobs_per_worker— pure computation, no I/O.#[cfg(test)]block inparallel.rs,compile_many.rs,framework_core_cache.rs,compiler_tests.rsusesstd::thread::sleep/std::fs::writelegitimately and is not on the production runtime.Dropimpls — none infbuild-buildreach for sync I/O on a hot path.arduino_props.rsparsing platform.txt at orchestrator construction; fine to keep sync.std::sync::Mutex<BuildLog>inpipeline/sequential.rs:116andesp32/orchestrator/build.rs:595— flagged MEDIUM, but the lock is only held for synclog.push/collect_warnings. Per tokio's "do not block the runtime" guidance, syncMutexis legal in async code as long as the critical section doesn't.await. Document the contract rather than swap the type.COMPILER_IDENTITY_CACHE(compiler.rs:63) — same as above; cache lookup never crosses.await.Sub-issue of #813.