mirror of
https://github.com/esphome/esphome.git
synced 2026-09-24 05:24:14 +00:00
[tests] Add CodSpeed benchmark for compiled-config cache fast path (#16402)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
pre-commit-ci-lite[bot]
parent
445d841229
commit
03f5e4775c
@@ -0,0 +1,116 @@
|
||||
"""CodSpeed benchmarks for the validated-config cache fast path.
|
||||
|
||||
PR #16381 added a cache that lets ``esphome upload`` / ``esphome logs``
|
||||
skip re-running the full config-validation pipeline. These benchmarks
|
||||
compare the cached path (``load_compiled_config``) against the slow
|
||||
path (``read_config``) on the same input.
|
||||
|
||||
The fixture YAML is a modest bluetooth-proxy device. The two paths
|
||||
end up close on a config this small -- the win grows with config
|
||||
complexity (external components, large package trees, deeply nested
|
||||
schemas), where the slow path can be orders of magnitude slower than
|
||||
the cache load.
|
||||
|
||||
Skipped when ``pytest-codspeed`` isn't installed so the regular
|
||||
unit-test suite keeps working unchanged.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.compiled_config import compiled_config_path, load_compiled_config
|
||||
from esphome.config import read_config
|
||||
from esphome.core import CORE
|
||||
from esphome.storage_json import ext_storage_path
|
||||
from esphome.writer import update_storage_json
|
||||
|
||||
pytest.importorskip("pytest_codspeed")
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
FIXTURE_YAML = HERE / "fixtures" / "bluetooth_proxy_device.yaml"
|
||||
|
||||
|
||||
def _stage_yaml(tmp_path: Path) -> Path:
|
||||
"""Copy fixture YAML into a fresh tmp dir.
|
||||
|
||||
Each benchmark gets its own copy so the cache files (under
|
||||
``.esphome/storage/`` next to the YAML) don't bleed between cases.
|
||||
"""
|
||||
target = tmp_path / FIXTURE_YAML.name
|
||||
shutil.copy2(FIXTURE_YAML, target)
|
||||
return target
|
||||
|
||||
|
||||
def _prime_cache(yaml_path: Path) -> None:
|
||||
"""Run full validation once and persist the cache + sidecar.
|
||||
|
||||
Mirrors ``esphome compile``: ``read_config`` populates ``CORE.config``,
|
||||
then ``update_storage_json`` writes both the StorageJSON sidecar and
|
||||
the ``.validated.yaml`` compiled-config cache.
|
||||
"""
|
||||
CORE.config_path = yaml_path
|
||||
config = read_config({}, skip_external_update=True)
|
||||
assert config is not None, f"fixture YAML failed to validate: {yaml_path}"
|
||||
CORE.config = config
|
||||
update_storage_json()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def staged_yaml(tmp_path: Path) -> Path:
|
||||
"""YAML copied into tmp_path; no cache files written yet."""
|
||||
return _stage_yaml(tmp_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def primed_yaml(staged_yaml: Path) -> Path:
|
||||
"""YAML plus a fresh cache + sidecar on disk."""
|
||||
_prime_cache(staged_yaml)
|
||||
assert compiled_config_path(staged_yaml.name).is_file()
|
||||
assert ext_storage_path(staged_yaml.name).is_file()
|
||||
return staged_yaml
|
||||
|
||||
|
||||
def _resetting_setup(
|
||||
yaml_path: Path,
|
||||
args: tuple[Any, ...],
|
||||
kwargs: dict[str, Any],
|
||||
) -> Callable[[], tuple[tuple[Any, ...], dict[str, Any]]]:
|
||||
"""Build a per-iteration setup that resets CORE and re-pins config_path."""
|
||||
|
||||
def setup() -> tuple[tuple[Any, ...], dict[str, Any]]:
|
||||
CORE.reset()
|
||||
CORE.config_path = yaml_path
|
||||
return args, kwargs
|
||||
|
||||
return setup
|
||||
|
||||
|
||||
def test_load_compiled_config_cached(primed_yaml: Path, benchmark) -> None:
|
||||
"""Fast path: deserialize the cached, already-validated config."""
|
||||
benchmark.pedantic(
|
||||
load_compiled_config,
|
||||
setup=_resetting_setup(primed_yaml, (primed_yaml,), {}),
|
||||
rounds=5,
|
||||
iterations=1,
|
||||
)
|
||||
|
||||
|
||||
def test_read_config_uncached(primed_yaml: Path, benchmark) -> None:
|
||||
"""Slow path: full validation pipeline (yaml load + schema + components).
|
||||
|
||||
Uses the same primed fixture as the cached path -- ``read_config``
|
||||
ignores the cache file on disk, so the two benchmarks measure the
|
||||
same input from two different code paths.
|
||||
"""
|
||||
benchmark.pedantic(
|
||||
read_config,
|
||||
setup=_resetting_setup(primed_yaml, ({},), {"skip_external_update": True}),
|
||||
rounds=3,
|
||||
iterations=1,
|
||||
)
|
||||
Reference in New Issue
Block a user