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