From 1589c06203c0bc9f87adcc97fe34d5c52aaf403a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 08:35:08 -0500 Subject: [PATCH] Significantly reduce clock_gettime syscalls on platforms with broken vdso (#81257) --- .../bluetooth/active_update_coordinator.py | 6 ++--- homeassistant/components/bluetooth/manager.py | 4 +-- homeassistant/components/bluetooth/scanner.py | 4 +-- homeassistant/components/bluetooth/util.py | 4 +-- .../components/esphome/bluetooth/scanner.py | 3 ++- homeassistant/util/dt.py | 26 +++++++++++++++++++ tests/util/test_dt.py | 6 +++++ 7 files changed, 43 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/bluetooth/active_update_coordinator.py b/homeassistant/components/bluetooth/active_update_coordinator.py index 37f049d3e07..ab26a0260f3 100644 --- a/homeassistant/components/bluetooth/active_update_coordinator.py +++ b/homeassistant/components/bluetooth/active_update_coordinator.py @@ -3,13 +3,13 @@ from __future__ import annotations from collections.abc import Callable, Coroutine import logging -import time from typing import Any, Generic, TypeVar from bleak import BleakError from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer +from homeassistant.util.dt import monotonic_time_coarse from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .passive_update_processor import PassiveBluetoothProcessorCoordinator @@ -94,7 +94,7 @@ class ActiveBluetoothProcessorCoordinator( """Return true if time to try and poll.""" poll_age: float | None = None if self._last_poll: - poll_age = time.monotonic() - self._last_poll + poll_age = monotonic_time_coarse() - self._last_poll return self._needs_poll_method(service_info, poll_age) async def _async_poll_data( @@ -124,7 +124,7 @@ class ActiveBluetoothProcessorCoordinator( self.last_poll_successful = False return finally: - self._last_poll = time.monotonic() + self._last_poll = monotonic_time_coarse() if not self.last_poll_successful: self.logger.debug("%s: Polling recovered") diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index aaefd3dcfc4..c3a0e0998f1 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -7,7 +7,6 @@ from dataclasses import replace from datetime import datetime, timedelta import itertools import logging -import time from typing import TYPE_CHECKING, Any, Final from bleak.backends.scanner import AdvertisementDataCallback @@ -22,6 +21,7 @@ from homeassistant.core import ( ) from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.dt import monotonic_time_coarse from .advertisement_tracker import AdvertisementTracker from .const import ( @@ -69,7 +69,7 @@ APPLE_START_BYTES_WANTED: Final = { APPLE_DEVICE_ID_START_BYTE, } -MONOTONIC_TIME: Final = time.monotonic +MONOTONIC_TIME: Final = monotonic_time_coarse _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index fe795f7ace5..6b23cae0218 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -6,7 +6,6 @@ from collections.abc import Callable from datetime import datetime import logging import platform -import time from typing import Any import async_timeout @@ -22,6 +21,7 @@ from dbus_fast import InvalidMessageError from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.dt import monotonic_time_coarse from homeassistant.util.package import is_docker_env from .const import ( @@ -35,7 +35,7 @@ from .models import BaseHaScanner, BluetoothScanningMode, BluetoothServiceInfoBl from .util import adapter_human_name, async_reset_adapter OriginalBleakScanner = bleak.BleakScanner -MONOTONIC_TIME = time.monotonic +MONOTONIC_TIME = monotonic_time_coarse # or_patterns is a workaround for the fact that passive scanning # needs at least one matcher to be set. The below matcher diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 860428a6106..181796d3d2d 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -2,11 +2,11 @@ from __future__ import annotations import platform -import time from bluetooth_auto_recovery import recover_adapter from homeassistant.core import callback +from homeassistant.util.dt import monotonic_time_coarse from .const import ( DEFAULT_ADAPTER_BY_PLATFORM, @@ -29,7 +29,7 @@ async def async_load_history_from_system() -> dict[str, BluetoothServiceInfoBlea bluez_dbus = BlueZDBusObjects() await bluez_dbus.load() - now = time.monotonic() + now = monotonic_time_coarse() return { address: BluetoothServiceInfoBleak( name=history.advertisement_data.local_name diff --git a/homeassistant/components/esphome/bluetooth/scanner.py b/homeassistant/components/esphome/bluetooth/scanner.py index 284e605fdfa..7c8064d5583 100644 --- a/homeassistant/components/esphome/bluetooth/scanner.py +++ b/homeassistant/components/esphome/bluetooth/scanner.py @@ -19,6 +19,7 @@ from homeassistant.components.bluetooth import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.dt import monotonic_time_coarse TWO_CHAR = re.compile("..") @@ -84,7 +85,7 @@ class ESPHomeScanner(BaseHaScanner): @callback def async_on_advertisement(self, adv: BluetoothLEAdvertisement) -> None: """Call the registered callback.""" - now = time.monotonic() + now = monotonic_time_coarse() address = ":".join(TWO_CHAR.findall("%012X" % adv.address)) # must be upper name = adv.name if prev_discovery := self._discovered_device_advertisement_datas.get(address): diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 80b322c1a14..44e4403d689 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -4,7 +4,9 @@ from __future__ import annotations import bisect from contextlib import suppress import datetime as dt +import platform import re +import time from typing import Any import zoneinfo @@ -13,6 +15,7 @@ import ciso8601 DATE_STR_FORMAT = "%Y-%m-%d" UTC = dt.timezone.utc DEFAULT_TIME_ZONE: dt.tzinfo = dt.timezone.utc +CLOCK_MONOTONIC_COARSE = 6 # EPOCHORDINAL is not exposed as a constant # https://github.com/python/cpython/blob/3.10/Lib/zoneinfo/_zoneinfo.py#L12 @@ -461,3 +464,26 @@ def _datetime_ambiguous(dattim: dt.datetime) -> bool: assert dattim.tzinfo is not None opposite_fold = dattim.replace(fold=not dattim.fold) return _datetime_exists(dattim) and dattim.utcoffset() != opposite_fold.utcoffset() + + +def __monotonic_time_coarse() -> float: + """Return a monotonic time in seconds. + + This is the coarse version of time_monotonic, which is faster but less accurate. + + Since many arm64 and 32-bit platforms don't support VDSO with time.monotonic + because of errata, we can't rely on the kernel to provide a fast + monotonic time. + + https://lore.kernel.org/lkml/20170404171826.25030-1-marc.zyngier@arm.com/ + """ + return 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 + ): + monotonic_time_coarse = __monotonic_time_coarse diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index 79cd4e5e0df..e902176bb35 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import datetime, timedelta +import time import pytest @@ -719,3 +720,8 @@ def test_find_next_time_expression_tenth_second_pattern_does_not_drift_entering_ assert (next_target - prev_target).total_seconds() == 60 assert next_target.second == 10 prev_target = next_target + + +def test_monotonic_time_coarse(): + """Test monotonic time coarse.""" + assert abs(time.monotonic() - dt_util.monotonic_time_coarse()) < 1