diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index 30385a1c267..ceb3c3a998b 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -1,6 +1,8 @@ """The profiler integration.""" import asyncio +from collections.abc import Generator +import contextlib from contextlib import suppress from datetime import timedelta from functools import _lru_cache_wrapper @@ -37,6 +39,7 @@ SERVICE_LRU_STATS = "lru_stats" SERVICE_LOG_THREAD_FRAMES = "log_thread_frames" SERVICE_LOG_EVENT_LOOP_SCHEDULED = "log_event_loop_scheduled" SERVICE_SET_ASYNCIO_DEBUG = "set_asyncio_debug" +SERVICE_LOG_CURRENT_TASKS = "log_current_tasks" _LRU_CACHE_WRAPPER_OBJECT = _lru_cache_wrapper.__name__ _SQLALCHEMY_LRU_OBJECT = "LRUCache" @@ -59,6 +62,7 @@ SERVICES = ( SERVICE_LOG_THREAD_FRAMES, SERVICE_LOG_EVENT_LOOP_SCHEDULED, SERVICE_SET_ASYNCIO_DEBUG, + SERVICE_LOG_CURRENT_TASKS, ) DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) @@ -241,21 +245,20 @@ async def async_setup_entry( # noqa: C901 "".join(traceback.format_stack(frames.get(ident))).strip(), ) + async def _async_dump_current_tasks(call: ServiceCall) -> None: + """Log all current tasks in the event loop.""" + with _increase_repr_limit(): + for task in asyncio.all_tasks(): + if not task.cancelled(): + _LOGGER.critical("Task: %s", _safe_repr(task)) + async def _async_dump_scheduled(call: ServiceCall) -> None: """Log all scheduled in the event loop.""" - arepr = reprlib.aRepr - original_maxstring = arepr.maxstring - original_maxother = arepr.maxother - arepr.maxstring = 300 - arepr.maxother = 300 - handle: asyncio.Handle - try: + with _increase_repr_limit(): + handle: asyncio.Handle for handle in getattr(hass.loop, "_scheduled"): if not handle.cancelled(): _LOGGER.critical("Scheduled: %s", handle) - finally: - arepr.maxstring = original_maxstring - arepr.maxother = original_maxother async def _async_asyncio_debug(call: ServiceCall) -> None: """Enable or disable asyncio debug.""" @@ -372,6 +375,13 @@ async def async_setup_entry( # noqa: C901 schema=vol.Schema({vol.Optional(CONF_ENABLED, default=True): cv.boolean}), ) + async_register_admin_service( + hass, + DOMAIN, + SERVICE_LOG_CURRENT_TASKS, + _async_dump_current_tasks, + ) + return True @@ -573,3 +583,18 @@ def _log_object_sources( _LOGGER.critical("New objects overflowed by %s", new_objects_overflow) elif not had_new_object_growth: _LOGGER.critical("No new object growth found") + + +@contextlib.contextmanager +def _increase_repr_limit() -> Generator[None, None, None]: + """Increase the repr limit.""" + arepr = reprlib.aRepr + original_maxstring = arepr.maxstring + original_maxother = arepr.maxother + arepr.maxstring = 300 + arepr.maxother = 300 + try: + yield + finally: + arepr.maxstring = original_maxstring + arepr.maxother = original_maxother diff --git a/homeassistant/components/profiler/icons.json b/homeassistant/components/profiler/icons.json index 9a8c0e85f0d..4dda003c186 100644 --- a/homeassistant/components/profiler/icons.json +++ b/homeassistant/components/profiler/icons.json @@ -8,6 +8,7 @@ "start_log_object_sources": "mdi:play", "stop_log_object_sources": "mdi:stop", "lru_stats": "mdi:chart-areaspline", + "log_current_tasks": "mdi:format-list-bulleted", "log_thread_frames": "mdi:format-list-bulleted", "log_event_loop_scheduled": "mdi:calendar-clock", "set_asyncio_debug": "mdi:bug-check" diff --git a/homeassistant/components/profiler/services.yaml b/homeassistant/components/profiler/services.yaml index 6842b2f45f2..82cdcf8d96e 100644 --- a/homeassistant/components/profiler/services.yaml +++ b/homeassistant/components/profiler/services.yaml @@ -59,3 +59,4 @@ set_asyncio_debug: default: true selector: boolean: +log_current_tasks: diff --git a/homeassistant/components/profiler/strings.json b/homeassistant/components/profiler/strings.json index 980550a1a4a..7a31c567040 100644 --- a/homeassistant/components/profiler/strings.json +++ b/homeassistant/components/profiler/strings.json @@ -93,6 +93,10 @@ "description": "Whether to enable or disable asyncio debug." } } + }, + "log_current_tasks": { + "name": "Log current asyncio tasks", + "description": "Logs all the current asyncio tasks." } } } diff --git a/tests/components/profiler/test_init.py b/tests/components/profiler/test_init.py index 3cade465347..ba605049e72 100644 --- a/tests/components/profiler/test_init.py +++ b/tests/components/profiler/test_init.py @@ -18,6 +18,7 @@ from homeassistant.components.profiler import ( CONF_ENABLED, CONF_SECONDS, SERVICE_DUMP_LOG_OBJECTS, + SERVICE_LOG_CURRENT_TASKS, SERVICE_LOG_EVENT_LOOP_SCHEDULED, SERVICE_LOG_THREAD_FRAMES, SERVICE_LRU_STATS, @@ -221,6 +222,28 @@ async def test_log_thread_frames( await hass.async_block_till_done() +async def test_log_current_tasks( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test we can log current tasks.""" + + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.services.has_service(DOMAIN, SERVICE_LOG_CURRENT_TASKS) + + await hass.services.async_call(DOMAIN, SERVICE_LOG_CURRENT_TASKS, {}, blocking=True) + + assert "test_log_current_tasks" in caplog.text + caplog.clear() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + async def test_log_scheduled( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: