Switch to using a ContextVar for template RenderInfo (#93016)

The ContextVar is about 40% faster than the attr
and dict lookups
This commit is contained in:
J. Nick Koston 2023-05-14 11:23:31 -05:00 committed by GitHub
parent 2848f8648d
commit 3314eed8d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -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):