Skip to content

Commit

Permalink
Avoid creating unneeded Context and Event objects when firing events (h…
Browse files Browse the repository at this point in the history
…ome-assistant#113798)

* Avoid creating unneeded Context and Event objects when firing events

* Add test

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
  • Loading branch information
emontnemery and bdraco authored Mar 20, 2024
1 parent 638020f commit d31124d
Show file tree
Hide file tree
Showing 19 changed files with 257 additions and 128 deletions.
39 changes: 22 additions & 17 deletions homeassistant/components/homeassistant/triggers/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from collections.abc import ItemsView
from collections.abc import ItemsView, Mapping
from typing import Any

import voluptuous as vol
Expand Down Expand Up @@ -101,30 +101,18 @@ async def async_attach_trigger(
job = HassJob(action, f"event trigger {trigger_info}")

@callback
def filter_event(event: Event) -> bool:
def filter_event(event_data: Mapping[str, Any]) -> bool:
"""Filter events."""
try:
# Check that the event data and context match the configured
# schema if one was provided
if event_data_items:
# Fast path for simple items comparison
if not (event.data.items() >= event_data_items):
if not (event_data.items() >= event_data_items):
return False
elif event_data_schema:
# Slow path for schema validation
event_data_schema(event.data)

if event_context_items:
# Fast path for simple items comparison
# This is safe because we do not mutate the event context
# pylint: disable-next=protected-access
if not (event.context._as_dict.items() >= event_context_items):
return False
elif event_context_schema:
# Slow path for schema validation
# This is safe because we make a copy of the event context
# pylint: disable-next=protected-access
event_context_schema(dict(event.context._as_dict))
event_data_schema(event_data)
except vol.Invalid:
# If event doesn't match, skip event
return False
Expand All @@ -133,6 +121,22 @@ def filter_event(event: Event) -> bool:
@callback
def handle_event(event: Event) -> None:
"""Listen for events and calls the action when data matches."""
if event_context_items:
# Fast path for simple items comparison
# This is safe because we do not mutate the event context
# pylint: disable-next=protected-access
if not (event.context._as_dict.items() >= event_context_items):
return
elif event_context_schema:
try:
# Slow path for schema validation
# This is safe because we make a copy of the event context
# pylint: disable-next=protected-access
event_context_schema(dict(event.context._as_dict))
except vol.Invalid:
# If event doesn't match, skip event
return

hass.async_run_hass_job(
job,
{
Expand All @@ -146,9 +150,10 @@ def handle_event(event: Event) -> None:
event.context,
)

event_filter = filter_event if event_data_items or event_data_schema else None
removes = [
hass.bus.async_listen(
event_type, handle_event, event_filter=filter_event, run_immediately=True
event_type, handle_event, event_filter=event_filter, run_immediately=True
)
for event_type in event_types
]
Expand Down
8 changes: 5 additions & 3 deletions homeassistant/components/mqtt_statestream/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Publish simple item state changes via MQTT."""

from collections.abc import Mapping
import json
import logging
from typing import Any

import voluptuous as vol

Expand Down Expand Up @@ -90,9 +92,9 @@ async def _state_publisher(evt: Event) -> None:
@callback
def _ha_started(hass: HomeAssistant) -> None:
@callback
def _event_filter(evt: Event) -> bool:
entity_id: str = evt.data["entity_id"]
new_state: State | None = evt.data["new_state"]
def _event_filter(event_data: Mapping[str, Any]) -> bool:
entity_id: str = event_data["entity_id"]
new_state: State | None = event_data["new_state"]
if new_state is None:
return False
if not publish_filter(entity_id):
Expand Down
8 changes: 4 additions & 4 deletions homeassistant/components/person/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from collections.abc import Callable
from collections.abc import Callable, Mapping
import logging
from typing import Any, Self

Expand Down Expand Up @@ -248,11 +248,11 @@ async def async_load(self) -> None:
)

@callback
def _entity_registry_filter(self, event: Event) -> bool:
def _entity_registry_filter(self, event_data: Mapping[str, Any]) -> bool:
"""Filter entity registry events."""
return (
event.data["action"] == "remove"
and split_entity_id(event.data[ATTR_ENTITY_ID])[0] == "device_tracker"
event_data["action"] == "remove"
and split_entity_id(event_data[ATTR_ENTITY_ID])[0] == "device_tracker"
)

async def _entity_registry_updated(self, event: Event) -> None:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/recorder/db_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ def to_native(self, validate_entity_id: bool = True) -> Event | None:
EventOrigin(self.origin)
if self.origin
else EVENT_ORIGIN_ORDER[self.origin_idx or 0],
dt_util.utc_from_timestamp(self.time_fired_ts or 0),
self.time_fired_ts or 0,
context=context,
)
except JSON_DECODE_EXCEPTIONS:
Expand Down
6 changes: 4 additions & 2 deletions homeassistant/components/recorder/entity_registry.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Recorder entity registry helper."""

from collections.abc import Mapping
import logging
from typing import Any

from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
Expand Down Expand Up @@ -29,9 +31,9 @@ def _async_entity_id_changed(event: Event) -> None:
)

@callback
def entity_registry_changed_filter(event: Event) -> bool:
def entity_registry_changed_filter(event_data: Mapping[str, Any]) -> bool:
"""Handle entity_id changed filter."""
return event.data["action"] == "update" and "old_entity_id" in event.data
return event_data["action"] == "update" and "old_entity_id" in event_data

@callback
def _setup_entity_registry_event_handler(hass: HomeAssistant) -> None:
Expand Down
7 changes: 5 additions & 2 deletions homeassistant/components/tasmota/device_automation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"""Provides device automations for Tasmota."""

from collections.abc import Mapping
from typing import Any

from hatasmota.const import AUTOMATION_TYPE_TRIGGER
from hatasmota.models import DiscoveryHashType
from hatasmota.trigger import TasmotaTrigger
Expand Down Expand Up @@ -27,9 +30,9 @@ async def async_device_removed(event: Event) -> None:
await async_remove_automations(hass, event.data["device_id"])

@callback
def _async_device_removed_filter(event: Event) -> bool:
def _async_device_removed_filter(event_data: Mapping[str, Any]) -> bool:
"""Filter device registry events."""
return event.data["action"] == "remove"
return event_data["action"] == "remove"

async def async_discover(
tasmota_automation: TasmotaTrigger, discovery_hash: DiscoveryHashType
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/voip/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def async_device_removed(ev: Event) -> None:
self.hass.bus.async_listen(
dr.EVENT_DEVICE_REGISTRY_UPDATED,
async_device_removed,
callback(lambda ev: ev.data.get("action") == "remove"),
callback(lambda event_data: event_data.get("action") == "remove"),
)
)

Expand Down
8 changes: 4 additions & 4 deletions homeassistant/config_entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -2519,16 +2519,16 @@ async def _handle_reload(self, _now: Any) -> None:


@callback
def _handle_entry_updated_filter(event: Event) -> bool:
def _handle_entry_updated_filter(event_data: Mapping[str, Any]) -> bool:
"""Handle entity registry entry update filter.
Only handle changes to "disabled_by".
If "disabled_by" was CONFIG_ENTRY, reload is not needed.
"""
if (
event.data["action"] != "update"
or "disabled_by" not in event.data["changes"]
or event.data["changes"]["disabled_by"]
event_data["action"] != "update"
or "disabled_by" not in event_data["changes"]
or event_data["changes"]["disabled_by"]
is entity_registry.RegistryEntryDisabler.CONFIG_ENTRY
):
return False
Expand Down
Loading

0 comments on commit d31124d

Please sign in to comment.