From e1355de4cb85ccdcc716e812880dcfcc256a3115 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 28 Jan 2026 06:06:33 -1000 Subject: [PATCH] [runtime_stats] Eliminate heap churn by using stack-allocated buffer for sorting (#13586) --- .../runtime_stats/runtime_stats.cpp | 59 ++++++++++++------- .../components/runtime_stats/runtime_stats.h | 12 ---- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/esphome/components/runtime_stats/runtime_stats.cpp b/esphome/components/runtime_stats/runtime_stats.cpp index 9a1e1a109a..410695da04 100644 --- a/esphome/components/runtime_stats/runtime_stats.cpp +++ b/esphome/components/runtime_stats/runtime_stats.cpp @@ -27,46 +27,61 @@ void RuntimeStatsCollector::record_component_time(Component *component, uint32_t } void RuntimeStatsCollector::log_stats_() { + // First pass: count active components + size_t count = 0; + for (const auto &it : this->component_stats_) { + if (it.second.get_period_count() > 0) { + count++; + } + } + ESP_LOGI(TAG, "Component Runtime Statistics\n" - " Period stats (last %" PRIu32 "ms):", - this->log_interval_); + " Period stats (last %" PRIu32 "ms): %zu active components", + this->log_interval_, count); - // First collect stats we want to display - std::vector stats_to_display; + if (count == 0) { + return; + } + // Stack buffer sized to actual active count (up to 256 components), heap fallback for larger + SmallBufferWithHeapFallback<256, Component *> buffer(count); + Component **sorted = buffer.get(); + + // Second pass: fill buffer with active components + size_t idx = 0; for (const auto &it : this->component_stats_) { - Component *component = it.first; - const ComponentRuntimeStats &stats = it.second; - if (stats.get_period_count() > 0) { - ComponentStatPair pair = {component, &stats}; - stats_to_display.push_back(pair); + if (it.second.get_period_count() > 0) { + sorted[idx++] = it.first; } } // Sort by period runtime (descending) - std::sort(stats_to_display.begin(), stats_to_display.end(), std::greater()); + std::sort(sorted, sorted + count, [this](Component *a, Component *b) { + return this->component_stats_[a].get_period_time_ms() > this->component_stats_[b].get_period_time_ms(); + }); // Log top components by period runtime - for (const auto &it : stats_to_display) { + for (size_t i = 0; i < count; i++) { + const auto &stats = this->component_stats_[sorted[i]]; ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", - LOG_STR_ARG(it.component->get_component_log_str()), it.stats->get_period_count(), - it.stats->get_period_avg_time_ms(), it.stats->get_period_max_time_ms(), it.stats->get_period_time_ms()); + LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_period_count(), stats.get_period_avg_time_ms(), + stats.get_period_max_time_ms(), stats.get_period_time_ms()); } - // Log total stats since boot - ESP_LOGI(TAG, " Total stats (since boot):"); + // Log total stats since boot (only for active components - idle ones haven't changed) + ESP_LOGI(TAG, " Total stats (since boot): %zu active components", count); // Re-sort by total runtime for all-time stats - std::sort(stats_to_display.begin(), stats_to_display.end(), - [](const ComponentStatPair &a, const ComponentStatPair &b) { - return a.stats->get_total_time_ms() > b.stats->get_total_time_ms(); - }); + std::sort(sorted, sorted + count, [this](Component *a, Component *b) { + return this->component_stats_[a].get_total_time_ms() > this->component_stats_[b].get_total_time_ms(); + }); - for (const auto &it : stats_to_display) { + for (size_t i = 0; i < count; i++) { + const auto &stats = this->component_stats_[sorted[i]]; ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", - LOG_STR_ARG(it.component->get_component_log_str()), it.stats->get_total_count(), - it.stats->get_total_avg_time_ms(), it.stats->get_total_max_time_ms(), it.stats->get_total_time_ms()); + LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_total_count(), stats.get_total_avg_time_ms(), + stats.get_total_max_time_ms(), stats.get_total_time_ms()); } } diff --git a/esphome/components/runtime_stats/runtime_stats.h b/esphome/components/runtime_stats/runtime_stats.h index 56122364c2..c7fea7474b 100644 --- a/esphome/components/runtime_stats/runtime_stats.h +++ b/esphome/components/runtime_stats/runtime_stats.h @@ -5,7 +5,6 @@ #ifdef USE_RUNTIME_STATS #include -#include #include #include #include "esphome/core/helpers.h" @@ -77,17 +76,6 @@ class ComponentRuntimeStats { uint32_t total_max_time_ms_; }; -// For sorting components by run time -struct ComponentStatPair { - Component *component; - const ComponentRuntimeStats *stats; - - bool operator>(const ComponentStatPair &other) const { - // Sort by period time as that's what we're displaying in the logs - return stats->get_period_time_ms() > other.stats->get_period_time_ms(); - } -}; - class RuntimeStatsCollector { public: RuntimeStatsCollector();