perf(compile): stop three sources of per-request stall

Follow-ups from the compile-latency audit, all measured on staging:

- The per-target build lock keyed every board through _idf_target, which
  defaults unknown FQBNs to 'esp32' - so AVR, RP2040 and STM32 compiles
  all took the 'esp32::esp32' key and queued behind unrelated ESP32
  builds. The lock now only applies to boards that actually route to the
  ESP-IDF lane (the same startswith('esp32:') predicate _run_compile
  uses); everything else keys on its own FQBN.
- ESP-IDF re-validated its python environment on every cmake configure
  (~1s of ~9s). The image pins that env at build time, so pass
  PYTHON_DEPS_CHECKED=1 - the escape hatch idf.py uses for itself.
- _is_core_installed shelled out to 'arduino-cli core list' (~1.5s) on
  every non-AVR compile from inside an async handler, blocking the whole
  event loop rather than just that request. Memoized, and invalidated
  where a core is installed.

New hand-written code, unique source, idle box, steady state:
  ESP32-P4 ESP-IDF   28.8s -> 11.1s
  ESP32-S3 ESP-IDF   39.3s -> 11.5s
  ESP32-C3 Arduino   56.2s -> 21.0s
(the ESP-IDF numbers also carry the ldgen memoizer from the previous
commit; the queue waits the audit saw - 41s and 43s - were the audit's
own five concurrent builds and are what the lock-key fix targets)
This commit is contained in:
David Montero Crespo 2026-08-20 06:14:21 +02:00
parent f15834d14d
commit 82f25a03c4
3 changed files with 35 additions and 6 deletions

View File

@ -163,6 +163,13 @@ def _build_identity(board_fqbn: str) -> str:
is the previous behaviour and never less strict than it needs to be for a
board whose variant we could not read.
"""
# Only the ESP-IDF lane HAS a shared build dir (the predicate is the same
# one _run_compile routes on). An AVR / RP2040 / STM32 FQBN has no IDF
# target at all, and _idf_target defaults unknown boards to 'esp32' — so
# every non-ESP32 board used to collapse onto the `esp32::esp32` key and
# queue behind unrelated ESP32 builds for no reason.
if not board_fqbn.startswith("esp32:"):
return board_fqbn
try:
target = espidf_compiler._idf_target(board_fqbn)
variant = espidf_compiler._arduino_variant(board_fqbn, target)

View File

@ -140,6 +140,9 @@ class ArduinoCLIService:
def __init__(self, cli_path: str = "arduino-cli"):
self.cli_path = cli_path
# Cached `core list` output; see _is_core_installed. Invalidated
# wherever a core is installed so a fresh install is seen immediately.
self._installed_cores: str | None = None
self._ensure_board_urls()
self._ensure_core_installed()
@ -235,12 +238,24 @@ class ArduinoCLIService:
return None
def _is_core_installed(self, core_id: str) -> bool:
"""Check whether a core is currently installed."""
result = subprocess.run(
[self.cli_path, "core", "list"],
capture_output=True, text=True
)
return core_id in result.stdout
"""Check whether a core is currently installed.
Memoized: this shells out to `arduino-cli core list` (~1.5 s measured)
and it used to run on EVERY compile of a non-AVR board, from inside an
async handler - so it blocked the event loop for the whole process, not
just the caller. Cores only appear via install_core() below, which
refreshes the set, and they are never removed at runtime.
"""
if self._installed_cores is None:
try:
result = subprocess.run(
[self.cli_path, "core", "list"],
capture_output=True, text=True, timeout=60,
)
self._installed_cores = result.stdout
except (subprocess.SubprocessError, OSError):
return False # unknown: let the caller try to install
return core_id in self._installed_cores
async def ensure_core_for_board(self, fqbn: str) -> dict:
"""
@ -260,6 +275,8 @@ class ArduinoCLIService:
install_spec = f"{core_id}@{version}" if version else core_id
print(f"[arduino-cli] Auto-installing core {install_spec} for board {fqbn}...")
self._installed_cores = None # a core is about to appear
def _install():
return subprocess.run(
[self.cli_path, "core", "install", install_spec],

View File

@ -3967,6 +3967,11 @@ class ESPIDFCompiler:
f'-DIDF_TARGET={idf_target}',
'-DCMAKE_BUILD_TYPE=Release',
f'-DSDKCONFIG_DEFAULTS={project_dir / "sdkconfig.defaults"}',
# IDF re-validates its python env on every configure (~1s of a
# ~9s configure). The image pins that env at build time and the
# entrypoint has already exercised it, so tell IDF it was checked
# externally - the same escape hatch idf.py uses for itself.
'-DPYTHON_DEPS_CHECKED=1',
str(project_dir),
]