Speed up generation of template states (#73728)

* Speed up generation of template states

* tweak

* cache

* cache hash

* weaken

* Revert "weaken"

This reverts commit 4856f500807c21aa1c9333d44fd53555bae7bb82.

* lower cache size as it tends to be the same ones over and over

* lower cache size as it tends to be the same ones over and over

* lower cache size as it tends to be the same ones over and over

* cover

* Update homeassistant/helpers/template.py

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* id reuse is possible

* account for iterting all sensors

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
J. Nick Koston 2022-06-24 16:28:26 -05:00 committed by GitHub
parent 57efa9569c
commit 32e0d9f47c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 63 additions and 6 deletions

View File

@ -1110,6 +1110,13 @@ class State:
self.domain, self.object_id = split_entity_id(self.entity_id) self.domain, self.object_id = split_entity_id(self.entity_id)
self._as_dict: ReadOnlyDict[str, Collection[Any]] | None = None self._as_dict: ReadOnlyDict[str, Collection[Any]] | None = None
def __hash__(self) -> int:
"""Make the state hashable.
State objects are effectively immutable.
"""
return hash((id(self), self.last_updated))
@property @property
def name(self) -> str: def name(self) -> str:
"""Name of this state.""" """Name of this state."""

View File

@ -9,7 +9,7 @@ from collections.abc import Callable, Generator, Iterable
from contextlib import contextmanager, suppress from contextlib import contextmanager, suppress
from contextvars import ContextVar from contextvars import ContextVar
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import partial, wraps from functools import cache, lru_cache, partial, wraps
import json import json
import logging import logging
import math import math
@ -98,6 +98,9 @@ template_cv: ContextVar[tuple[str, str] | None] = ContextVar(
"template_cv", default=None "template_cv", default=None
) )
CACHED_TEMPLATE_STATES = 512
EVAL_CACHE_SIZE = 512
@bind_hass @bind_hass
def attach(hass: HomeAssistant, obj: Any) -> None: def attach(hass: HomeAssistant, obj: Any) -> None:
@ -222,6 +225,9 @@ def _false(arg: str) -> bool:
return False return False
_cached_literal_eval = lru_cache(maxsize=EVAL_CACHE_SIZE)(literal_eval)
class RenderInfo: class RenderInfo:
"""Holds information about a template render.""" """Holds information about a template render."""
@ -318,6 +324,7 @@ class Template:
"_exc_info", "_exc_info",
"_limited", "_limited",
"_strict", "_strict",
"_hash_cache",
) )
def __init__(self, template, hass=None): def __init__(self, template, hass=None):
@ -333,6 +340,7 @@ class Template:
self._exc_info = None self._exc_info = None
self._limited = None self._limited = None
self._strict = None self._strict = None
self._hash_cache: int = hash(self.template)
@property @property
def _env(self) -> TemplateEnvironment: def _env(self) -> TemplateEnvironment:
@ -421,7 +429,7 @@ class Template:
def _parse_result(self, render_result: str) -> Any: def _parse_result(self, render_result: str) -> Any:
"""Parse the result.""" """Parse the result."""
try: try:
result = literal_eval(render_result) result = _cached_literal_eval(render_result)
if type(result) in RESULT_WRAPPERS: if type(result) in RESULT_WRAPPERS:
result = RESULT_WRAPPERS[type(result)]( result = RESULT_WRAPPERS[type(result)](
@ -618,16 +626,30 @@ class Template:
def __hash__(self) -> int: def __hash__(self) -> int:
"""Hash code for template.""" """Hash code for template."""
return hash(self.template) return self._hash_cache
def __repr__(self) -> str: def __repr__(self) -> str:
"""Representation of Template.""" """Representation of Template."""
return 'Template("' + self.template + '")' return 'Template("' + self.template + '")'
@cache
def _domain_states(hass: HomeAssistant, name: str) -> DomainStates:
return DomainStates(hass, name)
def _readonly(*args: Any, **kwargs: Any) -> Any:
"""Raise an exception when a states object is modified."""
raise RuntimeError(f"Cannot modify template States object: {args} {kwargs}")
class AllStates: class AllStates:
"""Class to expose all HA states as attributes.""" """Class to expose all HA states as attributes."""
__setitem__ = _readonly
__delitem__ = _readonly
__slots__ = ("_hass",)
def __init__(self, hass: HomeAssistant) -> None: def __init__(self, hass: HomeAssistant) -> None:
"""Initialize all states.""" """Initialize all states."""
self._hass = hass self._hass = hass
@ -643,7 +665,7 @@ class AllStates:
if not valid_entity_id(f"{name}.entity"): if not valid_entity_id(f"{name}.entity"):
raise TemplateError(f"Invalid domain name '{name}'") raise TemplateError(f"Invalid domain name '{name}'")
return DomainStates(self._hass, name) return _domain_states(self._hass, name)
# Jinja will try __getitem__ first and it avoids the need # Jinja will try __getitem__ first and it avoids the need
# to call is_safe_attribute # to call is_safe_attribute
@ -682,6 +704,11 @@ class AllStates:
class DomainStates: class DomainStates:
"""Class to expose a specific HA domain as attributes.""" """Class to expose a specific HA domain as attributes."""
__slots__ = ("_hass", "_domain")
__setitem__ = _readonly
__delitem__ = _readonly
def __init__(self, hass: HomeAssistant, domain: str) -> None: def __init__(self, hass: HomeAssistant, domain: str) -> None:
"""Initialize the domain states.""" """Initialize the domain states."""
self._hass = hass self._hass = hass
@ -727,6 +754,9 @@ class TemplateStateBase(State):
_state: State _state: State
__setitem__ = _readonly
__delitem__ = _readonly
# Inheritance is done so functions that check against State keep working # Inheritance is done so functions that check against State keep working
# pylint: disable=super-init-not-called # pylint: disable=super-init-not-called
def __init__(self, hass: HomeAssistant, collect: bool, entity_id: str) -> None: def __init__(self, hass: HomeAssistant, collect: bool, entity_id: str) -> None:
@ -865,10 +895,15 @@ def _collect_state(hass: HomeAssistant, entity_id: str) -> None:
entity_collect.entities.add(entity_id) entity_collect.entities.add(entity_id)
@lru_cache(maxsize=CACHED_TEMPLATE_STATES)
def _template_state_no_collect(hass: HomeAssistant, state: State) -> TemplateState:
return TemplateState(hass, state, collect=False)
def _state_generator(hass: HomeAssistant, domain: str | None) -> Generator: def _state_generator(hass: HomeAssistant, domain: str | None) -> Generator:
"""State generator for a domain or all states.""" """State generator for a domain or all states."""
for state in sorted(hass.states.async_all(domain), key=attrgetter("entity_id")): for state in sorted(hass.states.async_all(domain), key=attrgetter("entity_id")):
yield TemplateState(hass, state, collect=False) yield _template_state_no_collect(hass, state)
def _get_state_if_valid(hass: HomeAssistant, entity_id: str) -> TemplateState | None: def _get_state_if_valid(hass: HomeAssistant, entity_id: str) -> TemplateState | None:
@ -882,6 +917,11 @@ def _get_state(hass: HomeAssistant, entity_id: str) -> TemplateState | None:
return _get_template_state_from_state(hass, entity_id, hass.states.get(entity_id)) return _get_template_state_from_state(hass, entity_id, hass.states.get(entity_id))
@lru_cache(maxsize=CACHED_TEMPLATE_STATES)
def _template_state(hass: HomeAssistant, state: State) -> TemplateState:
return TemplateState(hass, state)
def _get_template_state_from_state( def _get_template_state_from_state(
hass: HomeAssistant, entity_id: str, state: State | None hass: HomeAssistant, entity_id: str, state: State | None
) -> TemplateState | None: ) -> TemplateState | None:
@ -890,7 +930,7 @@ def _get_template_state_from_state(
# access to the state properties in the state wrapper. # access to the state properties in the state wrapper.
_collect_state(hass, entity_id) _collect_state(hass, entity_id)
return None return None
return TemplateState(hass, state) return _template_state(hass, state)
def _resolve_state( def _resolve_state(

View File

@ -18,6 +18,7 @@ from homeassistant.const import (
MASS_GRAMS, MASS_GRAMS,
PRESSURE_PA, PRESSURE_PA,
SPEED_KILOMETERS_PER_HOUR, SPEED_KILOMETERS_PER_HOUR,
STATE_ON,
TEMP_CELSIUS, TEMP_CELSIUS,
VOLUME_LITERS, VOLUME_LITERS,
) )
@ -3831,3 +3832,12 @@ async def test_undefined_variable(hass, caplog):
"Template variable warning: 'no_such_variable' is undefined when rendering '{{ no_such_variable }}'" "Template variable warning: 'no_such_variable' is undefined when rendering '{{ no_such_variable }}'"
in caplog.text in caplog.text
) )
async def test_template_states_blocks_setitem(hass):
"""Test we cannot setitem on TemplateStates."""
hass.states.async_set("light.new", STATE_ON)
state = hass.states.get("light.new")
template_state = template.TemplateState(hass, state, True)
with pytest.raises(RuntimeError):
template_state["any"] = "any"