Avoid hashing attributes when they are already in the cache (#68395)

This commit is contained in:
J. Nick Koston 2022-03-19 20:33:37 -10:00 committed by GitHub
parent a91888a7f8
commit 0c0df07c52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 59 additions and 41 deletions

View File

@ -985,7 +985,7 @@ class Recorder(threading.Thread):
if event.event_type == EVENT_STATE_CHANGED: if event.event_type == EVENT_STATE_CHANGED:
try: try:
dbstate = States.from_event(event) dbstate = States.from_event(event)
dbstate_attributes = StateAttributes.from_event(event) shared_attrs = StateAttributes.shared_attrs_from_event(event)
except (TypeError, ValueError) as ex: except (TypeError, ValueError) as ex:
_LOGGER.warning( _LOGGER.warning(
"State is not JSON serializable: %s: %s", "State is not JSON serializable: %s: %s",
@ -995,27 +995,33 @@ class Recorder(threading.Thread):
return return
dbstate.attributes = None dbstate.attributes = None
shared_attrs = dbstate_attributes.shared_attrs
# Matching attributes found in the pending commit # Matching attributes found in the pending commit
if pending_attributes := self._pending_state_attributes.get(shared_attrs): if pending_attributes := self._pending_state_attributes.get(shared_attrs):
dbstate.state_attributes = pending_attributes dbstate.state_attributes = pending_attributes
# Matching attributes id found in the cache # Matching attributes id found in the cache
elif attributes_id := self._state_attributes_ids.get(shared_attrs): elif attributes_id := self._state_attributes_ids.get(shared_attrs):
dbstate.attributes_id = attributes_id dbstate.attributes_id = attributes_id
# Matching attributes found in the database
elif (
attributes := self.event_session.query(StateAttributes.attributes_id)
.filter(StateAttributes.hash == dbstate_attributes.hash)
.filter(StateAttributes.shared_attrs == shared_attrs)
.first()
):
dbstate.attributes_id = attributes[0]
self._state_attributes_ids[shared_attrs] = attributes[0]
# No matching attributes found, save them in the DB
else: else:
dbstate.state_attributes = dbstate_attributes attr_hash = StateAttributes.hash_shared_attrs(shared_attrs)
self._pending_state_attributes[shared_attrs] = dbstate_attributes # Matching attributes found in the database
self.event_session.add(dbstate_attributes) if (
attributes := self.event_session.query(
StateAttributes.attributes_id
)
.filter(StateAttributes.hash == attr_hash)
.filter(StateAttributes.shared_attrs == shared_attrs)
.first()
):
dbstate.attributes_id = attributes[0]
self._state_attributes_ids[shared_attrs] = attributes[0]
# No matching attributes found, save them in the DB
else:
dbstate_attributes = StateAttributes(
shared_attrs=shared_attrs, hash=attr_hash
)
dbstate.state_attributes = dbstate_attributes
self._pending_state_attributes[shared_attrs] = dbstate_attributes
self.event_session.add(dbstate_attributes)
if old_state := self._old_states.pop(dbstate.entity_id, None): if old_state := self._old_states.pop(dbstate.entity_id, None):
if old_state.state_id: if old_state.state_id:

View File

@ -1,5 +1,11 @@
"""Recorder constants.""" """Recorder constants."""
from functools import partial
import json
from typing import Final
from homeassistant.helpers.json import JSONEncoder
DATA_INSTANCE = "recorder_instance" DATA_INSTANCE = "recorder_instance"
SQLITE_URL_PREFIX = "sqlite://" SQLITE_URL_PREFIX = "sqlite://"
DOMAIN = "recorder" DOMAIN = "recorder"
@ -17,3 +23,5 @@ MAX_QUEUE_BACKLOG = 30000
MAX_ROWS_TO_PURGE = 998 MAX_ROWS_TO_PURGE = 998
DB_WORKER_PREFIX = "DbWorker" DB_WORKER_PREFIX = "DbWorker"
JSON_DUMP: Final = partial(json.dumps, cls=JSONEncoder, separators=(",", ":"))

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from datetime import datetime, timedelta from datetime import datetime, timedelta
import json import json
import logging import logging
from typing import TypedDict, overload from typing import Any, TypedDict, overload
from fnvhash import fnv1a_32 from fnvhash import fnv1a_32
from sqlalchemy import ( from sqlalchemy import (
@ -35,9 +35,10 @@ from homeassistant.const import (
MAX_LENGTH_STATE_STATE, MAX_LENGTH_STATE_STATE,
) )
from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id
from homeassistant.helpers.json import JSONEncoder
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from .const import JSON_DUMP
# SQLAlchemy Schema # SQLAlchemy Schema
# pylint: disable=invalid-name # pylint: disable=invalid-name
Base = declarative_base() Base = declarative_base()
@ -116,8 +117,7 @@ class Events(Base): # type: ignore[misc,valid-type]
"""Create an event database object from a native event.""" """Create an event database object from a native event."""
return Events( return Events(
event_type=event.event_type, event_type=event.event_type,
event_data=event_data event_data=event_data or JSON_DUMP(event.data),
or json.dumps(event.data, cls=JSONEncoder, separators=(",", ":")),
origin=str(event.origin.value), origin=str(event.origin.value),
time_fired=event.time_fired, time_fired=event.time_fired,
context_id=event.context.id, context_id=event.context.id,
@ -186,15 +186,13 @@ class States(Base): # type: ignore[misc,valid-type]
) )
@staticmethod @staticmethod
def from_event(event): def from_event(event) -> States:
"""Create object from a state_changed event.""" """Create object from a state_changed event."""
entity_id = event.data["entity_id"] entity_id = event.data["entity_id"]
state = event.data.get("new_state") state: State | None = event.data.get("new_state")
dbstate = States(entity_id=entity_id, attributes=None)
dbstate = States(entity_id=entity_id) # None state means the state was removed from the state machine
dbstate.attributes = None
# State got deleted
if state is None: if state is None:
dbstate.state = "" dbstate.state = ""
dbstate.domain = split_entity_id(entity_id)[0] dbstate.domain = split_entity_id(entity_id)[0]
@ -208,7 +206,7 @@ class States(Base): # type: ignore[misc,valid-type]
return dbstate return dbstate
def to_native(self, validate_entity_id=True): def to_native(self, validate_entity_id: bool = True) -> State | None:
"""Convert to an HA state object.""" """Convert to an HA state object."""
try: try:
return State( return State(
@ -221,7 +219,7 @@ class States(Base): # type: ignore[misc,valid-type]
process_timestamp(self.last_updated), process_timestamp(self.last_updated),
# Join the events table on event_id to get the context instead # Join the events table on event_id to get the context instead
# as it will always be there for state_changed events # as it will always be there for state_changed events
context=Context(id=None), context=Context(id=None), # type: ignore[arg-type]
validate_entity_id=validate_entity_id, validate_entity_id=validate_entity_id,
) )
except ValueError: except ValueError:
@ -251,23 +249,29 @@ class StateAttributes(Base): # type: ignore[misc,valid-type]
) )
@staticmethod @staticmethod
def from_event(event): def from_event(event: Event) -> StateAttributes:
"""Create object from a state_changed event.""" """Create object from a state_changed event."""
state = event.data.get("new_state") state: State | None = event.data.get("new_state")
dbstate = StateAttributes() # None state means the state was removed from the state machine
# State got deleted dbstate = StateAttributes(
if state is None: shared_attrs="{}" if state is None else JSON_DUMP(state.attributes)
dbstate.shared_attrs = "{}" )
else: dbstate.hash = StateAttributes.hash_shared_attrs(dbstate.shared_attrs)
dbstate.shared_attrs = json.dumps(
dict(state.attributes),
cls=JSONEncoder,
separators=(",", ":"),
)
dbstate.hash = fnv1a_32(dbstate.shared_attrs.encode("utf-8"))
return dbstate return dbstate
def to_native(self): @staticmethod
def shared_attrs_from_event(event: Event) -> str:
"""Create shared_attrs from a state_changed event."""
state: State | None = event.data.get("new_state")
# None state means the state was removed from the state machine
return "{}" if state is None else JSON_DUMP(state.attributes)
@staticmethod
def hash_shared_attrs(shared_attrs: str) -> int:
"""Return the hash of json encoded shared attributes."""
return fnv1a_32(shared_attrs.encode("utf-8"))
def to_native(self) -> dict[str, Any]:
"""Convert to an HA state object.""" """Convert to an HA state object."""
try: try:
return json.loads(self.shared_attrs) return json.loads(self.shared_attrs)