Improve isoformat timestamp performance (#36991)

* adj

* time_fired_isoformat

* remove unused code

* tests for processing timestamps

* restore missing import lost in merge conflict

* test for None case
This commit is contained in:
J. Nick Koston 2020-06-22 12:06:02 -05:00 committed by GitHub
parent 5446641f09
commit 53a91ece4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 125 additions and 13 deletions

View File

@ -13,7 +13,11 @@ import voluptuous as vol
from homeassistant.components import recorder from homeassistant.components import recorder
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.components.recorder.models import States, process_timestamp from homeassistant.components.recorder.models import (
States,
process_timestamp,
process_timestamp_to_utc_isoformat,
)
from homeassistant.components.recorder.util import execute, session_scope from homeassistant.components.recorder.util import execute, session_scope
from homeassistant.const import ( from homeassistant.const import (
ATTR_HIDDEN, ATTR_HIDDEN,
@ -318,7 +322,7 @@ def _sorted_states_to_json(
# Called in a tight loop so cache the function # Called in a tight loop so cache the function
# here # here
_process_timestamp = process_timestamp _process_timestamp_to_utc_isoformat = process_timestamp_to_utc_isoformat
# Append all changes to it # Append all changes to it
for ent_id, group in groupby(states, lambda state: state.entity_id): for ent_id, group in groupby(states, lambda state: state.entity_id):
@ -362,9 +366,9 @@ def _sorted_states_to_json(
ent_results.append( ent_results.append(
{ {
STATE_KEY: db_state.state, STATE_KEY: db_state.state,
LAST_CHANGED_KEY: _process_timestamp( LAST_CHANGED_KEY: _process_timestamp_to_utc_isoformat(
db_state.last_changed db_state.last_changed
).isoformat(), ),
} }
) )
prev_state = db_state prev_state = db_state

View File

@ -11,7 +11,12 @@ import voluptuous as vol
from homeassistant.components import sun from homeassistant.components import sun
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.components.recorder.models import Events, States, process_timestamp from homeassistant.components.recorder.models import (
Events,
States,
process_timestamp,
process_timestamp_to_utc_isoformat,
)
from homeassistant.components.recorder.util import ( from homeassistant.components.recorder.util import (
QUERY_RETRY_WAIT, QUERY_RETRY_WAIT,
RETRIES, RETRIES,
@ -248,7 +253,7 @@ def humanify(hass, events, entity_attr_cache, prev_states=None):
if event.event_type in external_events: if event.event_type in external_events:
domain, describe_event = external_events[event.event_type] domain, describe_event = external_events[event.event_type]
data = describe_event(event) data = describe_event(event)
data["when"] = event.time_fired data["when"] = event.time_fired_isoformat
data["domain"] = domain data["domain"] = domain
data["context_user_id"] = event.context_user_id data["context_user_id"] = event.context_user_id
yield data yield data
@ -275,7 +280,7 @@ def humanify(hass, events, entity_attr_cache, prev_states=None):
) or split_entity_id(entity_id)[1].replace("_", " ") ) or split_entity_id(entity_id)[1].replace("_", " ")
yield { yield {
"when": event.time_fired, "when": event.time_fired_isoformat,
"name": name, "name": name,
"message": _entry_message_from_event( "message": _entry_message_from_event(
hass, entity_id, domain, event, entity_attr_cache hass, entity_id, domain, event, entity_attr_cache
@ -290,7 +295,7 @@ def humanify(hass, events, entity_attr_cache, prev_states=None):
continue continue
yield { yield {
"when": event.time_fired, "when": event.time_fired_isoformat,
"name": "Home Assistant", "name": "Home Assistant",
"message": "started", "message": "started",
"domain": HA_DOMAIN, "domain": HA_DOMAIN,
@ -304,7 +309,7 @@ def humanify(hass, events, entity_attr_cache, prev_states=None):
action = "stopped" action = "stopped"
yield { yield {
"when": event.time_fired, "when": event.time_fired_isoformat,
"name": "Home Assistant", "name": "Home Assistant",
"message": action, "message": action,
"domain": HA_DOMAIN, "domain": HA_DOMAIN,
@ -322,7 +327,7 @@ def humanify(hass, events, entity_attr_cache, prev_states=None):
pass pass
yield { yield {
"when": event.time_fired, "when": event.time_fired_isoformat,
"name": event_data.get(ATTR_NAME), "name": event_data.get(ATTR_NAME),
"message": event_data.get(ATTR_MESSAGE), "message": event_data.get(ATTR_MESSAGE),
"domain": domain, "domain": domain,
@ -601,6 +606,7 @@ class LazyEventPartialState:
"_row", "_row",
"_event_data", "_event_data",
"_time_fired", "_time_fired",
"_time_fired_isoformat",
"_attributes", "_attributes",
"event_type", "event_type",
"entity_id", "entity_id",
@ -613,6 +619,7 @@ class LazyEventPartialState:
self._row = row self._row = row
self._event_data = None self._event_data = None
self._time_fired = None self._time_fired = None
self._time_fired_isoformat = None
self._attributes = None self._attributes = None
self.event_type = self._row.event_type self.event_type = self._row.event_type
self.entity_id = self._row.entity_id self.entity_id = self._row.entity_id
@ -662,6 +669,18 @@ class LazyEventPartialState:
) )
return self._time_fired return self._time_fired
@property
def time_fired_isoformat(self):
"""Time event was fired in utc isoformat."""
if not self._time_fired_isoformat:
if self._time_fired:
self._time_fired_isoformat = self._time_fired.isoformat()
else:
self._time_fired_isoformat = process_timestamp_to_utc_isoformat(
self._row.time_fired or dt_util.utcnow()
)
return self._time_fired_isoformat
@property @property
def has_old_and_new_state(self): def has_old_and_new_state(self):
"""Check the json data to see if new_state and old_state is present without decoding.""" """Check the json data to see if new_state and old_state is present without decoding."""

View File

@ -28,7 +28,7 @@ SCHEMA_VERSION = 8
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DB_TIMEZONE = "Z" DB_TIMEZONE = "+00:00"
class Events(Base): # type: ignore class Events(Base): # type: ignore
@ -202,3 +202,13 @@ def process_timestamp(ts):
return ts.replace(tzinfo=dt_util.UTC) return ts.replace(tzinfo=dt_util.UTC)
return dt_util.as_utc(ts) return dt_util.as_utc(ts)
def process_timestamp_to_utc_isoformat(ts):
"""Process a timestamp into UTC isotime."""
if ts is None:
return None
if ts.tzinfo is None:
return f"{ts.isoformat()}{DB_TIMEZONE}"
return dt_util.as_utc(ts).isoformat()

View File

@ -13,6 +13,7 @@ import voluptuous as vol
from homeassistant.components import logbook, recorder, sun from homeassistant.components import logbook, recorder, sun
from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME
from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED
from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat
from homeassistant.components.script import EVENT_SCRIPT_STARTED from homeassistant.components.script import EVENT_SCRIPT_STARTED
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
@ -1230,7 +1231,7 @@ class TestComponentLogbook(unittest.TestCase):
): ):
"""Assert an entry is what is expected.""" """Assert an entry is what is expected."""
if when: if when:
assert when == entry["when"] assert when.isoformat() == entry["when"]
if name: if name:
assert name == entry["name"] assert name == entry["name"]
@ -1639,3 +1640,8 @@ class MockLazyEventPartialState(ha.Event):
def context_user_id(self): def context_user_id(self):
"""Context user id of event.""" """Context user id of event."""
return self.context.user_id return self.context.user_id
@property
def time_fired_isoformat(self):
"""Time event was fired in utc isoformat."""
return process_timestamp_to_utc_isoformat(self.time_fired)

View File

@ -3,10 +3,18 @@ from datetime import datetime
import unittest import unittest
import pytest import pytest
import pytz
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.orm import scoped_session, sessionmaker
from homeassistant.components.recorder.models import Base, Events, RecorderRuns, States from homeassistant.components.recorder.models import (
Base,
Events,
RecorderRuns,
States,
process_timestamp,
process_timestamp_to_utc_isoformat,
)
from homeassistant.const import EVENT_STATE_CHANGED from homeassistant.const import EVENT_STATE_CHANGED
import homeassistant.core as ha import homeassistant.core as ha
from homeassistant.exceptions import InvalidEntityFormatError from homeassistant.exceptions import InvalidEntityFormatError
@ -165,3 +173,68 @@ def test_states_from_native_invalid_entity_id():
state = state.to_native(validate_entity_id=False) state = state.to_native(validate_entity_id=False)
assert state.entity_id == "test.invalid__id" assert state.entity_id == "test.invalid__id"
async def test_process_timestamp():
"""Test processing time stamp to UTC."""
datetime_with_tzinfo = datetime(2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC)
datetime_without_tzinfo = datetime(2016, 7, 9, 11, 0, 0)
est = pytz.timezone("US/Eastern")
datetime_est_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=est)
nst = pytz.timezone("Canada/Newfoundland")
datetime_nst_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=nst)
hst = pytz.timezone("US/Hawaii")
datetime_hst_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=hst)
assert process_timestamp(datetime_with_tzinfo) == datetime(
2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC
)
assert process_timestamp(datetime_without_tzinfo) == datetime(
2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC
)
assert process_timestamp(datetime_est_timezone) == datetime(
2016, 7, 9, 15, 56, tzinfo=dt.UTC
)
assert process_timestamp(datetime_nst_timezone) == datetime(
2016, 7, 9, 14, 31, tzinfo=dt.UTC
)
assert process_timestamp(datetime_hst_timezone) == datetime(
2016, 7, 9, 21, 31, tzinfo=dt.UTC
)
assert process_timestamp(None) is None
async def test_process_timestamp_to_utc_isoformat():
"""Test processing time stamp to UTC isoformat."""
datetime_with_tzinfo = datetime(2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC)
datetime_without_tzinfo = datetime(2016, 7, 9, 11, 0, 0)
est = pytz.timezone("US/Eastern")
datetime_est_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=est)
est = pytz.timezone("US/Eastern")
datetime_est_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=est)
nst = pytz.timezone("Canada/Newfoundland")
datetime_nst_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=nst)
hst = pytz.timezone("US/Hawaii")
datetime_hst_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=hst)
assert (
process_timestamp_to_utc_isoformat(datetime_with_tzinfo)
== "2016-07-09T11:00:00+00:00"
)
assert (
process_timestamp_to_utc_isoformat(datetime_without_tzinfo)
== "2016-07-09T11:00:00+00:00"
)
assert (
process_timestamp_to_utc_isoformat(datetime_est_timezone)
== "2016-07-09T15:56:00+00:00"
)
assert (
process_timestamp_to_utc_isoformat(datetime_nst_timezone)
== "2016-07-09T14:31:00+00:00"
)
assert (
process_timestamp_to_utc_isoformat(datetime_hst_timezone)
== "2016-07-09T21:31:00+00:00"
)
assert process_timestamp_to_utc_isoformat(None) is None