From d81febd3f4e8bfe6da0caa12403cf04bffe0abb9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Jan 2023 09:42:29 -1000 Subject: [PATCH] Small speed up to frequently called datetime functions (#85399) --- homeassistant/util/dt.py | 21 ++++++---- tests/common.py | 6 +-- tests/components/recorder/test_history.py | 48 +++++++++++------------ tests/conftest.py | 9 +++++ 4 files changed, 49 insertions(+), 35 deletions(-) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 44e4403d689..3e9ae088296 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -4,6 +4,7 @@ from __future__ import annotations import bisect from contextlib import suppress import datetime as dt +from functools import partial import platform import re import time @@ -98,9 +99,10 @@ def get_time_zone(time_zone_str: str) -> dt.tzinfo | None: return None -def utcnow() -> dt.datetime: - """Get now in UTC time.""" - return dt.datetime.now(UTC) +# We use a partial here since it is implemented in native code +# and avoids the global lookup of 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: @@ -466,8 +468,8 @@ def _datetime_ambiguous(dattim: dt.datetime) -> bool: return _datetime_exists(dattim) and dattim.utcoffset() != opposite_fold.utcoffset() -def __monotonic_time_coarse() -> float: - """Return a monotonic time in seconds. +def __gen_monotonic_time_coarse() -> partial[float]: + """Return a function that provides monotonic time in seconds. 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/ """ - 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 with suppress(Exception): if ( 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() diff --git a/tests/common.py b/tests/common.py index eb7c7ba63ab..eaa31851f0c 100644 --- a/tests/common.py +++ b/tests/common.py @@ -5,7 +5,7 @@ import asyncio from collections import OrderedDict from collections.abc import Awaitable, Callable, Collection from contextlib import contextmanager -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import functools as ft from io import StringIO import json @@ -396,7 +396,7 @@ def async_fire_time_changed_exact( approach, as this is only for testing. """ if datetime_ is None: - utc_datetime = date_util.utcnow() + utc_datetime = datetime.now(timezone.utc) else: 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. """ if datetime_ is None: - utc_datetime = date_util.utcnow() + utc_datetime = datetime.now(timezone.utc) else: utc_datetime = date_util.as_utc(datetime_) diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index 913ae3d8bf6..0465c10a8d2 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -7,7 +7,6 @@ from datetime import datetime, timedelta import json from unittest.mock import patch, sentinel -from freezegun import freeze_time import pytest 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 +@pytest.mark.freeze_time("2039-01-19 03:14:07.555555-00:00") async def test_get_full_significant_states_past_year_2038( async_setup_recorder_instance: SetupRecorderInstanceT, 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.""" await async_setup_recorder_instance(hass, {}) 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": "original"}) - 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) + hass.states.async_set("sensor.one", "on", {"attr": "new"}) + state1 = hass.states.get("sensor.one") - def _get_entries(): - 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, - ) + await async_wait_recording_done(hass) - 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 + def _get_entries(): + 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) + 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 diff --git a/tests/conftest.py b/tests/conftest.py index 307f6626ba8..75655cf2d86 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import AsyncGenerator, Callable, Generator from contextlib import asynccontextmanager +import datetime import functools import gc import itertools @@ -78,6 +79,14 @@ asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False)) 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): """Register custom pytest options.""" parser.addoption("--dburl", action="store", default="sqlite://")