Skip to content

perf(esp32): library-selection/LDF resolver is uncached on ESP32 while RP2040/Teensy/STM32 use the cached path #1539

Closed

Description

@zackees

What

ESP32's orchestrator resolves the project's library-selection / LDF result through the uncached path, while RP2040, Teensy and STM32 all go through the cached one. A library-selection cache store already exists on disk and is populated by other platforms; ESP32 never reads it.

  • ESP32 (uncached): crates/fbuild-build-esp/src/esp32/orchestrator/build.rs calls resolve_framework_library_selection_active_declared_with_extra
  • RP2040 (cached): crates/fbuild-build-arm/src/rp2040/orchestrator.rs:323 → resolve_framework_library_sources_cached
  • Teensy (cached): crates/fbuild-build-arm/src/teensy/orchestrator.rs:271
  • STM32 (cached): crates/fbuild-build-arm/src/stm32/orchestrator/mod.rs:403

The cached entry point lives in crates/fbuild-build-engine/src/framework_libs.rs alongside the uncached one, so this looks like an incomplete migration rather than a deliberate ESP32 choice.

Why it matters

The LDF pass is a header-scan + dependency resolution over every candidate library. On bench/blink it resolves 0 libraries and is under 1 ms, so it does not show up in today's ESP32 numbers — but it is the one platform where the cost is unamortized, and it is exactly the platform with the largest library surface (Arduino core + ESP-IDF bundled libraries + the framework's own libraries/). On a real FastLED sketch with several lib_deps, this is a per-build cost that other platforms already avoid.

A populated ~/.fbuild/prod/cache/library-selection store (~24 MB observed) exists on a machine that only builds ESP32, which suggests the ESP32 path used to be cached, or that another code path populates it.

Suspected fix

Switch the ESP32 call site to resolve_framework_library_sources_cached (matching the other three platforms), keeping the existing ESP32-specific inputs — or document why ESP32 must stay uncached.

Found while investigating #1537.

Activity

  1. zackees commented on Sep 28, 2026

    @zackees
    MemberAuthor

    Resolved by #1551 and #1554, which went after the measured cost rather than bolting ESP32 onto the cross-build selection cache.

    The uncached ESP32 path was not just unamortized; it was pathological. With ~400 ESP32 search paths, walker::resolve_include is_file()-probed every path for every include reference, across four walks per build (the defined-macro pre-walk also ran twice). A perf profile of an incremental FastLED build put 67% of daemon CPU there.

    Library selection in an incremental FastLED DemoReel100 esp32s3 build (sketch edit):

    selection time
    before ~17 s
    #1551: memoize each name's resolution; share the resolver across walks; drop the duplicate walk 1.2 s
    #1554: index the search roots so misses stop probing every path 0.23 s

    The selected set is the same (FS, I2S, LittleFS, SPI, WiFi), and the whole incremental build went from ~25 s to ~6–7 s.

    I deliberately did not adopt resolve_framework_library_sources_cached for ESP32. Its key hashes seed contents but not the headers those seeds reach. On ESP32, FastLED's sources are seeds and its headers change constantly, so a hit could replay a stale selection. At 0.23 s the remaining cost doesn't justify that risk.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions