Speed up generation of template states (#73728)

* Speed up generation of template states

* tweak

* cache

* cache hash

* weaken

* Revert "weaken"

This reverts commit 4856f50080.

* 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._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
def name(self) -> str:
"""Name of this state."""

View file

@ -9,7 +9,7 @@ from collections.abc import Callable, Generator, Iterable
from contextlib import contextmanager, suppress
from contextvars import ContextVar
from datetime import datetime, timedelta
from functools import partial, wraps
from functools import cache, lru_cache, partial, wraps
import json
import logging
import math
@ -98,6 +98,9 @@ template_cv: ContextVar[tuple[str, str] | None] = ContextVar(
"template_cv", default=None
)
CACHED_TEMPLATE_STATES = 512
EVAL_CACHE_SIZE = 512
@bind_hass
def attach(hass: HomeAssistant, obj: Any) -> None:
@ -222,6 +225,9 @@ def _false(arg: str) -> bool:
return False
_cached_literal_eval = lru_cache(maxsize=EVAL_CACHE_SIZE)(literal_eval)
class RenderInfo:
"""Holds information about a template render."""
@ -318,6 +324,7 @@ class Template:
"_exc_info",
"_limited",
"_strict",
"_hash_cache",
)
def __init__(self, template, hass=None):
@ -333,6 +340,7 @@ class Template:
self._exc_info = None
self._limited = None
self._strict = None
self._hash_cache: int = hash(self.template)
@property
def _env(self) -> TemplateEnvironment:
@ -421,7 +429,7 @@ class Template:
def _parse_result(self, render_result: str) -> Any:
"""Parse the result."""
try:
result = literal_eval(render_result)
result = _cached_literal_eval(render_result)
if type(result) in RESULT_WRAPPERS:
result = RESULT_WRAPPERS[type(result)](
@ -618,16 +626,30 @@ class Template:
def __hash__(self) -> int:
"""Hash code for template."""
return hash(self.template)
return self._hash_cache
def __repr__(self) -> str:
"""Representation of 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 to expose all HA states as attributes."""
__setitem__ = _readonly
__delitem__ = _readonly
__slots__ = ("_hass",)
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize all states."""
self._hass = hass
@ -643,7 +665,7 @@ class AllStates:
if not valid_entity_id(f"{name}.entity"):
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
# to call is_safe_attribute
@ -682,6 +704,11 @@ class AllStates:
class DomainStates:
"""Class to expose a specific HA domain as attributes."""
__slots__ = ("_hass", "_domain")
__setitem__ = _readonly
__delitem__ = _readonly
def __init__(self, hass: HomeAssistant, domain: str) -> None:
"""Initialize the domain states."""
self._hass = hass
@ -727,6 +754,9 @@ class TemplateStateBase(State):
_state: State
__setitem__ = _readonly
__delitem__ = _readonly
# Inheritance is done so functions that check against State keep working
# pylint: disable=super-init-not-called
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)
@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:
"""State generator for a domain or all states."""
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:
@ -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))
@lru_cache(maxsize=CACHED_TEMPLATE_STATES)
def _template_state(hass: HomeAssistant, state: State) -> TemplateState:
return TemplateState(hass, state)
def _get_template_state_from_state(
hass: HomeAssistant, entity_id: str, state: State | None
) -> TemplateState | None:
@ -890,7 +930,7 @@ def _get_template_state_from_state(
# access to the state properties in the state wrapper.
_collect_state(hass, entity_id)
return None
return TemplateState(hass, state)
return _template_state(hass, state)
def _resolve_state(

View file

@ -18,6 +18,7 @@ from homeassistant.const import (
MASS_GRAMS,
PRESSURE_PA,
SPEED_KILOMETERS_PER_HOUR,
STATE_ON,
TEMP_CELSIUS,
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 }}'"
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"