Summary
fbuild-daemon.exe grows to ~3.9 GB RSS and then fails its own health check, wedging every build with:
daemon error: daemon did not become healthy after 3 spawn attempts (10s each)
ERROR: Compilation failed for board teensy40
Killing the daemon restores normal operation immediately, so this is the daemon, not the build.
Evidence
Observed twice in one session on Windows 10 / x64. Sampling an idle daemon (no build in flight) three times in a row:
fbuild-daemon.exe 3,881,972 K
fbuild-daemon.exe 3,882,100 K
fbuild-daemon.exe 3,882,228 K
Memory is still climbing while idle — roughly 128 K per sample — so this reads as a slow leak that eventually crosses whatever threshold makes the health probe fail, rather than a legitimate working-set for a build.
Reproduction is not yet minimal. It appeared after a series of bash compile teensy40 --examples Blink runs interleaved with bash compile wasm --examples .... A second, healthy daemon process (~38 MB) coexisted with the bloated one at one point, which suggests a stale instance is not always reaped.
Recovery that works today:
taskkill /F /IM fbuild-daemon.exe
fbuild daemon stop returned success but did not clear the wedged process.
The profiling entry points exist but are not wired
The obvious next step is a heap profile, and zccache already ships the machinery — it just is not enabled in fbuild.
zccache/src/lib.rs gates it behind a feature and requires the embedding binary to choose the allocator:
/// Enable the `heap-profile` feature, install [`MiMalloc`] as the final
/// binary's `#[global_allocator]`, then use [`prof`] to start profiling and
/// write pprof-compatible snapshots. The allocator declaration belongs in
/// the embedding executable because a Rust library cannot select a global
/// allocator on its consumer's behalf.
#[cfg(feature = "heap-profile")]
pub mod heap_profile {
pub use mimalloc_pprof::{
enable_heap_profiling, enable_heap_profiling_with, prof, DumpFormat,
MiMalloc, ProfConfig, ProfConfigMode,
};
}
fbuild is half wired for this:
crates/fbuild-daemon/src/main.rs:1-2 does install a global allocator:
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
- But it is plain
mimalloc (Cargo.toml:130 — mimalloc = "0.1"), not mimalloc_pprof::MiMalloc, which is the one heap_profile needs.
- And no crate enables the zccache
heap-profile feature — every dep is a bare zccache = { git = ..., rev = ... } with no features.
So the pprof snapshot path is present in the dependency tree and unreachable from the shipped binary.
Suggested wiring
- Add
features = ["heap-profile"] to the zccache dep on a crate the daemon links (e.g. crates/fbuild-build/Cargo.toml:57), ideally behind an fbuild-side heap-profile feature so release builds are unaffected.
- In
crates/fbuild-daemon/src/main.rs, swap the allocator to zccache::heap_profile::MiMalloc under that feature.
- Expose a dump trigger — a
fbuild daemon heap-dump subcommand, or an env var read at startup — that calls prof and writes a pprof snapshot.
Off-CPU profiling is also not reachable
zccache's ZCCACHE_DAEMON_PROFILE=tokio-console (zccache-daemon-core/src/daemon/entry.rs:594) initializes tokio-console tracing, which is the right tool for async stalls. But that lives in zccache-daemon-core, which drives the zccache daemon; fbuild-daemon is a separate binary with its own tracing setup and does not consult that variable. Wiring tokio-console into fbuild-daemon would be a separate, similarly small change and would help distinguish "leaking memory" from "blocked on a task that never completes".
Why this matters beyond the leak
The health-check failure surfaces as a build error (Compilation failed for board teensy40), which sends people looking at their code. The message should distinguish "daemon unhealthy — try fbuild daemon kill-all" from a genuine compile failure.
Summary
fbuild-daemon.exegrows to ~3.9 GB RSS and then fails its own health check, wedging every build with:Killing the daemon restores normal operation immediately, so this is the daemon, not the build.
Evidence
Observed twice in one session on Windows 10 / x64. Sampling an idle daemon (no build in flight) three times in a row:
Memory is still climbing while idle — roughly 128 K per sample — so this reads as a slow leak that eventually crosses whatever threshold makes the health probe fail, rather than a legitimate working-set for a build.
Reproduction is not yet minimal. It appeared after a series of
bash compile teensy40 --examples Blinkruns interleaved withbash compile wasm --examples .... A second, healthy daemon process (~38 MB) coexisted with the bloated one at one point, which suggests a stale instance is not always reaped.Recovery that works today:
fbuild daemon stopreturned success but did not clear the wedged process.The profiling entry points exist but are not wired
The obvious next step is a heap profile, and zccache already ships the machinery — it just is not enabled in fbuild.
zccache/src/lib.rsgates it behind a feature and requires the embedding binary to choose the allocator:fbuild is half wired for this:
crates/fbuild-daemon/src/main.rs:1-2does install a global allocator:mimalloc(Cargo.toml:130—mimalloc = "0.1"), notmimalloc_pprof::MiMalloc, which is the oneheap_profileneeds.heap-profilefeature — every dep is a barezccache = { git = ..., rev = ... }with nofeatures.So the pprof snapshot path is present in the dependency tree and unreachable from the shipped binary.
Suggested wiring
features = ["heap-profile"]to the zccache dep on a crate the daemon links (e.g.crates/fbuild-build/Cargo.toml:57), ideally behind an fbuild-sideheap-profilefeature so release builds are unaffected.crates/fbuild-daemon/src/main.rs, swap the allocator tozccache::heap_profile::MiMallocunder that feature.fbuild daemon heap-dumpsubcommand, or an env var read at startup — that callsprofand writes a pprof snapshot.Off-CPU profiling is also not reachable
zccache's
ZCCACHE_DAEMON_PROFILE=tokio-console(zccache-daemon-core/src/daemon/entry.rs:594) initializes tokio-console tracing, which is the right tool for async stalls. But that lives inzccache-daemon-core, which drives the zccache daemon;fbuild-daemonis a separate binary with its own tracing setup and does not consult that variable. Wiring tokio-console intofbuild-daemonwould be a separate, similarly small change and would help distinguish "leaking memory" from "blocked on a task that never completes".Why this matters beyond the leak
The health-check failure surfaces as a build error (
Compilation failed for board teensy40), which sends people looking at their code. The message should distinguish "daemon unhealthy — tryfbuild daemon kill-all" from a genuine compile failure.