From 2a795c039742c31a72323e382a98e89e0d1fbfa9 Mon Sep 17 00:00:00 2001 From: Jason Hunter <hunterjm@gmail.com> Date: Wed, 28 Oct 2020 21:05:16 -0400 Subject: [PATCH] Add guppy3 memory profile to Profiler integration (#42435) * add guppy memory profile to profiler integration * add output path to notification * create new service for memory profile * address review comments --- homeassistant/components/profiler/__init__.py | 43 ++++++++++++++++++- .../components/profiler/manifest.json | 10 ++--- .../components/profiler/services.yaml | 6 +++ requirements_all.txt | 3 ++ requirements_test_all.txt | 3 ++ tests/components/profiler/test_init.py | 38 +++++++++++++++- 6 files changed, 94 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index 518e448d4ec..b4cca97e1fd 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -3,6 +3,7 @@ import asyncio import cProfile import time +from guppy import hpy from pyprof2calltree import convert import voluptuous as vol @@ -14,6 +15,7 @@ from homeassistant.helpers.typing import ConfigType from .const import DOMAIN SERVICE_START = "start" +SERVICE_MEMORY = "memory" CONF_SECONDS = "seconds" @@ -31,6 +33,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async with lock: await _async_generate_profile(hass, call) + async def _async_run_memory_profile(call: ServiceCall): + async with lock: + await _async_generate_memory_profile(hass, call) + async_register_admin_service( hass, DOMAIN, @@ -41,6 +47,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ), ) + async_register_admin_service( + hass, + DOMAIN, + SERVICE_MEMORY, + _async_run_memory_profile, + schema=vol.Schema( + {vol.Optional(CONF_SECONDS, default=60.0): vol.Coerce(float)} + ), + ) + return True @@ -53,7 +69,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): async def _async_generate_profile(hass: HomeAssistant, call: ServiceCall): start_time = int(time.time() * 1000000) hass.components.persistent_notification.async_create( - "The profile started. This notification will be updated when it is complete.", + "The profile has started. This notification will be updated when it is complete.", title="Profile Started", notification_id=f"profiler_{start_time}", ) @@ -74,7 +90,32 @@ async def _async_generate_profile(hass: HomeAssistant, call: ServiceCall): ) +async def _async_generate_memory_profile(hass: HomeAssistant, call: ServiceCall): + start_time = int(time.time() * 1000000) + hass.components.persistent_notification.async_create( + "The memory profile has started. This notification will be updated when it is complete.", + title="Profile Started", + notification_id=f"memory_profiler_{start_time}", + ) + heap_profiler = hpy() + heap_profiler.setref() + await asyncio.sleep(float(call.data[CONF_SECONDS])) + heap = heap_profiler.heap() + + heap_path = hass.config.path(f"heap_profile.{start_time}.hpy") + await hass.async_add_executor_job(_write_memory_profile, heap, heap_path) + hass.components.persistent_notification.async_create( + f"Wrote heapy memory profile to {heap_path}", + title="Profile Complete", + notification_id=f"memory_profiler_{start_time}", + ) + + def _write_profile(profiler, cprofile_path, callgrind_path): profiler.create_stats() profiler.dump_stats(cprofile_path) convert(profiler.getstats(), callgrind_path) + + +def _write_memory_profile(heap, heap_path): + heap.byrcs.dump(heap_path) diff --git a/homeassistant/components/profiler/manifest.json b/homeassistant/components/profiler/manifest.json index e740a083c77..c1be2025fb6 100644 --- a/homeassistant/components/profiler/manifest.json +++ b/homeassistant/components/profiler/manifest.json @@ -2,12 +2,8 @@ "domain": "profiler", "name": "Profiler", "documentation": "https://www.home-assistant.io/integrations/profiler", - "requirements": [ - "pyprof2calltree==1.4.5" - ], - "codeowners": [ - "@bdraco" - ], + "requirements": ["pyprof2calltree==1.4.5", "guppy3==3.1.0"], + "codeowners": ["@bdraco"], "quality_scale": "internal", "config_flow": true -} \ No newline at end of file +} diff --git a/homeassistant/components/profiler/services.yaml b/homeassistant/components/profiler/services.yaml index 7033e988fc5..e1c1db89688 100644 --- a/homeassistant/components/profiler/services.yaml +++ b/homeassistant/components/profiler/services.yaml @@ -4,3 +4,9 @@ start: seconds: description: The number of seconds to run the profiler. example: 60.0 +memory: + description: Start the Memory Profiler + fields: + seconds: + description: The number of seconds to run the memory profiler. + example: 60.0 diff --git a/requirements_all.txt b/requirements_all.txt index a9de43462b5..3b2b699e19c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -716,6 +716,9 @@ growattServer==0.1.1 # homeassistant.components.gstreamer gstreamer-player==1.1.2 +# homeassistant.components.profiler +guppy3==3.1.0 + # homeassistant.components.ffmpeg ha-ffmpeg==2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24275ad8585..2e35de3182e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -360,6 +360,9 @@ greeclimate==0.9.0 # homeassistant.components.griddy griddypower==0.1.0 +# homeassistant.components.profiler +guppy3==3.1.0 + # homeassistant.components.ffmpeg ha-ffmpeg==2.0 diff --git a/tests/components/profiler/test_init.py b/tests/components/profiler/test_init.py index 2373e64a593..d2daa117a43 100644 --- a/tests/components/profiler/test_init.py +++ b/tests/components/profiler/test_init.py @@ -2,7 +2,11 @@ import os from homeassistant import setup -from homeassistant.components.profiler import CONF_SECONDS, SERVICE_START +from homeassistant.components.profiler import ( + CONF_SECONDS, + SERVICE_MEMORY, + SERVICE_START, +) from homeassistant.components.profiler.const import DOMAIN from tests.async_mock import patch @@ -39,3 +43,35 @@ async def test_basic_usage(hass, tmpdir): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_memory_usage(hass, tmpdir): + """Test we can setup and the service is registered.""" + test_dir = tmpdir.mkdir("profiles") + + await setup.async_setup_component(hass, "persistent_notification", {}) + 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_MEMORY) + + last_filename = None + + def _mock_path(filename): + nonlocal last_filename + last_filename = f"{test_dir}/{filename}" + return last_filename + + with patch("homeassistant.components.profiler.hpy") as mock_hpy, patch.object( + hass.config, "path", _mock_path + ): + await hass.services.async_call(DOMAIN, SERVICE_MEMORY, {CONF_SECONDS: 0.000001}) + await hass.async_block_till_done() + + mock_hpy.assert_called_once() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() -- GitLab