From d3f01f7ea92e3a3866db4bc4308374fe131630f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Jun 2022 17:49:37 -1000 Subject: [PATCH] Reduce memory pressure from history_stats with large data sets (#73289) --- .../components/history_stats/data.py | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 3b17c715c97..33f32e72292 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -23,6 +23,14 @@ class HistoryStatsState: period: tuple[datetime.datetime, datetime.datetime] +@dataclass +class HistoryState: + """A minimal state to avoid holding on to State objects.""" + + state: str + last_changed: float + + class HistoryStats: """Manage history stats.""" @@ -40,7 +48,7 @@ class HistoryStats: self.entity_id = entity_id self._period = (MIN_TIME_UTC, MIN_TIME_UTC) self._state: HistoryStatsState = HistoryStatsState(None, None, self._period) - self._history_current_period: list[State] = [] + self._history_current_period: list[HistoryState] = [] self._previous_run_before_start = False self._entity_states = set(entity_states) self._duration = duration @@ -103,20 +111,18 @@ class HistoryStats: <= floored_timestamp(new_state.last_changed) <= current_period_end_timestamp ): - self._history_current_period.append(new_state) + self._history_current_period.append( + HistoryState( + new_state.state, new_state.last_changed.timestamp() + ) + ) new_data = True if not new_data and current_period_end_timestamp < now_timestamp: # If period has not changed and current time after the period end... # Don't compute anything as the value cannot have changed return self._state else: - self._history_current_period = await get_instance( - self.hass - ).async_add_executor_job( - self._update_from_database, - current_period_start, - current_period_end, - ) + await self._async_history_from_db(current_period_start, current_period_end) self._previous_run_before_start = False hours_matched, match_count = self._async_compute_hours_and_changes( @@ -127,7 +133,24 @@ class HistoryStats: self._state = HistoryStatsState(hours_matched, match_count, self._period) return self._state - def _update_from_database( + async def _async_history_from_db( + self, + current_period_start: datetime.datetime, + current_period_end: datetime.datetime, + ) -> None: + """Update history data for the current period from the database.""" + instance = get_instance(self.hass) + states = await instance.async_add_executor_job( + self._state_changes_during_period, + current_period_start, + current_period_end, + ) + self._history_current_period = [ + HistoryState(state.state, state.last_changed.timestamp()) + for state in states + ] + + def _state_changes_during_period( self, start: datetime.datetime, end: datetime.datetime ) -> list[State]: return history.state_changes_during_period( @@ -155,9 +178,9 @@ class HistoryStats: match_count = 1 if previous_state_matches else 0 # Make calculations - for item in self._history_current_period: - current_state_matches = item.state in self._entity_states - state_change_timestamp = item.last_changed.timestamp() + for history_state in self._history_current_period: + current_state_matches = history_state.state in self._entity_states + state_change_timestamp = history_state.last_changed if previous_state_matches: elapsed += state_change_timestamp - last_state_change_timestamp