Improve performance of sums in the energy dashboard (#91342)

This commit is contained in:
J. Nick Koston 2023-04-13 11:52:38 -10:00 committed by GitHub
parent 4e80154ebe
commit e1a5ad069c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 106 additions and 19 deletions

View File

@ -857,10 +857,13 @@ def _reduce_statistics(
} }
if _want_mean: if _want_mean:
row["mean"] = mean(mean_values) if mean_values else None row["mean"] = mean(mean_values) if mean_values else None
mean_values.clear()
if _want_min: if _want_min:
row["min"] = min(min_values) if min_values else None row["min"] = min(min_values) if min_values else None
min_values.clear()
if _want_max: if _want_max:
row["max"] = max(max_values) if max_values else None row["max"] = max(max_values) if max_values else None
max_values.clear()
if _want_last_reset: if _want_last_reset:
row["last_reset"] = prev_stat.get("last_reset") row["last_reset"] = prev_stat.get("last_reset")
if _want_state: if _want_state:
@ -868,10 +871,6 @@ def _reduce_statistics(
if _want_sum: if _want_sum:
row["sum"] = prev_stat["sum"] row["sum"] = prev_stat["sum"]
result[statistic_id].append(row) result[statistic_id].append(row)
max_values = []
mean_values = []
min_values = []
if _want_max and (_max := statistic.get("max")) is not None: if _want_max and (_max := statistic.get("max")) is not None:
max_values.append(_max) max_values.append(_max)
if _want_mean and (_mean := statistic.get("mean")) is not None: if _want_mean and (_mean := statistic.get("mean")) is not None:
@ -1560,20 +1559,6 @@ def _statistics_during_period_with_session(
if not stats: if not stats:
return {} return {}
# Return statistics combined with metadata
if period not in ("day", "week", "month"):
return _sorted_statistics_to_dict(
hass,
session,
stats,
statistic_ids,
metadata,
True,
table,
start_time,
units,
types,
)
result = _sorted_statistics_to_dict( result = _sorted_statistics_to_dict(
hass, hass,
@ -1588,6 +1573,10 @@ def _statistics_during_period_with_session(
types, types,
) )
# Return statistics combined with metadata
if period not in ("day", "week", "month"):
return result
if period == "day": if period == "day":
return _reduce_statistics_per_day(result, types) return _reduce_statistics_per_day(result, types)
@ -1829,7 +1818,34 @@ def _statistics_at_time(
return cast(Sequence[Row], execute_stmt_lambda_element(session, stmt)) return cast(Sequence[Row], execute_stmt_lambda_element(session, stmt))
def _sorted_statistics_to_dict( def _fast_build_sum_list(
stats_list: list[Row],
table_duration_seconds: float,
convert: Callable | None,
start_ts_idx: int,
sum_idx: int,
) -> list[StatisticsRow]:
"""Build a list of sum statistics."""
if convert:
return [
{
"start": (start_ts := db_state[start_ts_idx]),
"end": start_ts + table_duration_seconds,
"sum": convert(db_state[sum_idx]),
}
for db_state in stats_list
]
return [
{
"start": (start_ts := db_state[start_ts_idx]),
"end": start_ts + table_duration_seconds,
"sum": db_state[sum_idx],
}
for db_state in stats_list
]
def _sorted_statistics_to_dict( # noqa: C901
hass: HomeAssistant, hass: HomeAssistant,
session: Session, session: Session,
stats: Sequence[Row[Any]], stats: Sequence[Row[Any]],
@ -1888,6 +1904,7 @@ def _sorted_statistics_to_dict(
last_reset_ts_idx = field_map["last_reset_ts"] if "last_reset" in types else None last_reset_ts_idx = field_map["last_reset_ts"] if "last_reset" in types else None
state_idx = field_map["state"] if "state" in types else None state_idx = field_map["state"] if "state" in types else None
sum_idx = field_map["sum"] if "sum" in types else None sum_idx = field_map["sum"] if "sum" in types else None
sum_only = len(types) == 1 and sum_idx is not None
# Append all statistic entries, and optionally do unit conversion # Append all statistic entries, and optionally do unit conversion
table_duration_seconds = table.duration.total_seconds() table_duration_seconds = table.duration.total_seconds()
for meta_id, stats_list in stats_by_meta_id.items(): for meta_id, stats_list in stats_by_meta_id.items():
@ -1900,6 +1917,23 @@ def _sorted_statistics_to_dict(
convert = _get_statistic_to_display_unit_converter(unit, state_unit, units) convert = _get_statistic_to_display_unit_converter(unit, state_unit, units)
else: else:
convert = None convert = None
if sum_only:
# This function is extremely flexible and can handle all types of
# statistics, but in practice we only ever use a few combinations.
#
# For energy, we only need sum statistics, so we can optimize
# this path to avoid the overhead of the more generic function.
assert sum_idx is not None
result[statistic_id] = _fast_build_sum_list(
stats_list,
table_duration_seconds,
convert,
start_ts_idx,
sum_idx,
)
continue
ent_results_append = result[statistic_id].append ent_results_append = result[statistic_id].append
# #
# The below loop is a red hot path for energy, and every # The below loop is a red hot path for energy, and every

View File

@ -1223,6 +1223,59 @@ def test_monthly_statistics(
] ]
} }
stats = statistics_during_period(
hass,
start_time=zero,
statistic_ids=["not", "the", "same", "test:total_energy_import"],
period="month",
types={"sum"},
)
sep_start = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00"))
sep_end = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00"))
oct_start = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00"))
oct_end = dt_util.as_utc(dt_util.parse_datetime("2021-11-01 00:00:00"))
assert stats == {
"test:total_energy_import": [
{
"start": sep_start.timestamp(),
"end": sep_end.timestamp(),
"sum": pytest.approx(3.0),
},
{
"start": oct_start.timestamp(),
"end": oct_end.timestamp(),
"sum": pytest.approx(5.0),
},
]
}
stats = statistics_during_period(
hass,
start_time=zero,
statistic_ids=["not", "the", "same", "test:total_energy_import"],
period="month",
types={"sum"},
units={"energy": "Wh"},
)
sep_start = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00"))
sep_end = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00"))
oct_start = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00"))
oct_end = dt_util.as_utc(dt_util.parse_datetime("2021-11-01 00:00:00"))
assert stats == {
"test:total_energy_import": [
{
"start": sep_start.timestamp(),
"end": sep_end.timestamp(),
"sum": pytest.approx(3000.0),
},
{
"start": oct_start.timestamp(),
"end": oct_end.timestamp(),
"sum": pytest.approx(5000.0),
},
]
}
# Use 5minute to ensure table switch works # Use 5minute to ensure table switch works
stats = statistics_during_period( stats = statistics_during_period(
hass, hass,