From cc8889628010840dcbc9167e07adf6ec342e00b9 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Wed, 1 Apr 2026 17:04:22 +0200 Subject: [PATCH 1/3] [debug] add peripherals status (#12053) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/debug/debug_zephyr.cpp | 47 ++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index bf87b7ae3dc..d1580dae80d 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -91,6 +91,49 @@ void DebugComponent::log_partition_info_() { flash_area_foreach(fa_cb, nullptr); } +#ifdef ESPHOME_LOG_HAS_VERBOSE +// Check if an nRF peripheral's ENABLE register indicates it is enabled. +// periph: peripheral register prefix (e.g. USBD, UARTE, SPI) +// reg: register block pointer (e.g. NRF_USBD, NRF_UARTE0) +#define NRF_PERIPH_ENABLED(periph, reg) \ + YESNO(((reg)->ENABLE & periph##_ENABLE_ENABLE_Msk) == (periph##_ENABLE_ENABLE_Enabled << periph##_ENABLE_ENABLE_Pos)) + +static void log_peripherals_info() { + // most peripherals are enabled only when in use so ESP_LOGV is enough + ESP_LOGV(TAG, "Peripherals status:"); + ESP_LOGV(TAG, " USBD: %-3s| UARTE0: %-3s| UARTE1: %-3s| UART0: %-3s", // + NRF_PERIPH_ENABLED(USBD, NRF_USBD), NRF_PERIPH_ENABLED(UARTE, NRF_UARTE0), + NRF_PERIPH_ENABLED(UARTE, NRF_UARTE1), NRF_PERIPH_ENABLED(UART, NRF_UART0)); + ESP_LOGV(TAG, " TWIS0: %-3s| TWIS1: %-3s| TWIM0: %-3s| TWIM1: %-3s", // + NRF_PERIPH_ENABLED(TWIS, NRF_TWIS0), NRF_PERIPH_ENABLED(TWIS, NRF_TWIS1), + NRF_PERIPH_ENABLED(TWIM, NRF_TWIM0), NRF_PERIPH_ENABLED(TWIM, NRF_TWIM1)); + ESP_LOGV(TAG, " TWI0: %-3s| TWI1: %-3s| COMP: %-3s| CCM: %-3s", // + NRF_PERIPH_ENABLED(TWI, NRF_TWI0), NRF_PERIPH_ENABLED(TWI, NRF_TWI1), NRF_PERIPH_ENABLED(COMP, NRF_COMP), + NRF_PERIPH_ENABLED(CCM, NRF_CCM)); + ESP_LOGV(TAG, " PDM: %-3s| SPIS0: %-3s| SPIS1: %-3s| SPIS2: %-3s", // + NRF_PERIPH_ENABLED(PDM, NRF_PDM), NRF_PERIPH_ENABLED(SPIS, NRF_SPIS0), NRF_PERIPH_ENABLED(SPIS, NRF_SPIS1), + NRF_PERIPH_ENABLED(SPIS, NRF_SPIS2)); + ESP_LOGV(TAG, " SPIM0: %-3s| SPIM1: %-3s| SPIM2: %-3s| SPIM3: %-3s", // + NRF_PERIPH_ENABLED(SPIM, NRF_SPIM0), NRF_PERIPH_ENABLED(SPIM, NRF_SPIM1), + NRF_PERIPH_ENABLED(SPIM, NRF_SPIM2), NRF_PERIPH_ENABLED(SPIM, NRF_SPIM3)); + ESP_LOGV(TAG, " SPI0: %-3s| SPI1: %-3s| SPI2: %-3s| SAADC: %-3s", // + NRF_PERIPH_ENABLED(SPI, NRF_SPI0), NRF_PERIPH_ENABLED(SPI, NRF_SPI1), NRF_PERIPH_ENABLED(SPI, NRF_SPI2), + NRF_PERIPH_ENABLED(SAADC, NRF_SAADC)); + ESP_LOGV(TAG, " QSPI: %-3s| QDEC: %-3s| LPCOMP: %-3s| I2S: %-3s", // + NRF_PERIPH_ENABLED(QSPI, NRF_QSPI), NRF_PERIPH_ENABLED(QDEC, NRF_QDEC), + NRF_PERIPH_ENABLED(LPCOMP, NRF_LPCOMP), NRF_PERIPH_ENABLED(I2S, NRF_I2S)); + ESP_LOGV(TAG, " PWM0: %-3s| PWM1: %-3s| PWM2: %-3s| PWM3: %-3s", // + NRF_PERIPH_ENABLED(PWM, NRF_PWM0), NRF_PERIPH_ENABLED(PWM, NRF_PWM1), NRF_PERIPH_ENABLED(PWM, NRF_PWM2), + NRF_PERIPH_ENABLED(PWM, NRF_PWM3)); + ESP_LOGV(TAG, " AAR: %-3s| QSPI deep power-down:%-3s| CRYPTOCELL: %-3s", NRF_PERIPH_ENABLED(AAR, NRF_AAR), + YESNO((NRF_QSPI->IFCONFIG0 & QSPI_IFCONFIG0_DPMENABLE_Msk) == + (QSPI_IFCONFIG0_DPMENABLE_Enable << QSPI_IFCONFIG0_DPMENABLE_Pos)), + YESNO((NRF_CRYPTOCELL->ENABLE & CRYPTOCELL_ENABLE_ENABLE_Msk) == + (CRYPTOCELL_ENABLE_ENABLE_Enabled << CRYPTOCELL_ENABLE_ENABLE_Pos))); +} +#undef NRF_PERIPH_ENABLED +#endif + static const char *regout0_to_str(uint32_t value) { switch (value) { case (UICR_REGOUT0_VOUT_DEFAULT): @@ -354,7 +397,9 @@ size_t DebugComponent::get_device_info_(std::span }; ESP_LOGD(TAG, " NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str()); ESP_LOGD(TAG, " NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str()); - +#ifdef ESPHOME_LOG_HAS_VERBOSE + log_peripherals_info(); +#endif return pos; } From f33fd047ee5589fa7d21445ed42bb541fe06e92f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Pereira?= Date: Wed, 1 Apr 2026 17:09:22 +0100 Subject: [PATCH 2/3] [hdc2080] Add support for HDC2080 sensor (#9331) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Big Mike Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/hdc2080/__init__.py | 1 + esphome/components/hdc2080/hdc2080.cpp | 71 +++++++++++++++++++ esphome/components/hdc2080/hdc2080.h | 24 +++++++ esphome/components/hdc2080/sensor.py | 57 +++++++++++++++ tests/components/hdc2080/common.yaml | 7 ++ tests/components/hdc2080/test.esp32-idf.yaml | 4 ++ .../components/hdc2080/test.esp8266-ard.yaml | 4 ++ tests/components/hdc2080/test.rp2040-ard.yaml | 4 ++ 9 files changed, 173 insertions(+) create mode 100644 esphome/components/hdc2080/__init__.py create mode 100644 esphome/components/hdc2080/hdc2080.cpp create mode 100644 esphome/components/hdc2080/hdc2080.h create mode 100644 esphome/components/hdc2080/sensor.py create mode 100644 tests/components/hdc2080/common.yaml create mode 100644 tests/components/hdc2080/test.esp32-idf.yaml create mode 100644 tests/components/hdc2080/test.esp8266-ard.yaml create mode 100644 tests/components/hdc2080/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 8d297d7b072..03f41618af1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -217,6 +217,7 @@ esphome/components/hbridge/light/* @DotNetDann esphome/components/hbridge/switch/* @dwmw2 esphome/components/hc8/* @omartijn esphome/components/hdc2010/* @optimusprimespace @ssieb +esphome/components/hdc2080/* @G-Pereira @jesserockz esphome/components/hdc302x/* @joshuasing esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch diff --git a/esphome/components/hdc2080/__init__.py b/esphome/components/hdc2080/__init__.py new file mode 100644 index 00000000000..341ea610489 --- /dev/null +++ b/esphome/components/hdc2080/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@G-Pereira", "@jesserockz"] diff --git a/esphome/components/hdc2080/hdc2080.cpp b/esphome/components/hdc2080/hdc2080.cpp new file mode 100644 index 00000000000..dcb207e099b --- /dev/null +++ b/esphome/components/hdc2080/hdc2080.cpp @@ -0,0 +1,71 @@ +#include "hdc2080.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome::hdc2080 { + +static const char *const TAG = "hdc2080"; + +// Register map (Table 8-6) +static constexpr uint8_t REG_TEMPERATURE_LOW = 0x00; // Temperature [7:0] +static constexpr uint8_t REG_TEMPERATURE_HIGH = 0x01; // Temperature [15:8] +static constexpr uint8_t REG_HUMIDITY_LOW = 0x02; // Humidity [7:0] +static constexpr uint8_t REG_HUMIDITY_HIGH = 0x03; // Humidity [15:8] +static constexpr uint8_t REG_RESET_DRDY_INT_CONF = 0x0E; // Soft Reset and Interrupt Configuration +static constexpr uint8_t REG_MEASUREMENT_CONFIGURATION = 0x0F; + +// Measurement register (0x0F) bit fields +static constexpr uint8_t MEAS_TRIG = 0x01; // Bit 0: start measurement +static constexpr uint8_t MEAS_CONF_TEMP = 0x02; // Bits 2:1 = 01: temperature only +static constexpr uint8_t MEAS_CONF_HUM = 0x04; // Bits 2:1 = 10: humidity only + +void HDC2080Component::setup() { + const uint8_t data = 0x00; // automatic measurement mode disabled, heater off + if (this->write_register(REG_RESET_DRDY_INT_CONF, &data, 1) != i2c::ERROR_OK) { + this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + return; + } +} + +void HDC2080Component::dump_config() { + ESP_LOGCONFIG(TAG, "HDC2080:"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} + +void HDC2080Component::update() { + uint8_t data = MEAS_TRIG; // 14-bit resolution, measure both, start + if (this->temperature_sensor_ != nullptr && this->humidity_sensor_ == nullptr) { + data = MEAS_TRIG | MEAS_CONF_TEMP; + } else if (this->temperature_sensor_ == nullptr && this->humidity_sensor_ != nullptr) { + data = MEAS_TRIG | MEAS_CONF_HUM; + } + if (this->write_register(REG_MEASUREMENT_CONFIGURATION, &data, 1) != i2c::ERROR_OK) { + this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + return; + } + // wait for conversion to complete 2ms should be enough, more is fine + this->set_timeout(5, [this]() { + uint8_t raw_data[4]; + if (this->read_register(REG_TEMPERATURE_LOW, raw_data, 4) != i2c::ERROR_OK) { + this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + return; + } + this->status_clear_warning(); + if (this->temperature_sensor_ != nullptr) { + float temp = encode_uint16(raw_data[1], raw_data[0]) * (165.0f / 65536.0f) - 40.5f; + this->temperature_sensor_->publish_state(temp); + } + if (this->humidity_sensor_ != nullptr) { + float humidity = encode_uint16(raw_data[3], raw_data[2]) * (100.0f / 65536.0f); + this->humidity_sensor_->publish_state(humidity); + } + }); +} + +} // namespace esphome::hdc2080 diff --git a/esphome/components/hdc2080/hdc2080.h b/esphome/components/hdc2080/hdc2080.h new file mode 100644 index 00000000000..daa10d371d6 --- /dev/null +++ b/esphome/components/hdc2080/hdc2080.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +namespace esphome::hdc2080 { + +class HDC2080Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_temperature(sensor::Sensor *temperature) { this->temperature_sensor_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { this->humidity_sensor_ = humidity; } + + /// Setup the sensor and check for connection. + void setup() override; + void dump_config() override; + void update() override; + + protected: + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; +}; + +} // namespace esphome::hdc2080 diff --git a/esphome/components/hdc2080/sensor.py b/esphome/components/hdc2080/sensor.py new file mode 100644 index 00000000000..777fc51cbaf --- /dev/null +++ b/esphome/components/hdc2080/sensor.py @@ -0,0 +1,57 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +DEPENDENCIES = ["i2c"] + +hdc2080_ns = cg.esphome_ns.namespace("hdc2080") +HDC2080Component = hdc2080_ns.class_( + "HDC2080Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HDC2080Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x40)) + .add_extra(cv.has_at_least_one_key(CONF_TEMPERATURE, CONF_HUMIDITY)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature(sens)) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity(sens)) diff --git a/tests/components/hdc2080/common.yaml b/tests/components/hdc2080/common.yaml new file mode 100644 index 00000000000..cb14cb183b5 --- /dev/null +++ b/tests/components/hdc2080/common.yaml @@ -0,0 +1,7 @@ +sensor: + - platform: hdc2080 + temperature: + name: Temperature + humidity: + name: Humidity + update_interval: 15s diff --git a/tests/components/hdc2080/test.esp32-idf.yaml b/tests/components/hdc2080/test.esp32-idf.yaml new file mode 100644 index 00000000000..b47e39c3898 --- /dev/null +++ b/tests/components/hdc2080/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc2080/test.esp8266-ard.yaml b/tests/components/hdc2080/test.esp8266-ard.yaml new file mode 100644 index 00000000000..4a98b9388ab --- /dev/null +++ b/tests/components/hdc2080/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc2080/test.rp2040-ard.yaml b/tests/components/hdc2080/test.rp2040-ard.yaml new file mode 100644 index 00000000000..319a7c71a65 --- /dev/null +++ b/tests/components/hdc2080/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml From ea609d3552fe0685a68a97362abc5658f203837b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Apr 2026 07:09:04 -1000 Subject: [PATCH 3/3] [runtime_stats] Store stats inline on Component to eliminate std::map lookup (#15345) --- .../runtime_stats/runtime_stats.cpp | 76 +++++++++---------- .../components/runtime_stats/runtime_stats.h | 75 +----------------- esphome/core/application.h | 9 +++ esphome/core/component.cpp | 12 +-- esphome/core/component.h | 42 ++++++++++ esphome/core/defines.h | 1 + 6 files changed, 92 insertions(+), 123 deletions(-) diff --git a/esphome/components/runtime_stats/runtime_stats.cpp b/esphome/components/runtime_stats/runtime_stats.cpp index cb28acc96c6..06714b5a446 100644 --- a/esphome/components/runtime_stats/runtime_stats.cpp +++ b/esphome/components/runtime_stats/runtime_stats.cpp @@ -2,6 +2,7 @@ #ifdef USE_RUNTIME_STATS +#include "esphome/core/application.h" #include "esphome/core/component.h" #include @@ -13,20 +14,16 @@ RuntimeStatsCollector::RuntimeStatsCollector() : log_interval_(60000), next_log_ global_runtime_stats = this; } -void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_us) { - if (component == nullptr) - return; - - // Record stats using component pointer as key - this->component_stats_[component].record_time(duration_us); -} - void RuntimeStatsCollector::log_stats_() { - // First pass: count active components + auto &components = App.components_; + + // Single pass: collect active components into stack buffer + SmallBufferWithHeapFallback<256, Component *> buffer(components.size()); + Component **sorted = buffer.get(); size_t count = 0; - for (const auto &it : this->component_stats_) { - if (it.second.get_period_count() > 0) { - count++; + for (auto *component : components) { + if (component->runtime_stats_.period_count > 0) { + sorted[count++] = component; } } @@ -39,61 +36,58 @@ void RuntimeStatsCollector::log_stats_() { 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_) { - if (it.second.get_period_count() > 0) { - sorted[idx++] = it.first; - } - } - // Sort by period runtime (descending) - std::sort(sorted, sorted + count, [this](Component *a, Component *b) { - return this->component_stats_[a].get_period_time_us() > this->component_stats_[b].get_period_time_us(); - }); + std::sort(sorted, sorted + count, compare_period_time); // Log top components by period runtime for (size_t i = 0; i < count; i++) { - const auto &stats = this->component_stats_[sorted[i]]; + const auto &stats = sorted[i]->runtime_stats_; ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms", - LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_period_count(), - stats.get_period_avg_time_us() / 1000.0f, stats.get_period_max_time_us() / 1000.0f, - stats.get_period_time_us() / 1000.0f); + LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.period_count, + stats.period_count > 0 ? stats.period_time_us / (float) stats.period_count / 1000.0f : 0.0f, + stats.period_max_time_us / 1000.0f, stats.period_time_us / 1000.0f); } // 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(sorted, sorted + count, [this](Component *a, Component *b) { - return this->component_stats_[a].get_total_time_us() > this->component_stats_[b].get_total_time_us(); - }); + std::sort(sorted, sorted + count, compare_total_time); for (size_t i = 0; i < count; i++) { - const auto &stats = this->component_stats_[sorted[i]]; + const auto &stats = sorted[i]->runtime_stats_; ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms", - LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_total_count(), - stats.get_total_avg_time_us() / 1000.0f, stats.get_total_max_time_us() / 1000.0f, - stats.get_total_time_us() / 1000.0); + LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count, + stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f, + stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0); } + + // Reset period stats + for (auto *component : components) { + component->runtime_stats_.reset_period(); + } +} + +bool RuntimeStatsCollector::compare_period_time(Component *a, Component *b) { + return a->runtime_stats_.period_time_us > b->runtime_stats_.period_time_us; +} + +bool RuntimeStatsCollector::compare_total_time(Component *a, Component *b) { + return a->runtime_stats_.total_time_us > b->runtime_stats_.total_time_us; } void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) { if ((int32_t) (current_time - this->next_log_time_) >= 0) { 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) +runtime_stats::RuntimeStatsCollector + *global_runtime_stats = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + nullptr; } // namespace esphome diff --git a/esphome/components/runtime_stats/runtime_stats.h b/esphome/components/runtime_stats/runtime_stats.h index 303d895985f..3c2c9f78add 100644 --- a/esphome/components/runtime_stats/runtime_stats.h +++ b/esphome/components/runtime_stats/runtime_stats.h @@ -4,11 +4,8 @@ #ifdef USE_RUNTIME_STATS -#include #include -#include #include "esphome/core/hal.h" -#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -19,64 +16,6 @@ namespace runtime_stats { static const char *const TAG = "runtime_stats"; -class ComponentRuntimeStats { - public: - ComponentRuntimeStats() - : period_count_(0), - period_time_us_(0), - period_max_time_us_(0), - total_count_(0), - total_time_us_(0), - total_max_time_us_(0) {} - - void record_time(uint32_t duration_us) { - // Update period counters - this->period_count_++; - this->period_time_us_ += duration_us; - if (duration_us > this->period_max_time_us_) - this->period_max_time_us_ = duration_us; - - // Update total counters (uint64_t to avoid overflow — uint32_t would overflow after ~10 hours) - this->total_count_++; - this->total_time_us_ += duration_us; - if (duration_us > this->total_max_time_us_) - this->total_max_time_us_ = duration_us; - } - - void reset_period_stats() { - this->period_count_ = 0; - this->period_time_us_ = 0; - this->period_max_time_us_ = 0; - } - - // Period stats (reset each logging interval) - uint32_t get_period_count() const { return this->period_count_; } - uint32_t get_period_time_us() const { return this->period_time_us_; } - uint32_t get_period_max_time_us() const { return this->period_max_time_us_; } - float get_period_avg_time_us() const { - return this->period_count_ > 0 ? this->period_time_us_ / static_cast(this->period_count_) : 0.0f; - } - - // Total stats (persistent until reboot, uint64_t to avoid overflow) - uint32_t get_total_count() const { return this->total_count_; } - uint64_t get_total_time_us() const { return this->total_time_us_; } - uint32_t get_total_max_time_us() const { return this->total_max_time_us_; } - float get_total_avg_time_us() const { - return this->total_count_ > 0 ? this->total_time_us_ / static_cast(this->total_count_) : 0.0f; - } - - protected: - // Period stats (reset each logging interval) - uint32_t period_count_; - uint32_t period_time_us_; - uint32_t period_max_time_us_; - - // Total stats (persistent until reboot) - uint32_t total_count_; - uint64_t total_time_us_; - uint32_t total_max_time_us_; -}; - class RuntimeStatsCollector { public: RuntimeStatsCollector(); @@ -87,23 +26,15 @@ class RuntimeStatsCollector { } uint32_t get_log_interval() const { return this->log_interval_; } - void record_component_time(Component *component, uint32_t duration_us); - // Process any pending stats printing (should be called after component loop) void process_pending_stats(uint32_t current_time); protected: void log_stats_(); + // Static comparators — member functions have friend access, lambdas do not + static bool compare_period_time(Component *a, Component *b); + static bool compare_total_time(Component *a, Component *b); - void reset_stats_() { - for (auto &it : this->component_stats_) { - it.second.reset_period_stats(); - } - } - - // Map from component to its stats - // We use Component* as the key since each component is unique - std::map component_stats_; uint32_t log_interval_; uint32_t next_log_time_{0}; }; diff --git a/esphome/core/application.h b/esphome/core/application.h index 06ff30e81f6..6cc61bc9547 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -130,6 +130,12 @@ bool socket_ready_fd(int fd, bool loop_monitored); // NOLINT(readability-redund #endif } // namespace esphome::socket +#ifdef USE_RUNTIME_STATS +namespace esphome::runtime_stats { +class RuntimeStatsCollector; +} // namespace esphome::runtime_stats +#endif + // Forward declarations for friend access from codegen-generated setup() void setup(); // NOLINT(readability-redundant-declaration) - may be declared in Arduino.h void original_setup(); // NOLINT(readability-redundant-declaration) - used by cpp unit tests @@ -590,6 +596,9 @@ class Application { friend Component; #if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) friend bool socket::socket_ready_fd(int fd, bool loop_monitored); +#endif +#ifdef USE_RUNTIME_STATS + friend class runtime_stats::RuntimeStatsCollector; #endif friend void ::setup(); friend void ::original_setup(); diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 00a36fce3d8..955596ce956 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -9,9 +9,6 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#ifdef USE_RUNTIME_STATS -#include "esphome/components/runtime_stats/runtime_stats.h" -#endif namespace esphome { @@ -524,13 +521,8 @@ WarnIfComponentBlockingGuard::warn_blocking(Component *component, uint32_t block #ifdef USE_RUNTIME_STATS void WarnIfComponentBlockingGuard::record_runtime_stats_() { - // Use micros() for accurate sub-millisecond timing. millis() has insufficient - // resolution — most components complete in microseconds but millis() only has - // 1ms granularity, so results were essentially random noise. - if (global_runtime_stats != nullptr) { - uint32_t duration_us = micros() - this->started_us_; - global_runtime_stats->record_component_time(this->component_, duration_us); - } + uint32_t duration_us = micros() - this->started_us_; + this->component_->runtime_stats_.record_time(duration_us); } #endif diff --git a/esphome/core/component.h b/esphome/core/component.h index c390a205f0f..c5a331ee298 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -20,6 +20,12 @@ namespace esphome { // Forward declaration for LogString struct LogString; +#ifdef USE_RUNTIME_STATS +namespace runtime_stats { +class RuntimeStatsCollector; +} // namespace runtime_stats +#endif + /** Default setup priorities for components of different types. * * Components should return one of these setup priorities in get_setup_priority. @@ -92,6 +98,37 @@ inline constexpr uint8_t WARN_IF_BLOCKING_OVER_CS = 5U; // 50ms in centiseconds /// Weak default returns "" so builds without codegen still link. const LogString *component_source_lookup(uint8_t index); +#ifdef USE_RUNTIME_STATS +/// Inline runtime statistics — eliminates std::map lookup on every loop iteration. +/// Only present when USE_RUNTIME_STATS is defined (profiling builds). +struct ComponentRuntimeStats { + // Period stats (reset each logging interval) + uint32_t period_count{0}; + uint32_t period_time_us{0}; + uint32_t period_max_time_us{0}; + // Total stats (persistent until reboot, uint64_t to avoid overflow) + uint32_t total_count{0}; + uint64_t total_time_us{0}; + uint32_t total_max_time_us{0}; + + void record_time(uint32_t duration_us) { + this->period_count++; + this->period_time_us += duration_us; + if (duration_us > this->period_max_time_us) + this->period_max_time_us = duration_us; + this->total_count++; + this->total_time_us += duration_us; + if (duration_us > this->total_max_time_us) + this->total_max_time_us = duration_us; + } + void reset_period() { + this->period_count = 0; + this->period_time_us = 0; + this->period_max_time_us = 0; + } +}; +#endif + class Component { public: /** Where the component's initialization should happen. @@ -529,6 +566,11 @@ class Component { /// Bits 6-7: Unused - reserved for future expansion uint8_t component_state_{0x00}; volatile bool pending_enable_loop_{false}; ///< ISR-safe flag for enable_loop_soon_any_context +#ifdef USE_RUNTIME_STATS + friend class runtime_stats::RuntimeStatsCollector; + friend class WarnIfComponentBlockingGuard; + ComponentRuntimeStats runtime_stats_; +#endif }; /** This class simplifies creating components that periodically check a state. diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 7259167a527..23e65f55bc0 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -180,6 +180,7 @@ #define USE_RUNTIME_IMAGE_BMP #define USE_RUNTIME_IMAGE_PNG #define USE_RUNTIME_IMAGE_JPEG +#define USE_RUNTIME_STATS #define USE_OTA #define USE_OTA_PASSWORD #define USE_OTA_STATE_LISTENER