From b3743937de96b9a339ff99b471f126383ec70d56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 Oct 2023 06:45:22 -0500 Subject: [PATCH] Avoid looking up the callable type for HassJob when we already know it (#102962) * Avoid looking up the callable type for HassJob when we already know it When we connect the frontend we call async_listen with run_immediately MANY times when we already know the job type (it will always be a callback). This reduces the latency to get the frontend going * missing coverage --- homeassistant/core.py | 22 ++++++++++---- homeassistant/helpers/event.py | 8 +++++- homeassistant/helpers/update_coordinator.py | 15 ++++++++-- tests/test_core.py | 32 +++++++++++++++++++++ 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 48cc70e7727..01a3dd7fbe6 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -282,11 +282,12 @@ class HassJob(Generic[_P, _R_co]): name: str | None = None, *, cancel_on_shutdown: bool | None = None, + job_type: HassJobType | None = None, ) -> None: """Create a job object.""" self.target = target self.name = name - self.job_type = _get_hassjob_callable_job_type(target) + self.job_type = job_type or _get_hassjob_callable_job_type(target) self._cancel_on_shutdown = cancel_on_shutdown @property @@ -1153,13 +1154,20 @@ class EventBus: This method must be run in the event loop. """ + job_type: HassJobType | None = None if event_filter is not None and not is_callback_check_partial(event_filter): raise HomeAssistantError(f"Event filter {event_filter} is not a callback") - if run_immediately and not is_callback_check_partial(listener): - raise HomeAssistantError(f"Event listener {listener} is not a callback") + if run_immediately: + if not is_callback_check_partial(listener): + raise HomeAssistantError(f"Event listener {listener} is not a callback") + job_type = HassJobType.Callback return self._async_listen_filterable_job( event_type, - (HassJob(listener, f"listen {event_type}"), event_filter, run_immediately), + ( + HassJob(listener, f"listen {event_type}", job_type=job_type), + event_filter, + run_immediately, + ), ) @callback @@ -1234,7 +1242,11 @@ class EventBus: ) filterable_job = ( - HassJob(_onetime_listener, f"onetime listen {event_type} {listener}"), + HassJob( + _onetime_listener, + f"onetime listen {event_type} {listener}", + job_type=HassJobType.Callback, + ), None, False, ) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index ab0fc25f04d..648e0e5bd09 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -24,6 +24,7 @@ from homeassistant.const import ( from homeassistant.core import ( CALLBACK_TYPE, HassJob, + HassJobType, HomeAssistant, State, callback, @@ -1376,6 +1377,7 @@ def async_track_point_in_time( utc_converter, name=f"{job.name} UTC converter", cancel_on_shutdown=job.cancel_on_shutdown, + job_type=HassJobType.Callback, ) return async_track_point_in_utc_time(hass, track_job, point_in_time) @@ -1531,7 +1533,10 @@ def async_track_time_interval( job_name = f"track time interval {interval} {action}" interval_listener_job = HassJob( - interval_listener, job_name, cancel_on_shutdown=cancel_on_shutdown + interval_listener, + job_name, + cancel_on_shutdown=cancel_on_shutdown, + job_type=HassJobType.Callback, ) remove = async_call_later(hass, interval_seconds, interval_listener_job) @@ -1703,6 +1708,7 @@ def async_track_utc_time_change( pattern_time_change_listener_job = HassJob( pattern_time_change_listener, f"time change listener {hour}:{minute}:{second} {action}", + job_type=HassJobType.Callback, ) time_listener = async_track_point_in_utc_time( hass, pattern_time_change_listener_job, calculate_next(dt_util.utcnow()) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 2b570009a57..b74c22c9ead 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -16,7 +16,14 @@ import requests from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HassJob, + HassJobType, + HomeAssistant, + callback, +) from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryError, @@ -104,7 +111,11 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): job_name += f" {name}" if entry := self.config_entry: job_name += f" {entry.title} {entry.domain} {entry.entry_id}" - self._job = HassJob(self._handle_refresh_interval, job_name) + self._job = HassJob( + self._handle_refresh_interval, + job_name, + job_type=HassJobType.Coroutinefunction, + ) self._unsub_refresh: CALLBACK_TYPE | None = None self._unsub_shutdown: CALLBACK_TYPE | None = None self._request_refresh_task: asyncio.TimerHandle | None = None diff --git a/tests/test_core.py b/tests/test_core.py index 9fed1141a76..43291c032d7 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -816,6 +816,16 @@ async def test_eventbus_run_immediately(hass: HomeAssistant) -> None: unsub() +async def test_eventbus_run_immediately_not_callback(hass: HomeAssistant) -> None: + """Test we raise when passing a non-callback with run_immediately.""" + + def listener(event): + """Mock listener.""" + + with pytest.raises(HomeAssistantError): + hass.bus.async_listen("test", listener, run_immediately=True) + + async def test_eventbus_unsubscribe_listener(hass: HomeAssistant) -> None: """Test unsubscribe listener from returned function.""" calls = [] @@ -2534,3 +2544,25 @@ def test_is_callback_check_partial(): assert HassJob(ha.callback(functools.partial(not_callback_func))).job_type == ( ha.HassJobType.Executor ) + + +def test_hassjob_passing_job_type(): + """Test passing the job type to HassJob when we already know it.""" + + @ha.callback + def callback_func(): + pass + + def not_callback_func(): + pass + + assert ( + HassJob(callback_func, job_type=ha.HassJobType.Callback).job_type + == ha.HassJobType.Callback + ) + + # We should trust the job_type passed in + assert ( + HassJob(not_callback_func, job_type=ha.HassJobType.Callback).job_type + == ha.HassJobType.Callback + )