Small speed up to frequently called datetime functions (#85399)

This commit is contained in:
J. Nick Koston 2023-01-08 09:42:29 -10:00 committed by GitHub
parent 45eb1efc6f
commit d81febd3f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 49 additions and 35 deletions

View file

@ -4,6 +4,7 @@ from __future__ import annotations
import bisect import bisect
from contextlib import suppress from contextlib import suppress
import datetime as dt import datetime as dt
from functools import partial
import platform import platform
import re import re
import time import time
@ -98,9 +99,10 @@ def get_time_zone(time_zone_str: str) -> dt.tzinfo | None:
return None return None
def utcnow() -> dt.datetime: # We use a partial here since it is implemented in native code
"""Get now in UTC time.""" # and avoids the global lookup of UTC
return dt.datetime.now(UTC) utcnow: partial[dt.datetime] = partial(dt.datetime.now, UTC)
utcnow.__doc__ = "Get now in UTC time."
def now(time_zone: dt.tzinfo | None = None) -> dt.datetime: def now(time_zone: dt.tzinfo | None = None) -> dt.datetime:
@ -466,8 +468,8 @@ def _datetime_ambiguous(dattim: dt.datetime) -> bool:
return _datetime_exists(dattim) and dattim.utcoffset() != opposite_fold.utcoffset() return _datetime_exists(dattim) and dattim.utcoffset() != opposite_fold.utcoffset()
def __monotonic_time_coarse() -> float: def __gen_monotonic_time_coarse() -> partial[float]:
"""Return a monotonic time in seconds. """Return a function that provides monotonic time in seconds.
This is the coarse version of time_monotonic, which is faster but less accurate. This is the coarse version of time_monotonic, which is faster but less accurate.
@ -477,13 +479,16 @@ def __monotonic_time_coarse() -> float:
https://lore.kernel.org/lkml/20170404171826.25030-1-marc.zyngier@arm.com/ https://lore.kernel.org/lkml/20170404171826.25030-1-marc.zyngier@arm.com/
""" """
return time.clock_gettime(CLOCK_MONOTONIC_COARSE) # We use a partial here since its implementation is in native code
# which allows us to avoid the overhead of the global lookup
# of CLOCK_MONOTONIC_COARSE.
return partial(time.clock_gettime, CLOCK_MONOTONIC_COARSE)
monotonic_time_coarse = time.monotonic monotonic_time_coarse = time.monotonic
with suppress(Exception): with suppress(Exception):
if ( if (
platform.system() == "Linux" platform.system() == "Linux"
and abs(time.monotonic() - __monotonic_time_coarse()) < 1 and abs(time.monotonic() - __gen_monotonic_time_coarse()()) < 1
): ):
monotonic_time_coarse = __monotonic_time_coarse monotonic_time_coarse = __gen_monotonic_time_coarse()

View file

@ -5,7 +5,7 @@ import asyncio
from collections import OrderedDict from collections import OrderedDict
from collections.abc import Awaitable, Callable, Collection from collections.abc import Awaitable, Callable, Collection
from contextlib import contextmanager from contextlib import contextmanager
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
import functools as ft import functools as ft
from io import StringIO from io import StringIO
import json import json
@ -396,7 +396,7 @@ def async_fire_time_changed_exact(
approach, as this is only for testing. approach, as this is only for testing.
""" """
if datetime_ is None: if datetime_ is None:
utc_datetime = date_util.utcnow() utc_datetime = datetime.now(timezone.utc)
else: else:
utc_datetime = date_util.as_utc(datetime_) utc_datetime = date_util.as_utc(datetime_)
@ -418,7 +418,7 @@ def async_fire_time_changed(
for an exact microsecond, use async_fire_time_changed_exact. for an exact microsecond, use async_fire_time_changed_exact.
""" """
if datetime_ is None: if datetime_ is None:
utc_datetime = date_util.utcnow() utc_datetime = datetime.now(timezone.utc)
else: else:
utc_datetime = date_util.as_utc(datetime_) utc_datetime = date_util.as_utc(datetime_)

View file

@ -7,7 +7,6 @@ from datetime import datetime, timedelta
import json import json
from unittest.mock import patch, sentinel from unittest.mock import patch, sentinel
from freezegun import freeze_time
import pytest import pytest
from sqlalchemy import text from sqlalchemy import text
@ -973,6 +972,7 @@ def test_state_changes_during_period_multiple_entities_single_test(hass_recorder
hist[entity_id][0].state == value hist[entity_id][0].state == value
@pytest.mark.freeze_time("2039-01-19 03:14:07.555555-00:00")
async def test_get_full_significant_states_past_year_2038( async def test_get_full_significant_states_past_year_2038(
async_setup_recorder_instance: SetupRecorderInstanceT, async_setup_recorder_instance: SetupRecorderInstanceT,
hass: ha.HomeAssistant, hass: ha.HomeAssistant,
@ -980,29 +980,29 @@ async def test_get_full_significant_states_past_year_2038(
"""Test we can store times past year 2038.""" """Test we can store times past year 2038."""
await async_setup_recorder_instance(hass, {}) await async_setup_recorder_instance(hass, {})
past_2038_time = dt_util.parse_datetime("2039-01-19 03:14:07.555555-00:00") past_2038_time = dt_util.parse_datetime("2039-01-19 03:14:07.555555-00:00")
hass.states.async_set("sensor.one", "on", {"attr": "original"})
state0 = hass.states.get("sensor.one")
await hass.async_block_till_done()
with freeze_time(past_2038_time): hass.states.async_set("sensor.one", "on", {"attr": "new"})
hass.states.async_set("sensor.one", "on", {"attr": "original"}) state1 = hass.states.get("sensor.one")
state0 = hass.states.get("sensor.one")
await hass.async_block_till_done()
hass.states.async_set("sensor.one", "on", {"attr": "new"})
state1 = hass.states.get("sensor.one")
await async_wait_recording_done(hass)
def _get_entries(): await async_wait_recording_done(hass)
with session_scope(hass=hass) as session:
return history.get_full_significant_states_with_session(
hass,
session,
past_2038_time - timedelta(days=365),
past_2038_time + timedelta(days=365),
entity_ids=["sensor.one"],
significant_changes_only=False,
)
states = await recorder.get_instance(hass).async_add_executor_job(_get_entries) def _get_entries():
sensor_one_states: list[State] = states["sensor.one"] with session_scope(hass=hass) as session:
assert sensor_one_states[0] == state0 return history.get_full_significant_states_with_session(
assert sensor_one_states[1] == state1 hass,
assert sensor_one_states[0].last_changed == past_2038_time session,
assert sensor_one_states[0].last_updated == past_2038_time past_2038_time - timedelta(days=365),
past_2038_time + timedelta(days=365),
entity_ids=["sensor.one"],
significant_changes_only=False,
)
states = await recorder.get_instance(hass).async_add_executor_job(_get_entries)
sensor_one_states: list[State] = states["sensor.one"]
assert sensor_one_states[0] == state0
assert sensor_one_states[1] == state1
assert sensor_one_states[0].last_changed == past_2038_time
assert sensor_one_states[0].last_updated == past_2038_time

View file

@ -4,6 +4,7 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import AsyncGenerator, Callable, Generator from collections.abc import AsyncGenerator, Callable, Generator
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import datetime
import functools import functools
import gc import gc
import itertools import itertools
@ -78,6 +79,14 @@ asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False))
asyncio.set_event_loop_policy = lambda policy: None asyncio.set_event_loop_policy = lambda policy: None
def _utcnow():
"""Make utcnow patchable by freezegun."""
return datetime.datetime.now(datetime.timezone.utc)
dt_util.utcnow = _utcnow
def pytest_addoption(parser): def pytest_addoption(parser):
"""Register custom pytest options.""" """Register custom pytest options."""
parser.addoption("--dburl", action="store", default="sqlite://") parser.addoption("--dburl", action="store", default="sqlite://")