Refactor integration startup time tracking to reduce overhead (#110136)
* Refactor integration startup time tracking to reduce overhead - Use monotonic time for watching integration startup time as it avoids incorrect values if time moves backwards because of ntp during startup and reduces many time conversions since we want durations in seconds and not local time - Use loop scheduling instead of a task - Moves all the dispatcher logic into the new _WatchPendingSetups * websocket as well * tweaks * simplify logic * preserve logic * preserve logic * lint * adjust
This commit is contained in:
parent
9bc130c131
commit
def6c5c21c
5 changed files with 79 additions and 59 deletions
|
@ -4,9 +4,10 @@ from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Coroutine
|
from collections.abc import Coroutine
|
||||||
import contextlib
|
import contextlib
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
|
from operator import itemgetter
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
|
@ -49,7 +50,6 @@ from .setup import (
|
||||||
async_set_domains_to_be_loaded,
|
async_set_domains_to_be_loaded,
|
||||||
async_setup_component,
|
async_setup_component,
|
||||||
)
|
)
|
||||||
from .util import dt as dt_util
|
|
||||||
from .util.logging import async_activate_log_queue_handler
|
from .util.logging import async_activate_log_queue_handler
|
||||||
from .util.package import async_get_user_site, is_virtual_env
|
from .util.package import async_get_user_site, is_virtual_env
|
||||||
|
|
||||||
|
@ -545,36 +545,70 @@ def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]:
|
||||||
return domains
|
return domains
|
||||||
|
|
||||||
|
|
||||||
async def _async_watch_pending_setups(hass: core.HomeAssistant) -> None:
|
class _WatchPendingSetups:
|
||||||
"""Periodic log of setups that are pending.
|
"""Periodic log and dispatch of setups that are pending."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, hass: core.HomeAssistant, setup_started: dict[str, float]
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the WatchPendingSetups class."""
|
||||||
|
self._hass = hass
|
||||||
|
self._setup_started = setup_started
|
||||||
|
self._duration_count = 0
|
||||||
|
self._handle: asyncio.TimerHandle | None = None
|
||||||
|
self._previous_was_empty = True
|
||||||
|
self._loop = hass.loop
|
||||||
|
|
||||||
|
def _async_watch(self) -> None:
|
||||||
|
"""Periodic log of setups that are pending."""
|
||||||
|
now = monotonic()
|
||||||
|
self._duration_count += SLOW_STARTUP_CHECK_INTERVAL
|
||||||
|
|
||||||
Pending for longer than LOG_SLOW_STARTUP_INTERVAL.
|
|
||||||
"""
|
|
||||||
loop_count = 0
|
|
||||||
setup_started: dict[str, datetime] = hass.data[DATA_SETUP_STARTED]
|
|
||||||
previous_was_empty = True
|
|
||||||
while True:
|
|
||||||
now = dt_util.utcnow()
|
|
||||||
remaining_with_setup_started = {
|
remaining_with_setup_started = {
|
||||||
domain: (now - setup_started[domain]).total_seconds()
|
domain: (now - start_time)
|
||||||
for domain in setup_started
|
for domain, start_time in self._setup_started.items()
|
||||||
}
|
}
|
||||||
_LOGGER.debug("Integration remaining: %s", remaining_with_setup_started)
|
_LOGGER.debug("Integration remaining: %s", remaining_with_setup_started)
|
||||||
if remaining_with_setup_started or not previous_was_empty:
|
if remaining_with_setup_started or not self._previous_was_empty:
|
||||||
async_dispatcher_send(
|
self._async_dispatch(remaining_with_setup_started)
|
||||||
hass, SIGNAL_BOOTSTRAP_INTEGRATIONS, remaining_with_setup_started
|
self._previous_was_empty = not remaining_with_setup_started
|
||||||
)
|
|
||||||
previous_was_empty = not remaining_with_setup_started
|
|
||||||
await asyncio.sleep(SLOW_STARTUP_CHECK_INTERVAL)
|
|
||||||
loop_count += SLOW_STARTUP_CHECK_INTERVAL
|
|
||||||
|
|
||||||
if loop_count >= LOG_SLOW_STARTUP_INTERVAL and setup_started:
|
if (
|
||||||
|
self._setup_started
|
||||||
|
and self._duration_count % LOG_SLOW_STARTUP_INTERVAL == 0
|
||||||
|
):
|
||||||
|
# We log every LOG_SLOW_STARTUP_INTERVAL until all integrations are done
|
||||||
|
# once we take over LOG_SLOW_STARTUP_INTERVAL (60s) to start up
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Waiting on integrations to complete setup: %s",
|
"Waiting on integrations to complete setup: %s",
|
||||||
", ".join(setup_started),
|
", ".join(self._setup_started),
|
||||||
)
|
)
|
||||||
loop_count = 0
|
|
||||||
_LOGGER.debug("Running timeout Zones: %s", hass.timeout.zones)
|
_LOGGER.debug("Running timeout Zones: %s", self._hass.timeout.zones)
|
||||||
|
self._async_schedule_next()
|
||||||
|
|
||||||
|
def _async_dispatch(self, remaining_with_setup_started: dict[str, float]) -> None:
|
||||||
|
"""Dispatch the signal."""
|
||||||
|
async_dispatcher_send(
|
||||||
|
self._hass, SIGNAL_BOOTSTRAP_INTEGRATIONS, remaining_with_setup_started
|
||||||
|
)
|
||||||
|
|
||||||
|
def _async_schedule_next(self) -> None:
|
||||||
|
"""Schedule the next call."""
|
||||||
|
self._handle = self._loop.call_later(
|
||||||
|
SLOW_STARTUP_CHECK_INTERVAL, self._async_watch
|
||||||
|
)
|
||||||
|
|
||||||
|
def async_start(self) -> None:
|
||||||
|
"""Start watching."""
|
||||||
|
self._async_schedule_next()
|
||||||
|
|
||||||
|
def async_stop(self) -> None:
|
||||||
|
"""Stop watching."""
|
||||||
|
self._async_dispatch({})
|
||||||
|
if self._handle:
|
||||||
|
self._handle.cancel()
|
||||||
|
self._handle = None
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_multi_components(
|
async def async_setup_multi_components(
|
||||||
|
@ -683,10 +717,12 @@ async def _async_set_up_integrations(
|
||||||
hass: core.HomeAssistant, config: dict[str, Any]
|
hass: core.HomeAssistant, config: dict[str, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up all the integrations."""
|
"""Set up all the integrations."""
|
||||||
hass.data[DATA_SETUP_STARTED] = {}
|
setup_started: dict[str, float] = {}
|
||||||
|
hass.data[DATA_SETUP_STARTED] = setup_started
|
||||||
setup_time: dict[str, timedelta] = hass.data.setdefault(DATA_SETUP_TIME, {})
|
setup_time: dict[str, timedelta] = hass.data.setdefault(DATA_SETUP_TIME, {})
|
||||||
|
|
||||||
watch_task = asyncio.create_task(_async_watch_pending_setups(hass))
|
watcher = _WatchPendingSetups(hass, setup_started)
|
||||||
|
watcher.async_start()
|
||||||
|
|
||||||
domains_to_setup, integration_cache = await _async_resolve_domains_to_setup(
|
domains_to_setup, integration_cache = await _async_resolve_domains_to_setup(
|
||||||
hass, config
|
hass, config
|
||||||
|
@ -782,15 +818,9 @@ async def _async_set_up_integrations(
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
_LOGGER.warning("Setup timed out for bootstrap - moving forward")
|
_LOGGER.warning("Setup timed out for bootstrap - moving forward")
|
||||||
|
|
||||||
watch_task.cancel()
|
watcher.async_stop()
|
||||||
async_dispatcher_send(hass, SIGNAL_BOOTSTRAP_INTEGRATIONS, {})
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Integration setup times: %s",
|
"Integration setup times: %s",
|
||||||
{
|
dict(sorted(setup_time.items(), key=itemgetter(1))),
|
||||||
integration: timedelta.total_seconds()
|
|
||||||
for integration, timedelta in sorted(
|
|
||||||
setup_time.items(), key=lambda item: item[1].total_seconds()
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
import datetime as dt
|
|
||||||
from functools import lru_cache, partial
|
from functools import lru_cache, partial
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
@ -540,13 +539,12 @@ def handle_integration_setup_info(
|
||||||
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle integrations command."""
|
"""Handle integrations command."""
|
||||||
|
setup_time: dict[str, float] = hass.data[DATA_SETUP_TIME]
|
||||||
connection.send_result(
|
connection.send_result(
|
||||||
msg["id"],
|
msg["id"],
|
||||||
[
|
[
|
||||||
{"domain": integration, "seconds": timedelta.total_seconds()}
|
{"domain": integration, "seconds": seconds}
|
||||||
for integration, timedelta in cast(
|
for integration, seconds in setup_time.items()
|
||||||
dict[str, dt.timedelta], hass.data[DATA_SETUP_TIME]
|
|
||||||
).items()
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable, Callable, Generator, Iterable
|
from collections.abc import Awaitable, Callable, Generator, Iterable
|
||||||
import contextlib
|
import contextlib
|
||||||
from datetime import timedelta
|
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
|
import time
|
||||||
from timeit import default_timer as timer
|
from timeit import default_timer as timer
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Any, Final, TypedDict
|
from typing import Any, Final, TypedDict
|
||||||
|
@ -21,7 +21,7 @@ from .core import CALLBACK_TYPE, DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant,
|
||||||
from .exceptions import DependencyError, HomeAssistantError
|
from .exceptions import DependencyError, HomeAssistantError
|
||||||
from .helpers.issue_registry import IssueSeverity, async_create_issue
|
from .helpers.issue_registry import IssueSeverity, async_create_issue
|
||||||
from .helpers.typing import ConfigType, EventType
|
from .helpers.typing import ConfigType, EventType
|
||||||
from .util import dt as dt_util, ensure_unique_string
|
from .util import ensure_unique_string
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -566,7 +566,7 @@ def async_start_setup(
|
||||||
) -> Generator[None, None, None]:
|
) -> Generator[None, None, None]:
|
||||||
"""Keep track of when setup starts and finishes."""
|
"""Keep track of when setup starts and finishes."""
|
||||||
setup_started = hass.data.setdefault(DATA_SETUP_STARTED, {})
|
setup_started = hass.data.setdefault(DATA_SETUP_STARTED, {})
|
||||||
started = dt_util.utcnow()
|
started = time.monotonic()
|
||||||
unique_components: dict[str, str] = {}
|
unique_components: dict[str, str] = {}
|
||||||
for domain in components:
|
for domain in components:
|
||||||
unique = ensure_unique_string(domain, setup_started)
|
unique = ensure_unique_string(domain, setup_started)
|
||||||
|
@ -575,8 +575,8 @@ def async_start_setup(
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
setup_time: dict[str, timedelta] = hass.data.setdefault(DATA_SETUP_TIME, {})
|
setup_time: dict[str, float] = hass.data.setdefault(DATA_SETUP_TIME, {})
|
||||||
time_taken = dt_util.utcnow() - started
|
time_taken = time.monotonic() - started
|
||||||
for unique, domain in unique_components.items():
|
for unique, domain in unique_components.items():
|
||||||
del setup_started[unique]
|
del setup_started[unique]
|
||||||
integration = domain.partition(".")[0]
|
integration = domain.partition(".")[0]
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
"""Tests for WebSocket API commands."""
|
"""Tests for WebSocket API commands."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
import datetime
|
|
||||||
import logging
|
import logging
|
||||||
from unittest.mock import ANY, AsyncMock, Mock, patch
|
from unittest.mock import ANY, AsyncMock, Mock, patch
|
||||||
|
|
||||||
|
@ -2494,8 +2493,8 @@ async def test_integration_setup_info(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test subscribe/unsubscribe bootstrap_integrations."""
|
"""Test subscribe/unsubscribe bootstrap_integrations."""
|
||||||
hass.data[DATA_SETUP_TIME] = {
|
hass.data[DATA_SETUP_TIME] = {
|
||||||
"august": datetime.timedelta(seconds=12.5),
|
"august": 12.5,
|
||||||
"isy994": datetime.timedelta(seconds=12.8),
|
"isy994": 12.8,
|
||||||
}
|
}
|
||||||
await websocket_client.send_json({"id": 7, "type": "integration/setup_info"})
|
await websocket_client.send_json({"id": 7, "type": "integration/setup_info"})
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
"""Test component/platform setup."""
|
"""Test component/platform setup."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
|
||||||
import threading
|
import threading
|
||||||
from unittest.mock import AsyncMock, Mock, patch
|
from unittest.mock import AsyncMock, Mock, patch
|
||||||
|
|
||||||
|
@ -714,28 +713,22 @@ async def test_integration_only_setup_entry(hass: HomeAssistant) -> None:
|
||||||
async def test_async_start_setup(hass: HomeAssistant) -> None:
|
async def test_async_start_setup(hass: HomeAssistant) -> None:
|
||||||
"""Test setup started context manager keeps track of setup times."""
|
"""Test setup started context manager keeps track of setup times."""
|
||||||
with setup.async_start_setup(hass, ["august"]):
|
with setup.async_start_setup(hass, ["august"]):
|
||||||
assert isinstance(
|
assert isinstance(hass.data[setup.DATA_SETUP_STARTED]["august"], float)
|
||||||
hass.data[setup.DATA_SETUP_STARTED]["august"], datetime.datetime
|
|
||||||
)
|
|
||||||
with setup.async_start_setup(hass, ["august"]):
|
with setup.async_start_setup(hass, ["august"]):
|
||||||
assert isinstance(
|
assert isinstance(hass.data[setup.DATA_SETUP_STARTED]["august_2"], float)
|
||||||
hass.data[setup.DATA_SETUP_STARTED]["august_2"], datetime.datetime
|
|
||||||
)
|
|
||||||
|
|
||||||
assert "august" not in hass.data[setup.DATA_SETUP_STARTED]
|
assert "august" not in hass.data[setup.DATA_SETUP_STARTED]
|
||||||
assert isinstance(hass.data[setup.DATA_SETUP_TIME]["august"], datetime.timedelta)
|
assert isinstance(hass.data[setup.DATA_SETUP_TIME]["august"], float)
|
||||||
assert "august_2" not in hass.data[setup.DATA_SETUP_TIME]
|
assert "august_2" not in hass.data[setup.DATA_SETUP_TIME]
|
||||||
|
|
||||||
|
|
||||||
async def test_async_start_setup_platforms(hass: HomeAssistant) -> None:
|
async def test_async_start_setup_platforms(hass: HomeAssistant) -> None:
|
||||||
"""Test setup started context manager keeps track of setup times for platforms."""
|
"""Test setup started context manager keeps track of setup times for platforms."""
|
||||||
with setup.async_start_setup(hass, ["august.sensor"]):
|
with setup.async_start_setup(hass, ["august.sensor"]):
|
||||||
assert isinstance(
|
assert isinstance(hass.data[setup.DATA_SETUP_STARTED]["august.sensor"], float)
|
||||||
hass.data[setup.DATA_SETUP_STARTED]["august.sensor"], datetime.datetime
|
|
||||||
)
|
|
||||||
|
|
||||||
assert "august" not in hass.data[setup.DATA_SETUP_STARTED]
|
assert "august" not in hass.data[setup.DATA_SETUP_STARTED]
|
||||||
assert isinstance(hass.data[setup.DATA_SETUP_TIME]["august"], datetime.timedelta)
|
assert isinstance(hass.data[setup.DATA_SETUP_TIME]["august"], float)
|
||||||
assert "sensor" not in hass.data[setup.DATA_SETUP_TIME]
|
assert "sensor" not in hass.data[setup.DATA_SETUP_TIME]
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue