From 3314eed8d1f108a1a8c2a393161ab1291b6eb293 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 May 2023 11:23:31 -0500 Subject: [PATCH] Switch to using a ContextVar for template RenderInfo (#93016) The ContextVar is about 40% faster than the attr and dict lookups --- homeassistant/helpers/template.py | 46 +++++++++++++++---------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 267e2e6c565..6ff48d83241 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -89,7 +89,6 @@ _LOGGER = logging.getLogger(__name__) _SENTINEL = object() DATE_STR_FORMAT = "%Y-%m-%d %H:%M:%S" -_RENDER_INFO = "template.render_info" _ENVIRONMENT = "template.environment" _ENVIRONMENT_LIMITED = "template.environment_limited" _ENVIRONMENT_STRICT = "template.environment_strict" @@ -122,6 +121,9 @@ _P = ParamSpec("_P") ALL_STATES_RATE_LIMIT = timedelta(minutes=1) DOMAIN_STATES_RATE_LIMIT = timedelta(seconds=1) +_render_info: ContextVar[RenderInfo | None] = ContextVar("_render_info", default=None) + + template_cv: ContextVar[tuple[str, str] | None] = ContextVar( "template_cv", default=None ) @@ -651,7 +653,7 @@ class Template: ) -> RenderInfo: """Render the template and collect an entity filter.""" self._renders += 1 - assert self.hass and _RENDER_INFO not in self.hass.data + assert self.hass and _render_info.get() is None render_info = RenderInfo(self) @@ -661,13 +663,13 @@ class Template: render_info._freeze_static() return render_info - self.hass.data[_RENDER_INFO] = render_info + token = _render_info.set(render_info) try: render_info._result = self.async_render(variables, strict=strict, **kwargs) except TemplateError as ex: render_info.exception = ex finally: - del self.hass.data[_RENDER_INFO] + _render_info.reset(token) render_info._freeze() return render_info @@ -807,13 +809,11 @@ class AllStates: __getitem__ = __getattr__ def _collect_all(self) -> None: - render_info = self._hass.data.get(_RENDER_INFO) - if render_info is not None: + if (render_info := _render_info.get()) is not None: render_info.all_states = True def _collect_all_lifecycle(self) -> None: - render_info = self._hass.data.get(_RENDER_INFO) - if render_info is not None: + if (render_info := _render_info.get()) is not None: render_info.all_states_lifecycle = True def __iter__(self) -> Generator[TemplateState, None, None]: @@ -869,14 +869,12 @@ class DomainStates: __getitem__ = __getattr__ def _collect_domain(self) -> None: - entity_collect = self._hass.data.get(_RENDER_INFO) - if entity_collect is not None: - entity_collect.domains.add(self._domain) + if (entity_collect := _render_info.get()) is not None: + entity_collect.domains.add(self._domain) # type: ignore[attr-defined] def _collect_domain_lifecycle(self) -> None: - entity_collect = self._hass.data.get(_RENDER_INFO) - if entity_collect is not None: - entity_collect.domains_lifecycle.add(self._domain) + if (entity_collect := _render_info.get()) is not None: + entity_collect.domains_lifecycle.add(self._domain) # type: ignore[attr-defined] def __iter__(self) -> Generator[TemplateState, None, None]: """Return the iteration over all the states.""" @@ -913,8 +911,8 @@ class TemplateStateBase(State): self._as_dict: ReadOnlyDict[str, Collection[Any]] | None = None def _collect_state(self) -> None: - if self._collect and (_render_info := self._hass.data.get(_RENDER_INFO)): - _render_info.entities.add(self._entity_id) + if self._collect and (render_info := _render_info.get()): + render_info.entities.add(self._entity_id) # type: ignore[attr-defined] # Jinja will try __getitem__ first and it avoids the need # to call is_safe_attribute @@ -922,8 +920,8 @@ class TemplateStateBase(State): """Return a property as an attribute for jinja.""" if item in _COLLECTABLE_STATE_ATTRIBUTES: # _collect_state inlined here for performance - if self._collect and (_render_info := self._hass.data.get(_RENDER_INFO)): - _render_info.entities.add(self._entity_id) + if self._collect and (render_info := _render_info.get()): + render_info.entities.add(self._entity_id) # type: ignore[attr-defined] return getattr(self._state, item) if item == "entity_id": return self._entity_id @@ -1057,8 +1055,8 @@ _create_template_state_no_collect = partial(TemplateState, collect=False) def _collect_state(hass: HomeAssistant, entity_id: str) -> None: - if (entity_collect := hass.data.get(_RENDER_INFO)) is not None: - entity_collect.entities.add(entity_id) + if (entity_collect := _render_info.get()) is not None: + entity_collect.entities.add(entity_id) # type: ignore[attr-defined] def _state_generator( @@ -1575,7 +1573,7 @@ def has_value(hass: HomeAssistant, entity_id: str) -> bool: def now(hass: HomeAssistant) -> datetime: """Record fetching now.""" - if (render_info := hass.data.get(_RENDER_INFO)) is not None: + if (render_info := _render_info.get()) is not None: render_info.has_time = True return dt_util.now() @@ -1583,7 +1581,7 @@ def now(hass: HomeAssistant) -> datetime: def utcnow(hass: HomeAssistant) -> datetime: """Record fetching utcnow.""" - if (render_info := hass.data.get(_RENDER_INFO)) is not None: + if (render_info := _render_info.get()) is not None: render_info.has_time = True return dt_util.utcnow() @@ -2081,7 +2079,7 @@ def random_every_time(context, values): def today_at(hass: HomeAssistant, time_str: str = "") -> datetime: """Record fetching now where the time has been replaced with value.""" - if (render_info := hass.data.get(_RENDER_INFO)) is not None: + if (render_info := _render_info.get()) is not None: render_info.has_time = True today = dt_util.start_of_local_day() @@ -2106,7 +2104,7 @@ def relative_time(hass: HomeAssistant, value: Any) -> Any: If the input are not a datetime object the input will be returned unmodified. """ - if (render_info := hass.data.get(_RENDER_INFO)) is not None: + if (render_info := _render_info.get()) is not None: render_info.has_time = True if not isinstance(value, datetime):