mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-26 12:43:48 +00:00 
			
		
		
		
	Add runtime_stats component for performance debugging and analysis (#9386)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
This commit is contained in:
		
							
								
								
									
										34
									
								
								esphome/components/runtime_stats/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								esphome/components/runtime_stats/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| """ | ||||
| Runtime statistics component for ESPHome. | ||||
| """ | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ID | ||||
|  | ||||
| CODEOWNERS = ["@bdraco"] | ||||
|  | ||||
| CONF_LOG_INTERVAL = "log_interval" | ||||
|  | ||||
| runtime_stats_ns = cg.esphome_ns.namespace("runtime_stats") | ||||
| RuntimeStatsCollector = runtime_stats_ns.class_("RuntimeStatsCollector") | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(RuntimeStatsCollector), | ||||
|         cv.Optional( | ||||
|             CONF_LOG_INTERVAL, default="60s" | ||||
|         ): cv.positive_time_period_milliseconds, | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     """Generate code for the runtime statistics component.""" | ||||
|     # Define USE_RUNTIME_STATS when this component is used | ||||
|     cg.add_define("USE_RUNTIME_STATS") | ||||
|  | ||||
|     # Create the runtime stats instance (constructor sets global_runtime_stats) | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|  | ||||
|     cg.add(var.set_log_interval(config[CONF_LOG_INTERVAL])) | ||||
							
								
								
									
										102
									
								
								esphome/components/runtime_stats/runtime_stats.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								esphome/components/runtime_stats/runtime_stats.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| #include "runtime_stats.h" | ||||
|  | ||||
| #ifdef USE_RUNTIME_STATS | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include <algorithm> | ||||
|  | ||||
| namespace esphome { | ||||
|  | ||||
| namespace runtime_stats { | ||||
|  | ||||
| RuntimeStatsCollector::RuntimeStatsCollector() : log_interval_(60000), next_log_time_(0) { | ||||
|   global_runtime_stats = this; | ||||
| } | ||||
|  | ||||
| void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time) { | ||||
|   if (component == nullptr) | ||||
|     return; | ||||
|  | ||||
|   // Check if we have cached the name for this component | ||||
|   auto name_it = this->component_names_cache_.find(component); | ||||
|   if (name_it == this->component_names_cache_.end()) { | ||||
|     // First time seeing this component, cache its name | ||||
|     const char *source = component->get_component_source(); | ||||
|     this->component_names_cache_[component] = source; | ||||
|     this->component_stats_[source].record_time(duration_ms); | ||||
|   } else { | ||||
|     this->component_stats_[name_it->second].record_time(duration_ms); | ||||
|   } | ||||
|  | ||||
|   if (this->next_log_time_ == 0) { | ||||
|     this->next_log_time_ = current_time + this->log_interval_; | ||||
|     return; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void RuntimeStatsCollector::log_stats_() { | ||||
|   ESP_LOGI(TAG, "Component Runtime Statistics"); | ||||
|   ESP_LOGI(TAG, "Period stats (last %" PRIu32 "ms):", this->log_interval_); | ||||
|  | ||||
|   // First collect stats we want to display | ||||
|   std::vector<ComponentStatPair> stats_to_display; | ||||
|  | ||||
|   for (const auto &it : this->component_stats_) { | ||||
|     const ComponentRuntimeStats &stats = it.second; | ||||
|     if (stats.get_period_count() > 0) { | ||||
|       ComponentStatPair pair = {it.first, &stats}; | ||||
|       stats_to_display.push_back(pair); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Sort by period runtime (descending) | ||||
|   std::sort(stats_to_display.begin(), stats_to_display.end(), std::greater<ComponentStatPair>()); | ||||
|  | ||||
|   // Log top components by period runtime | ||||
|   for (const auto &it : stats_to_display) { | ||||
|     const char *source = it.name; | ||||
|     const ComponentRuntimeStats *stats = it.stats; | ||||
|  | ||||
|     ESP_LOGI(TAG, "  %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source, | ||||
|              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):"); | ||||
|  | ||||
|   // 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(); | ||||
|             }); | ||||
|  | ||||
|   for (const auto &it : stats_to_display) { | ||||
|     const char *source = it.name; | ||||
|     const ComponentRuntimeStats *stats = it.stats; | ||||
|  | ||||
|     ESP_LOGI(TAG, "  %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source, | ||||
|              stats->get_total_count(), stats->get_total_avg_time_ms(), stats->get_total_max_time_ms(), | ||||
|              stats->get_total_time_ms()); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) { | ||||
|   if (this->next_log_time_ == 0) | ||||
|     return; | ||||
|  | ||||
|   if (current_time >= this->next_log_time_) { | ||||
|     this->log_stats_(); | ||||
|     this->reset_stats_(); | ||||
|     this->next_log_time_ = current_time + this->log_interval_; | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace runtime_stats | ||||
|  | ||||
| runtime_stats::RuntimeStatsCollector *global_runtime_stats = | ||||
|     nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|  | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_RUNTIME_STATS | ||||
							
								
								
									
										132
									
								
								esphome/components/runtime_stats/runtime_stats.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								esphome/components/runtime_stats/runtime_stats.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/defines.h" | ||||
|  | ||||
| #ifdef USE_RUNTIME_STATS | ||||
|  | ||||
| #include <map> | ||||
| #include <vector> | ||||
| #include <cstdint> | ||||
| #include <cstring> | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
|  | ||||
| class Component;  // Forward declaration | ||||
|  | ||||
| namespace runtime_stats { | ||||
|  | ||||
| static const char *const TAG = "runtime_stats"; | ||||
|  | ||||
| class ComponentRuntimeStats { | ||||
|  public: | ||||
|   ComponentRuntimeStats() | ||||
|       : period_count_(0), | ||||
|         period_time_ms_(0), | ||||
|         period_max_time_ms_(0), | ||||
|         total_count_(0), | ||||
|         total_time_ms_(0), | ||||
|         total_max_time_ms_(0) {} | ||||
|  | ||||
|   void record_time(uint32_t duration_ms) { | ||||
|     // Update period counters | ||||
|     this->period_count_++; | ||||
|     this->period_time_ms_ += duration_ms; | ||||
|     if (duration_ms > this->period_max_time_ms_) | ||||
|       this->period_max_time_ms_ = duration_ms; | ||||
|  | ||||
|     // Update total counters | ||||
|     this->total_count_++; | ||||
|     this->total_time_ms_ += duration_ms; | ||||
|     if (duration_ms > this->total_max_time_ms_) | ||||
|       this->total_max_time_ms_ = duration_ms; | ||||
|   } | ||||
|  | ||||
|   void reset_period_stats() { | ||||
|     this->period_count_ = 0; | ||||
|     this->period_time_ms_ = 0; | ||||
|     this->period_max_time_ms_ = 0; | ||||
|   } | ||||
|  | ||||
|   // Period stats (reset each logging interval) | ||||
|   uint32_t get_period_count() const { return this->period_count_; } | ||||
|   uint32_t get_period_time_ms() const { return this->period_time_ms_; } | ||||
|   uint32_t get_period_max_time_ms() const { return this->period_max_time_ms_; } | ||||
|   float get_period_avg_time_ms() const { | ||||
|     return this->period_count_ > 0 ? this->period_time_ms_ / static_cast<float>(this->period_count_) : 0.0f; | ||||
|   } | ||||
|  | ||||
|   // Total stats (persistent until reboot) | ||||
|   uint32_t get_total_count() const { return this->total_count_; } | ||||
|   uint32_t get_total_time_ms() const { return this->total_time_ms_; } | ||||
|   uint32_t get_total_max_time_ms() const { return this->total_max_time_ms_; } | ||||
|   float get_total_avg_time_ms() const { | ||||
|     return this->total_count_ > 0 ? this->total_time_ms_ / static_cast<float>(this->total_count_) : 0.0f; | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   // Period stats (reset each logging interval) | ||||
|   uint32_t period_count_; | ||||
|   uint32_t period_time_ms_; | ||||
|   uint32_t period_max_time_ms_; | ||||
|  | ||||
|   // Total stats (persistent until reboot) | ||||
|   uint32_t total_count_; | ||||
|   uint32_t total_time_ms_; | ||||
|   uint32_t total_max_time_ms_; | ||||
| }; | ||||
|  | ||||
| // For sorting components by run time | ||||
| struct ComponentStatPair { | ||||
|   const char *name; | ||||
|   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(); | ||||
|  | ||||
|   void set_log_interval(uint32_t log_interval) { this->log_interval_ = log_interval; } | ||||
|   uint32_t get_log_interval() const { return this->log_interval_; } | ||||
|  | ||||
|   void record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time); | ||||
|  | ||||
|   // Process any pending stats printing (should be called after component loop) | ||||
|   void process_pending_stats(uint32_t current_time); | ||||
|  | ||||
|  protected: | ||||
|   void log_stats_(); | ||||
|  | ||||
|   void reset_stats_() { | ||||
|     for (auto &it : this->component_stats_) { | ||||
|       it.second.reset_period_stats(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Use const char* keys for efficiency | ||||
|   // Custom comparator for const char* keys in map | ||||
|   // Without this, std::map would compare pointer addresses instead of string contents, | ||||
|   // causing identical component names at different addresses to be treated as different keys | ||||
|   struct CStrCompare { | ||||
|     bool operator()(const char *a, const char *b) const { return std::strcmp(a, b) < 0; } | ||||
|   }; | ||||
|   std::map<const char *, ComponentRuntimeStats, CStrCompare> component_stats_; | ||||
|   std::map<Component *, const char *> component_names_cache_; | ||||
|   uint32_t log_interval_; | ||||
|   uint32_t next_log_time_; | ||||
| }; | ||||
|  | ||||
| }  // namespace runtime_stats | ||||
|  | ||||
| extern runtime_stats::RuntimeStatsCollector | ||||
|     *global_runtime_stats;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|  | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_RUNTIME_STATS | ||||
		Reference in New Issue
	
	Block a user