Every \`esphome\` CLI invocation pays the cost of whatever \`esphome/__main__.py\`
imports at module scope before the requested command even runs. This
moves three heavy imports into the functions that actually use them:
- \`esphome.zeroconf\` (discover_mdns_devices): only needed for the
name_add_mac_suffix OTA discovery path.
- \`esphome.writer\`: only needed by compile/clean paths.
- \`esphome.yaml_util\`: only needed by codegen, config dump, and rename.
Local measurement drops from ~75ms to ~47ms (-37%) for a cold
\`python -c 'import esphome.__main__'\`. The zeroconf chain alone accounts
for most of the gain — the case that motivated the CI budget check.
Also adds a module-level note explaining the intent so future PRs don't
innocently promote these back to the top.
Retargets four test patches from \`esphome.__main__.discover_mdns_devices\`
to \`esphome.zeroconf.discover_mdns_devices\` now that the symbol is only
bound inside the function that calls it.
- determine-jobs: trigger on changes to requirements_test.txt too; that
file is hashed into the venv cache key and installed during
restore-python, so a change there can alter the import environment.
- ci.yml: merge the --check and --har steps so we only run
importtime-waterfall once per job (was measuring twice: ~7-8s of
wasted CI time per run). Uses the new script/check_import_time.py
--check --har <path> combination; the HAR reflects the same
measurement that produced the pass/fail decision.
- script/check_import_time.py: refactor the CLI so --har is a standalone
option rather than a mutually-exclusive mode. --check and --update
each accept an optional --har PATH that writes the HAR from the same
subprocess invocation. Plain --har is still supported for local use.
- tests: add tests/script/test_check_import_time.py covering HAR parsing,
root lookup, offender ranking/dedup, budget round-trip, and the three
--check exit paths (pass, regression, missing budget) plus the new
--check --har combined write. Add requirements_test.txt case to the
should_run_import_time parametrized test.
The previous parser walked `-X importtime` stderr by hand (string-splitting
on `|`, indent-width math). Replace it with a load of the HAR JSON that
importtime-waterfall already produces: each entry carries the module name,
self-time, and cumulative — no tree reconstruction needed. Drops the
hand-rolled retry loop too, since importtime-waterfall does best-of-6
internally.
The committed budget (75.2ms) was seeded on a fast local machine; GHA
runners measured 122.8ms, so the first CI run tripped the check. Reset
the baseline to 123ms with a 25% margin (ceiling ~154ms) to absorb GHA
variance. Tighten later once we have several data points.
Adds unit tests for should_run_import_time across the trigger matrix and
wires the new mock into the existing test_main_* suite.