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
This commit is contained in:
J. Nick Koston 2024-01-16 03:05:01 -10:00 committed by GitHub
parent 26058bf922
commit 3d595fff13
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 54 additions and 19 deletions

View file

@ -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} comp_state: dict[str, Any] = {COMPRESSED_STATE_STATE: state.state}
if not no_attributes or state.domain in history.NEED_ATTRIBUTE_DOMAINS: if not no_attributes or state.domain in history.NEED_ATTRIBUTE_DOMAINS:
comp_state[COMPRESSED_STATE_ATTRIBUTES] = state.attributes comp_state[COMPRESSED_STATE_ATTRIBUTES] = state.attributes
comp_state[COMPRESSED_STATE_LAST_UPDATED] = dt_util.utc_to_timestamp( comp_state[COMPRESSED_STATE_LAST_UPDATED] = state.last_updated_timestamp
state.last_updated
)
if state.last_changed != state.last_updated: if state.last_changed != state.last_updated:
comp_state[COMPRESSED_STATE_LAST_CHANGED] = dt_util.utc_to_timestamp( comp_state[COMPRESSED_STATE_LAST_CHANGED] = state.last_changed_timestamp
state.last_changed
)
return comp_state return comp_state

View file

@ -16,7 +16,6 @@ from homeassistant.components.recorder.models import (
) )
from homeassistant.const import ATTR_ICON, EVENT_STATE_CHANGED from homeassistant.const import ATTR_ICON, EVENT_STATE_CHANGED
from homeassistant.core import Context, Event, State, callback 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.json import json_loads
from homeassistant.util.ulid import ulid_to_bytes 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_id_bin=ulid_to_bytes(context.id),
context_user_id_bin=uuid_hex_to_bytes_or_none(context.user_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), 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), row_id=hash(event),
) )
# States are prefiltered so we never get states # 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_id_bin=ulid_to_bytes(context.id),
context_user_id_bin=uuid_hex_to_bytes_or_none(context.user_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), 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), row_id=hash(event),
icon=new_state.attributes.get(ATTR_ICON), icon=new_state.attributes.get(ATTR_ICON),
) )

View file

@ -296,7 +296,7 @@ class Events(Base):
event_data=None, event_data=None,
origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin),
time_fired=None, 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=None,
context_id_bin=ulid_to_bytes_or_none(event.context.id), context_id_bin=ulid_to_bytes_or_none(event.context.id),
context_user_id=None, context_user_id=None,
@ -495,16 +495,16 @@ class States(Base):
# None state means the state was removed from the state machine # None state means the state was removed from the state machine
if state is None: if state is None:
dbstate.state = "" 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 dbstate.last_changed_ts = None
return dbstate return dbstate
dbstate.state = state.state 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: if state.last_updated == state.last_changed:
dbstate.last_changed_ts = None dbstate.last_changed_ts = None
else: else:
dbstate.last_changed_ts = dt_util.utc_to_timestamp(state.last_changed) dbstate.last_changed_ts = state.last_changed_timestamp
return dbstate return dbstate

View file

@ -183,9 +183,9 @@ def _state_diff(
if old_state.state != new_state.state: if old_state.state != new_state.state:
additions[COMPRESSED_STATE_STATE] = new_state.state additions[COMPRESSED_STATE_STATE] = new_state.state
if old_state.last_changed != new_state.last_changed: 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: 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: if old_state_context.parent_id != new_state_context.parent_id:
additions[COMPRESSED_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: if old_state_context.user_id != new_state_context.user_id:

View file

@ -1077,6 +1077,11 @@ class Event:
if not context.origin_event: if not context.origin_event:
context.origin_event = self 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 @cached_property
def _as_dict(self) -> dict[str, Any]: def _as_dict(self) -> dict[str, Any]:
"""Create a dict representation of this Event. """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 @cached_property
def _as_dict(self) -> dict[str, Any]: def _as_dict(self) -> dict[str, Any]:
"""Return a dict representation of the State. """Return a dict representation of the State.
@ -1526,12 +1541,12 @@ class State:
COMPRESSED_STATE_STATE: self.state, COMPRESSED_STATE_STATE: self.state,
COMPRESSED_STATE_ATTRIBUTES: self.attributes, COMPRESSED_STATE_ATTRIBUTES: self.attributes,
COMPRESSED_STATE_CONTEXT: context, 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: if self.last_changed != self.last_updated:
compressed_state[COMPRESSED_STATE_LAST_UPDATED] = dt_util.utc_to_timestamp( compressed_state[
self.last_updated COMPRESSED_STATE_LAST_UPDATED
) ] = self.last_updated_timestamp
return compressed_state return compressed_state
@cached_property @cached_property

View file

@ -625,6 +625,14 @@ def test_event_eq() -> None:
assert event1.as_dict() == event2.as_dict() 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: def test_event_json_fragment() -> None:
"""Test event JSON fragments.""" """Test event JSON fragments."""
now = dt_util.utcnow() 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( async def test_state_firing_event_matches_context_id_ulid_time(
hass: HomeAssistant, hass: HomeAssistant,
) -> None: ) -> None: