From 3d595fff13498b1eaf716bbd9fed8ee5e493df1b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 16 Jan 2024 03:05:01 -1000 Subject: [PATCH] Avoid duplicate timestamp conversions for websocket api and recorder (#108144) * Avoid duplicate timestamp conversions for websocket api and recorder We convert the time from datetime to timestamps one per open websocket connection and the recorder for every state update. Only do the conversion once since its ~30% of the cost of building the state diff * more * two more * two more in live history --- .../components/history/websocket_api.py | 8 ++---- homeassistant/components/logbook/models.py | 5 ++-- .../components/recorder/db_schema.py | 8 +++--- .../components/websocket_api/messages.py | 4 +-- homeassistant/core.py | 23 ++++++++++++++--- tests/test_core.py | 25 +++++++++++++++++++ 6 files changed, 54 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/history/websocket_api.py b/homeassistant/components/history/websocket_api.py index 4be63f29c02..5bc14cd4c02 100644 --- a/homeassistant/components/history/websocket_api.py +++ b/homeassistant/components/history/websocket_api.py @@ -302,13 +302,9 @@ def _history_compressed_state(state: State, no_attributes: bool) -> dict[str, An comp_state: dict[str, Any] = {COMPRESSED_STATE_STATE: state.state} if not no_attributes or state.domain in history.NEED_ATTRIBUTE_DOMAINS: comp_state[COMPRESSED_STATE_ATTRIBUTES] = state.attributes - comp_state[COMPRESSED_STATE_LAST_UPDATED] = dt_util.utc_to_timestamp( - state.last_updated - ) + comp_state[COMPRESSED_STATE_LAST_UPDATED] = state.last_updated_timestamp if state.last_changed != state.last_updated: - comp_state[COMPRESSED_STATE_LAST_CHANGED] = dt_util.utc_to_timestamp( - state.last_changed - ) + comp_state[COMPRESSED_STATE_LAST_CHANGED] = state.last_changed_timestamp return comp_state diff --git a/homeassistant/components/logbook/models.py b/homeassistant/components/logbook/models.py index 04a2458237f..84ae84a3b70 100644 --- a/homeassistant/components/logbook/models.py +++ b/homeassistant/components/logbook/models.py @@ -16,7 +16,6 @@ from homeassistant.components.recorder.models import ( ) from homeassistant.const import ATTR_ICON, EVENT_STATE_CHANGED from homeassistant.core import Context, Event, State, callback -import homeassistant.util.dt as dt_util from homeassistant.util.json import json_loads from homeassistant.util.ulid import ulid_to_bytes @@ -131,7 +130,7 @@ def async_event_to_row(event: Event) -> EventAsRow: context_id_bin=ulid_to_bytes(context.id), context_user_id_bin=uuid_hex_to_bytes_or_none(context.user_id), context_parent_id_bin=ulid_to_bytes_or_none(context.parent_id), - time_fired_ts=dt_util.utc_to_timestamp(event.time_fired), + time_fired_ts=event.time_fired_timestamp, row_id=hash(event), ) # States are prefiltered so we never get states @@ -147,7 +146,7 @@ def async_event_to_row(event: Event) -> EventAsRow: context_id_bin=ulid_to_bytes(context.id), context_user_id_bin=uuid_hex_to_bytes_or_none(context.user_id), context_parent_id_bin=ulid_to_bytes_or_none(context.parent_id), - time_fired_ts=dt_util.utc_to_timestamp(new_state.last_updated), + time_fired_ts=new_state.last_updated_timestamp, row_id=hash(event), icon=new_state.attributes.get(ATTR_ICON), ) diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index b864e104ae6..dff26214d67 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -296,7 +296,7 @@ class Events(Base): event_data=None, origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), time_fired=None, - time_fired_ts=dt_util.utc_to_timestamp(event.time_fired), + time_fired_ts=event.time_fired_timestamp, context_id=None, context_id_bin=ulid_to_bytes_or_none(event.context.id), context_user_id=None, @@ -495,16 +495,16 @@ class States(Base): # None state means the state was removed from the state machine if state is None: dbstate.state = "" - dbstate.last_updated_ts = dt_util.utc_to_timestamp(event.time_fired) + dbstate.last_updated_ts = event.time_fired_timestamp dbstate.last_changed_ts = None return dbstate dbstate.state = state.state - dbstate.last_updated_ts = dt_util.utc_to_timestamp(state.last_updated) + dbstate.last_updated_ts = state.last_updated_timestamp if state.last_updated == state.last_changed: dbstate.last_changed_ts = None else: - dbstate.last_changed_ts = dt_util.utc_to_timestamp(state.last_changed) + dbstate.last_changed_ts = state.last_changed_timestamp return dbstate diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index 1d3181fcf3a..55144217fdc 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -183,9 +183,9 @@ def _state_diff( if old_state.state != new_state.state: additions[COMPRESSED_STATE_STATE] = new_state.state if old_state.last_changed != new_state.last_changed: - additions[COMPRESSED_STATE_LAST_CHANGED] = new_state.last_changed.timestamp() + additions[COMPRESSED_STATE_LAST_CHANGED] = new_state.last_changed_timestamp elif old_state.last_updated != new_state.last_updated: - additions[COMPRESSED_STATE_LAST_UPDATED] = new_state.last_updated.timestamp() + additions[COMPRESSED_STATE_LAST_UPDATED] = new_state.last_updated_timestamp if old_state_context.parent_id != new_state_context.parent_id: additions[COMPRESSED_STATE_CONTEXT] = {"parent_id": new_state_context.parent_id} if old_state_context.user_id != new_state_context.user_id: diff --git a/homeassistant/core.py b/homeassistant/core.py index ebd40330d13..e65273538a8 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1077,6 +1077,11 @@ class Event: if not context.origin_event: context.origin_event = self + @cached_property + def time_fired_timestamp(self) -> float: + """Return time fired as a timestamp.""" + return self.time_fired.timestamp() + @cached_property def _as_dict(self) -> dict[str, Any]: """Create a dict representation of this Event. @@ -1445,6 +1450,16 @@ class State: "_", " " ) + @cached_property + def last_updated_timestamp(self) -> float: + """Timestamp of last update.""" + return self.last_updated.timestamp() + + @cached_property + def last_changed_timestamp(self) -> float: + """Timestamp of last change.""" + return self.last_changed.timestamp() + @cached_property def _as_dict(self) -> dict[str, Any]: """Return a dict representation of the State. @@ -1526,12 +1541,12 @@ class State: COMPRESSED_STATE_STATE: self.state, COMPRESSED_STATE_ATTRIBUTES: self.attributes, COMPRESSED_STATE_CONTEXT: context, - COMPRESSED_STATE_LAST_CHANGED: dt_util.utc_to_timestamp(self.last_changed), + COMPRESSED_STATE_LAST_CHANGED: self.last_changed_timestamp, } if self.last_changed != self.last_updated: - compressed_state[COMPRESSED_STATE_LAST_UPDATED] = dt_util.utc_to_timestamp( - self.last_updated - ) + compressed_state[ + COMPRESSED_STATE_LAST_UPDATED + ] = self.last_updated_timestamp return compressed_state @cached_property diff --git a/tests/test_core.py b/tests/test_core.py index 918f098eab7..c2a5a73e6ee 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -625,6 +625,14 @@ def test_event_eq() -> None: assert event1.as_dict() == event2.as_dict() +def test_event_time_fired_timestamp() -> None: + """Test time_fired_timestamp.""" + now = dt_util.utcnow() + event = ha.Event("some_type", {"some": "attr"}, time_fired=now) + assert event.time_fired_timestamp == now.timestamp() + assert event.time_fired_timestamp == now.timestamp() + + def test_event_json_fragment() -> None: """Test event JSON fragments.""" now = dt_util.utcnow() @@ -2453,6 +2461,23 @@ async def test_state_change_events_context_id_match_state_time( ) +def test_state_timestamps() -> None: + """Test timestamp functions for State.""" + now = dt_util.utcnow() + state = ha.State( + "light.bedroom", + "on", + {"brightness": 100}, + last_changed=now, + last_updated=now, + context=ha.Context(id="1234"), + ) + assert state.last_changed_timestamp == now.timestamp() + assert state.last_changed_timestamp == now.timestamp() + assert state.last_updated_timestamp == now.timestamp() + assert state.last_updated_timestamp == now.timestamp() + + async def test_state_firing_event_matches_context_id_ulid_time( hass: HomeAssistant, ) -> None: