From 82f25a03c4770166ee65e4e85208f2bd49edaccc Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Thu, 20 Aug 2026 06:14:21 +0200 Subject: [PATCH] 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) --- backend/app/api/routes/compile.py | 7 ++++++ backend/app/services/arduino_cli.py | 29 ++++++++++++++++++++----- backend/app/services/espidf_compiler.py | 5 +++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/backend/app/api/routes/compile.py b/backend/app/api/routes/compile.py index 803969b1..8d46bf50 100644 --- a/backend/app/api/routes/compile.py +++ b/backend/app/api/routes/compile.py @@ -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) diff --git a/backend/app/services/arduino_cli.py b/backend/app/services/arduino_cli.py index 1701f167..0496a383 100644 --- a/backend/app/services/arduino_cli.py +++ b/backend/app/services/arduino_cli.py @@ -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], diff --git a/backend/app/services/espidf_compiler.py b/backend/app/services/espidf_compiler.py index ab6f9343..ea390169 100644 --- a/backend/app/services/espidf_compiler.py +++ b/backend/app/services/espidf_compiler.py @@ -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), ]