1 """The profiler integration."""
4 from collections.abc
import Generator
6 from contextlib
import suppress
7 from datetime
import timedelta
8 from functools
import _lru_cache_wrapper
15 from typing
import Any, cast
18 import voluptuous
as vol
29 from .const
import DOMAIN
31 SERVICE_START =
"start"
32 SERVICE_MEMORY =
"memory"
33 SERVICE_START_LOG_OBJECTS =
"start_log_objects"
34 SERVICE_STOP_LOG_OBJECTS =
"stop_log_objects"
35 SERVICE_START_LOG_OBJECT_SOURCES =
"start_log_object_sources"
36 SERVICE_STOP_LOG_OBJECT_SOURCES =
"stop_log_object_sources"
37 SERVICE_DUMP_LOG_OBJECTS =
"dump_log_objects"
38 SERVICE_LRU_STATS =
"lru_stats"
39 SERVICE_LOG_THREAD_FRAMES =
"log_thread_frames"
40 SERVICE_LOG_EVENT_LOOP_SCHEDULED =
"log_event_loop_scheduled"
41 SERVICE_SET_ASYNCIO_DEBUG =
"set_asyncio_debug"
42 SERVICE_LOG_CURRENT_TASKS =
"log_current_tasks"
44 _LRU_CACHE_WRAPPER_OBJECT = _lru_cache_wrapper.__name__
45 _SQLALCHEMY_LRU_OBJECT =
"LRUCache"
47 _KNOWN_LRU_CLASSES = (
51 "StateAttributesManager",
52 "StatisticsMetaManager",
58 SERVICE_START_LOG_OBJECTS,
59 SERVICE_STOP_LOG_OBJECTS,
60 SERVICE_DUMP_LOG_OBJECTS,
62 SERVICE_LOG_THREAD_FRAMES,
63 SERVICE_LOG_EVENT_LOOP_SCHEDULED,
64 SERVICE_SET_ASYNCIO_DEBUG,
65 SERVICE_LOG_CURRENT_TASKS,
70 DEFAULT_MAX_OBJECTS = 5
72 CONF_ENABLED =
"enabled"
73 CONF_SECONDS =
"seconds"
74 CONF_MAX_OBJECTS =
"max_objects"
76 LOG_INTERVAL_SUB =
"log_interval_subscription"
79 _LOGGER = logging.getLogger(__name__)
83 hass: HomeAssistant, entry: ConfigEntry
85 """Set up Profiler from a config entry."""
87 domain_data = hass.data[DOMAIN] = {}
89 async
def _async_run_profile(call: ServiceCall) ->
None:
93 async
def _async_run_memory_profile(call: ServiceCall) ->
None:
97 async
def _async_start_log_objects(call: ServiceCall) ->
None:
98 if LOG_INTERVAL_SUB
in domain_data:
101 persistent_notification.async_create(
104 "Object growth logging has started. See [the logs](/config/logs) to"
105 " track the growth of new objects."
107 title=
"Object growth logging started",
108 notification_id=
"profile_object_logging",
110 await hass.async_add_executor_job(_log_objects)
112 hass, _log_objects, call.data[CONF_SCAN_INTERVAL]
115 async
def _async_stop_log_objects(call: ServiceCall) ->
None:
116 if LOG_INTERVAL_SUB
not in domain_data:
119 persistent_notification.async_dismiss(hass,
"profile_object_logging")
120 domain_data.pop(LOG_INTERVAL_SUB)()
122 async
def _async_start_object_sources(call: ServiceCall) ->
None:
123 if LOG_INTERVAL_SUB
in domain_data:
126 persistent_notification.async_create(
129 "Object source logging has started. See [the logs](/config/logs) to"
130 " track the growth of new objects."
132 title=
"Object source logging started",
133 notification_id=
"profile_object_source_logging",
136 last_ids: set[int] = set()
137 last_stats: dict[str, int] = {}
139 async
def _log_object_sources_with_max(*_: Any) ->
None:
140 await hass.async_add_executor_job(
141 _log_object_sources, call.data[CONF_MAX_OBJECTS], last_ids, last_stats
144 await _log_object_sources_with_max()
146 hass, _log_object_sources_with_max, call.data[CONF_SCAN_INTERVAL]
155 domain_data[LOG_INTERVAL_SUB] = _cancel
158 def _async_stop_object_sources(call: ServiceCall) ->
None:
159 if LOG_INTERVAL_SUB
not in domain_data:
162 persistent_notification.async_dismiss(hass,
"profile_object_source_logging")
163 domain_data.pop(LOG_INTERVAL_SUB)()
165 def _dump_log_objects(call: ServiceCall) ->
None:
171 obj_type = call.data[CONF_TYPE]
173 for obj
in objgraph.by_type(obj_type):
175 "%s object in memory: %s",
180 persistent_notification.create(
183 f
"Objects with type {obj_type} have been dumped to the log. See [the"
184 " logs](/config/logs) to review the repr of the objects."
186 title=
"Object dump completed",
187 notification_id=
"profile_object_dump",
190 def _lru_stats(call: ServiceCall) ->
None:
191 """Log the stats of all lru caches."""
197 for lru
in objgraph.by_type(_LRU_CACHE_WRAPPER_OBJECT):
198 lru = cast(_lru_cache_wrapper, lru)
200 "Cache stats for lru_cache %s at %s: %s",
206 for _class
in _KNOWN_LRU_CLASSES:
207 for class_with_lru_attr
in objgraph.by_type(_class):
208 for maybe_lru
in class_with_lru_attr.__dict__.values():
209 if isinstance(maybe_lru, LRU):
211 "Cache stats for LRU %s at %s: %s",
212 type(class_with_lru_attr),
214 maybe_lru.get_stats(),
217 for lru
in objgraph.by_type(_SQLALCHEMY_LRU_OBJECT):
218 if (data := getattr(lru,
"_data",
None))
and isinstance(data, dict):
219 for key, value
in dict(data).items():
221 "Cache data for sqlalchemy LRUCache %s: %s: %s", lru, key, value
224 persistent_notification.create(
227 "LRU cache states have been dumped to the log. See [the"
228 " logs](/config/logs) to review the stats."
230 title=
"LRU stats completed",
231 notification_id=
"profile_lru_stats",
234 async
def _async_dump_thread_frames(call: ServiceCall) ->
None:
235 """Log all thread frames."""
236 frames = sys._current_frames()
237 main_thread = threading.main_thread()
238 for thread
in threading.enumerate():
239 if thread == main_thread:
241 ident = cast(int, thread.ident)
245 "".join(traceback.format_stack(frames.get(ident))).strip(),
248 async
def _async_dump_current_tasks(call: ServiceCall) ->
None:
249 """Log all current tasks in the event loop."""
251 for task
in asyncio.all_tasks():
252 if not task.cancelled():
253 _LOGGER.critical(
"Task: %s",
_safe_repr(task))
255 async
def _async_dump_scheduled(call: ServiceCall) ->
None:
256 """Log all scheduled in the event loop."""
258 handle: asyncio.Handle
259 for handle
in getattr(hass.loop,
"_scheduled"):
260 if not handle.cancelled():
261 _LOGGER.critical(
"Scheduled: %s", handle)
263 async
def _async_asyncio_debug(call: ServiceCall) ->
None:
264 """Enable or disable asyncio debug."""
265 enabled = call.data[CONF_ENABLED]
268 _LOGGER.critical(
"Setting asyncio debug to %s", enabled)
271 base_logger = logging.getLogger()
272 if enabled
and base_logger.getEffectiveLevel() > logging.INFO:
273 base_logger.setLevel(logging.INFO)
274 hass.loop.set_debug(enabled)
282 {vol.Optional(CONF_SECONDS, default=60.0): vol.Coerce(float)}
290 _async_run_memory_profile,
292 {vol.Optional(CONF_SECONDS, default=60.0): vol.Coerce(float)}
299 SERVICE_START_LOG_OBJECTS,
300 _async_start_log_objects,
304 CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
313 SERVICE_STOP_LOG_OBJECTS,
314 _async_stop_log_objects,
320 SERVICE_START_LOG_OBJECT_SOURCES,
321 _async_start_object_sources,
325 CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
327 vol.Optional(CONF_MAX_OBJECTS, default=DEFAULT_MAX_OBJECTS): vol.Range(
337 SERVICE_STOP_LOG_OBJECT_SOURCES,
338 _async_stop_object_sources,
344 SERVICE_DUMP_LOG_OBJECTS,
346 schema=vol.Schema({vol.Required(CONF_TYPE): str}),
359 SERVICE_LOG_THREAD_FRAMES,
360 _async_dump_thread_frames,
366 SERVICE_LOG_EVENT_LOOP_SCHEDULED,
367 _async_dump_scheduled,
373 SERVICE_SET_ASYNCIO_DEBUG,
374 _async_asyncio_debug,
375 schema=vol.Schema({vol.Optional(CONF_ENABLED, default=
True): cv.boolean}),
381 SERVICE_LOG_CURRENT_TASKS,
382 _async_dump_current_tasks,
389 """Unload a config entry."""
390 for service
in SERVICES:
391 hass.services.async_remove(domain=DOMAIN, service=service)
392 if LOG_INTERVAL_SUB
in hass.data[DOMAIN]:
393 hass.data[DOMAIN][LOG_INTERVAL_SUB]()
394 hass.data.pop(DOMAIN)
404 start_time =
int(time.time() * 1000000)
405 persistent_notification.async_create(
408 "The profile has started. This notification will be updated when it is"
411 title=
"Profile Started",
412 notification_id=f
"profiler_{start_time}",
414 profiler = cProfile.Profile()
416 await asyncio.sleep(
float(call.data[CONF_SECONDS]))
419 cprofile_path = hass.config.path(f
"profile.{start_time}.cprof")
420 callgrind_path = hass.config.path(f
"callgrind.out.{start_time}")
421 await hass.async_add_executor_job(
422 _write_profile, profiler, cprofile_path, callgrind_path
424 persistent_notification.async_create(
427 f
"Wrote cProfile data to {cprofile_path} and callgrind data to"
430 title=
"Profile Complete",
431 notification_id=f
"profiler_{start_time}",
439 if sys.version_info >= (3, 13):
441 "Memory profiling is not supported on Python 3.13. Please use Python 3.12."
443 from guppy
import hpy
445 start_time =
int(time.time() * 1000000)
446 persistent_notification.async_create(
449 "The memory profile has started. This notification will be updated when it"
452 title=
"Profile Started",
453 notification_id=f
"memory_profiler_{start_time}",
455 heap_profiler = hpy()
456 heap_profiler.setref()
457 await asyncio.sleep(
float(call.data[CONF_SECONDS]))
458 heap = heap_profiler.heap()
460 heap_path = hass.config.path(f
"heap_profile.{start_time}.hpy")
461 await hass.async_add_executor_job(_write_memory_profile, heap, heap_path)
462 persistent_notification.async_create(
464 f
"Wrote heapy memory profile to {heap_path}",
465 title=
"Profile Complete",
466 notification_id=f
"memory_profiler_{start_time}",
474 from pyprof2calltree
import convert
476 profiler.create_stats()
477 profiler.dump_stats(cprofile_path)
478 convert(profiler.getstats(), callgrind_path)
482 heap.byrcs.dump(heap_path)
491 _LOGGER.critical(
"Memory Growth: %s", objgraph.growth(limit=1000))
495 """Get the absolute file path of a function."""
498 abs_file: str |
None =
None
499 with suppress(Exception):
500 abs_file = inspect.getabsfile(func)
505 """Get the repr of an object but keep going if there is an exception.
507 We wrap repr to ensure if one object cannot be serialized, we can
513 return f
"Failed to serialize {type(obj)}"
521 for backref
in objgraph.find_backref_chain(
522 _object,
lambda obj: obj
is not _object
528 max_objects: int, last_ids: set[int], last_stats: dict[str, int]
537 objects = gc.get_objects()
538 new_objects: list[object] = []
539 new_objects_overflow: dict[str, int] = {}
541 new_stats: dict[str, int] = {}
542 had_new_object_growth =
False
544 for _object
in objects:
545 object_type = type(_object).__name__
546 new_stats[object_type] = new_stats.get(object_type, 0) + 1
548 for _object
in objects:
553 object_type = type(_object).__name__
554 if last_stats.get(object_type, 0) < new_stats[object_type]:
555 if len(new_objects) < max_objects:
556 new_objects.append(_object)
558 new_objects_overflow.setdefault(object_type, 0)
559 new_objects_overflow[object_type] += 1
561 for _object
in new_objects:
562 had_new_object_growth =
True
563 object_type = type(_object).__name__
565 "New object %s (%s/%s) at %s: %s",
567 last_stats.get(object_type, 0),
568 new_stats[object_type],
573 for object_type, count
in last_stats.items():
574 new_stats[object_type] =
max(new_stats.get(object_type, 0), count)
580 last_ids.update(current_ids)
582 last_stats.update(new_stats)
586 if new_objects_overflow:
587 _LOGGER.critical(
"New objects overflowed by %s", new_objects_overflow)
588 elif not had_new_object_growth:
589 _LOGGER.critical(
"No new object growth found")
592 @contextlib.contextmanager
594 """Increase the repr limit."""
595 arepr = reprlib.aRepr
596 original_maxstring = arepr.maxstring
597 original_maxother = arepr.maxother
598 arepr.maxstring = 300
603 arepr.maxstring = original_maxstring
604 arepr.maxother = original_maxother
def _async_generate_memory_profile(HomeAssistant hass, ServiceCall call)
def _async_generate_profile(HomeAssistant hass, ServiceCall call)
None _log_object_sources(int max_objects, set[int] last_ids, dict[str, int] last_stats)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
str|None _get_function_absfile(Any func)
Generator[None] _increase_repr_limit()
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
def _write_memory_profile(heap, heap_path)
list[str] _find_backrefs_not_to_self(Any _object)
def _write_profile(profiler, cprofile_path, callgrind_path)
CALLBACK_TYPE async_track_time_interval(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, timedelta interval, *str|None name=None, bool|None cancel_on_shutdown=None)
None async_register_admin_service(HomeAssistant hass, str domain, str service, Callable[[ServiceCall], Awaitable[None]|None] service_func, VolSchemaType schema=vol.Schema({}, extra=vol.PREVENT_EXTRA))