Skip to content

fbuild-daemon grows to ~3.9GB while idle then fails health check; heap-profile entry points not wired #1360

Description

@zackees

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:130mimalloc = "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

  1. 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.
  2. In crates/fbuild-daemon/src/main.rs, swap the allocator to zccache::heap_profile::MiMalloc under that feature.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions