Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion homeassistant/components/profiler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import cProfile
import time

from guppy import hpy
from pyprof2calltree import convert
import voluptuous as vol

Expand All @@ -14,6 +15,7 @@
from .const import DOMAIN

SERVICE_START = "start"
SERVICE_MEMORY = "memory"
CONF_SECONDS = "seconds"


Expand All @@ -31,6 +33,10 @@ async def _async_run_profile(call: ServiceCall):
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,
Expand All @@ -41,6 +47,16 @@ async def _async_run_profile(call: ServiceCall):
),
)

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


Expand All @@ -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}",
)
Expand All @@ -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)
10 changes: 3 additions & 7 deletions homeassistant/components/profiler/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
6 changes: 6 additions & 0 deletions homeassistant/components/profiler/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,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

Expand Down
3 changes: 3 additions & 0 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,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

Expand Down
38 changes: 37 additions & 1 deletion tests/components/profiler/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -39,3 +43,35 @@ def _mock_path(filename):

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()