Keep esphome.components out of the dashboard's main process#1493
Conversation
The dashboard process eagerly imported esphome.components.esp32, which pulls in espidf, requests and esphome.config; on a slow SBC like the HA Green that was roughly 0.5 to 1.5s of cold start before the first log line, and the modules stayed resident after the first firmware download. Static platform data (esp32 variants, native wifi capability, download routing, and the download types for esp32, esp8266 and rp2040) now ships in a nightly generated platform_capabilities.index.json that the dashboard reads at runtime with no esphome import. The platforms that read the build dir at download time (libretiny, nrf52) are answered by a new device-builder-helper subprocess, so the dashboard process never imports esphome.components. A ported import time budget check guards against a fresh eager import creeping back.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1493 +/- ##
==========================================
+ Coverage 99.50% 99.54% +0.03%
==========================================
Files 222 223 +1
Lines 17318 17418 +100
==========================================
+ Hits 17233 17338 +105
+ Misses 85 80 -5
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
|
esphbot
left a comment
There was a problem hiding this comment.
Blocking issues found — see the review comment above.
The CI matrix runs newer esphome (beta / dev) with extra esp32 variants, so asserting the committed index equals the installed esphome failed. Routing tests now drive off the index (what routing actually uses), and the index test asserts the committed data is a subset of the installed esphome rather than equal; exact parity stays the sync workflow's job. Review cleanups: cache load_platform_capabilities_index so the several import time callers share one parse; pass close_fds=False to the helper subprocess to match helpers.subprocess; reuse _find_sibling_cli for the helper argv (it resolves sys.executable); read/write helper JSON through helpers.json (orjson) instead of stdlib json; return a _DownloadRouting dataclass from _platform_sets instead of a positional tuple. Add in process helper_cli coverage, loader error branch tests, a wifi parity check that our inference agrees with upstream on the inputs the index covers, and run the golden helper e2e through _helper_cmd so the installed console-script entry point is exercised under CI.
Keep the helper's stdout pure JSON: route anything esphome prints to stdout during the component import / get_download_types to stderr, so a banner or deprecation notice can't make the parent's parse choke and silently drop artifacts. Split the parent-side error handling so an infrastructure failure (helper not installed, timeout, esphome import error) logs the child's stderr and is diagnosable, distinct from a non-JSON reply; both still degrade to [] so the device listing keeps rendering. Note in check_import_time that the absolute budget should be regenerated on the CI runner class, with the structural cold-import test as the authoritative guard.
|
Thanks, addressed in the follow-up commits:
Also note the index-vs-esphome test is now a subset check rather than equality, since the CI matrix runs newer esphome (beta / dev) with extra esp32 variants. |
|
|
esphbot
left a comment
There was a problem hiding this comment.
No blocking issues found.
Run the device-builder-helper JSON reply through the same coercion the generated index uses (coerce_download_entries), so a malformed payload (non-list, entries without a string file) can't reach a downstream entry["file"]; it drops to [] or the well-shaped subset. With coercion total, the parse guard narrows from a catch-all to JSONDecodeError.
|
Thanks. This review is against
CI is green-pending on |
The sys.modules invariant check has to run in a fresh interpreter, but the inline textwrap script wasn't linted or type-checked. Move it to tests/_probe_download_no_components.py and run it with the repo root on the child's PYTHONPATH so it imports this checkout's source.
There was a problem hiding this comment.
Pull request overview
This PR removes esphome.components.* imports from the dashboard’s main process by shifting platform metadata to a generated JSON index and routing build-dir-dependent download-type resolution through a short-lived helper subprocess, plus adds CI guardrails to prevent import-time regressions.
Changes:
- Add a generated
platform_capabilities.index.jsonand runtime loader to resolve ESP32 variants, Wi-Fi capability, and static download types without importing ESPHome components. - Introduce
device-builder-helpersubprocess for download-type queries that depend on the build directory (e.g., LibreTiny / nrf52). - Add import-time budget checking in CI and update/extend tests to pin both behavior and “no heavy imports in the main process” invariants.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_resolve_download_component.py | Stops importing ESPHome platform constants; derives routing sets from the generated index. |
| tests/test_remote_build_artifacts_download.py | Updates artifact-pack tests to exercise helper/subprocess failure paths and new _download_type_files signature. |
| tests/test_platform_capabilities.py | New tests validating the generated platform-capabilities index parsing and containment vs installed ESPHome. |
| tests/test_helper_cli.py | New tests for device-builder-helper and for ensuring download resolution doesn’t import esphome.components.* in the parent process. |
| tests/test_device_yaml.py | Updates Wi-Fi capability inference tests to use index-based _has_native_wifi and adds parity assertions vs upstream for indexed inputs. |
| tests/test_cold_import_floor.py | Extends “cold import” module allowlist to include esp32/espidf/wifi modules. |
| tests/e2e/slow/esp32/test_esp_idf_compile_download.py | Updates calls to collect_download_entries to pass storage_path. |
| tests/controllers/firmware/test_get_binaries.py | Refactors tests to cover index-vs-helper split (_download_types_for) and updates signatures/behavior expectations. |
| script/sync_components.py | Adds emission of platform_capabilities.index.json during sync. |
| script/import_time_budget.json | Adds baseline import-time budget for CI gating. |
| script/check_import_time.py | New script to measure and enforce import-time budget using importtime_waterfall. |
| pyproject.toml | Adds importtime-waterfall test dep and registers device-builder-helper console script. |
| esphome_device_builder/helpers/device_yaml/_generation.py | Switches native-Wi-Fi inference to index-based data (no esphome.components.wifi import). |
| esphome_device_builder/helpers/device_yaml/init.py | Removes exports for now-deleted upstream/fallback Wi-Fi helper selection internals. |
| esphome_device_builder/helper_cli.py | New helper CLI module that imports ESPHome components in a subprocess and prints JSON to stdout. |
| esphome_device_builder/definitions/platform_capabilities.index.json | New committed platform-capabilities index artifact. |
| esphome_device_builder/definitions/init.py | Adds PlatformCapabilities type + loader/coercers for the new index. |
| esphome_device_builder/controllers/remote_build/artifacts_tarball.py | Routes download-type file inclusion through firmware download resolver (index/helper), removing ESPHome component imports. |
| esphome_device_builder/controllers/remote_build/artifact_platforms/init.py | Resolves ESP32 variants via the generated index (no esphome.components.esp32 import). |
| esphome_device_builder/controllers/firmware/helpers.py | Extends sibling-CLI discovery to support differing console-script name vs module path. |
| esphome_device_builder/controllers/firmware/download.py | Implements index-backed + helper-backed download-type resolution; updates APIs to accept storage_path. |
| .github/workflows/test.yml | Adds import-time budget check step and uploads the HAR waterfall artifact. |
Comments suppressed due to low confidence (1)
tests/test_helper_cli.py:157
- In the embedded subprocess script,
StorageJSON(firmware_bin_path=...)is given aPathobject, but other fixtures in this file passstr(...). Keeping it as a string avoids potential JSON-serialization issues insideStorageJSON.save()and keeps the fixture consistent.
)
assert result.returncode == 0, f"leaked:\n{result.stdout}\nstderr:\n{result.stderr}"
_parse_download_types collapses to a dict comprehension with the non-list filter. The artifacts_tarball -> firmware.download import stays function-local; a top-level import is a circular import (firmware/__init__ -> controller -> remote_runner -> helpers.remote_artifacts_materialise -> back into artifacts_tarball), now noted in a comment so it isn't re-attempted.
|
|
esphbot
left a comment
There was a problem hiding this comment.
Blocking issues found — see the review comment above.
Add the cold-start section to docs/ARCHITECTURE.md and the generated index, helper entry point, import budget, and the no-esphome.components invariant to CLAUDE.md. Hoist the firmware_bin_path-is-None short-circuit above _download_types_for so an unbuilt libretiny/nrf52 device returns [] without spawning the helper. Shape the helper's reply through coerce_download_entries (tolerates a malformed entry instead of a KeyError aborting the whole reply), and log when coercion drops entries so a silently-dropped artifact is diagnosable.
|
Thanks, all addressed in
|
|
|
esphbot
left a comment
There was a problem hiding this comment.
No blocking issues found.
Validate the helper's component argument against [a-z0-9_]+ before interpolating it into import_module, so a crafted target_platform can't steer the import to a dotted sub-path (defence in depth; the name is already confined to the esphome.components. prefix and import_module is not eval). Fold any esp32-prefixed target_platform to the esp32 component even when the index is degraded (empty variants), so a missing index makes an ESP32 variant download slow rather than broken. Log the non-list branch of coerce_download_entries so a wholesale-malformed helper reply is diagnosable.
|
Thanks. Addressed in
Also added defence-in-depth: the helper validates its |
The 403ms baseline was measured on a dev laptop; CI runners read ~3x slower (1194ms observed), so the 15% margin flaked. Rebaseline to the CI figure with a 50% margin (and bump the script default to match) so unrelated PRs don't trip the gate; the structural cold-import test stays the precise invariant.
|
|
esphbot
left a comment
There was a problem hiding this comment.
No blocking issues found.
|
|
esphbot
left a comment
There was a problem hiding this comment.
No blocking issues found.
|
Thanks:
|
|
Tested with every board type download so should be good to go now |
|
|
esphbot
left a comment
There was a problem hiding this comment.
No blocking issues found.
build_files_for_platform now folds any esp32-prefixed target_platform to the esp32 BUILD_FILES even on a degraded index, matching _resolve_download_component so an offloaded ESP32 variant packs rather than raising on empty build_files. Make collect_download_entries' storage_path a required positional (every caller already passes it), so a future omitting caller fails mypy rather than silently returning [] for libretiny/nrf52.
|
Thanks. Both suggestions taken in
On the silent-failure notes:
|
PR Review — Keep esphome.components out of the dashboard's main processSolid, well-scoped change that delivers exactly what the description promises — no blocking issues. Strengths:
🟢 Suggestions1. Each get_binaries on a libretiny/nrf52 device spawns a fresh ~10s esphome-importing subprocess (`esphome_device_builder/controllers/firmware/download.py`, L158-170)For libretiny/nrf52 devices,
Worth confirming it's the intended trade-off: re-opening the download panel on the same libretiny device re-spawns the child each time. If that proves slow in practice, a small memoization keyed on Checklist
Silent Failure Analysis🟡 **MEDIUM** — silent empty return masks incomplete artifact (`esphome_device_builder/controllers/remote_build/artifacts_tarball.py:287-300`)Risk: When the helper subprocess fails for a build-dir-dependent platform (libretiny/nrf52), Fix: Distinguish 'platform has no download types' from 'helper failed to resolve them' and raise (or at least warn at the tarball boundary) when files can't be resolved for a build-dir-dependent platform, rather than packing a partial tarball. 🟡 **MEDIUM** — fallback value hides failure (`esphome_device_builder/controllers/firmware/download.py:195-210`)Risk: A helper infrastructure failure (not installed, timeout, import error) for a libretiny/nrf52 device degrades to Fix: Consider surfacing the helper failure to the client (e.g. an explicit error/diagnostic field) instead of collapsing it into the same empty result a genuinely-unbuilt device produces. 🟡 **MEDIUM** — fail-open fallback hides failure (`esphome_device_builder/definitions/__init__.py:480-497`)Risk: A missing or malformed index degrades silently to empty lists, so Fix: For the wifi-inference consumers, prefer failing closed (or emitting a louder, user-visible signal) when the capabilities index is unavailable rather than silently assuming wifi support. Automated review by Kōan (Claude) |
esphbot
left a comment
There was a problem hiding this comment.
No blocking issues found.
What does this implement/fix?
The dashboard process eagerly imported
esphome.components.esp32, which pulls in espidf, requests andesphome.config; on a slow SBC like the HA Green that was around 9s of cold start before the first log line, and those modules stayed resident after the first firmware download.Static platform data (esp32 variants, native wifi capability, download routing, and the download types for esp32, esp8266 and rp2040) now ships in a nightly generated
platform_capabilities.index.jsonthat the dashboard reads at runtime with no esphome import. The platforms that read the build dir at download time (libretiny, nrf52) are answered by a newdevice-builder-helpersubprocess, so the dashboard process never importsesphome.components. A ported import time budget check (script/check_import_time.py) fails CI if a fresh eager import creeps back.Behaviour is unchanged; tests pin that the index and the helper produce the same output as the in-process esphome calls they replace, across every platform.
Related issue or feature (if applicable):
Types of changes
bugfixnew-featureenhancementbreaking-changerefactordocsmaintenancecidependenciesFrontend coordination
Checklist
ruff,codespell, yaml/json/python checks).tests/where applicable.components.index.json/definitions/components/*.jsonhave not been hand-edited (regenerate viascript/sync_components.pyif a sync is needed).docs/ARCHITECTURE.mdand/ordocs/API.md.