Compare commits

..

9 Commits

Author SHA1 Message Date
Jesse Hills
a6e7e48b73 10 years 2025-06-26 16:30:18 +12:00
Jesse Hills
f80610d958 365 days 2025-06-26 16:23:10 +12:00
Jesse Hills
1aacf13888 Use shared workflow for locking 2025-06-26 16:19:48 +12:00
J. Nick Koston
23b1e428de Optimize Application class memory layout and reduce loop_interval size (#9208) 2025-06-26 15:35:01 +12:00
J. Nick Koston
f029f4f20e Fix missing protobuf message dump for batched messages with very verbose logging (#9206) 2025-06-26 13:57:41 +12:00
J. Nick Koston
79e3d2b2d7 Optimize API connection memory with tagged pointers (#9203) 2025-06-26 13:55:12 +12:00
J. Nick Koston
c74e5e0f04 Optimize TemplatableValue memory (#9202) 2025-06-26 13:51:51 +12:00
J. Nick Koston
15ef93ccc9 Optimize API connection loop performance (#9184) 2025-06-26 13:47:41 +12:00
J. Nick Koston
e017250445 Reduce logger CPU usage by disabling loop when buffer is empty (#9160)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-26 13:44:07 +12:00
20 changed files with 171 additions and 581 deletions

View File

@@ -1,28 +1,13 @@
---
name: Lock
name: Lock closed issues and PRs
on:
schedule:
- cron: "30 0 * * *"
- cron: "30 0 * * *" # Run daily at 00:30 UTC
workflow_dispatch:
permissions:
issues: write
pull-requests: write
concurrency:
group: lock
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5.0.1
with:
pr-inactive-days: "1"
pr-lock-reason: ""
exclude-any-pr-labels: keep-open
issue-inactive-days: "7"
issue-lock-reason: ""
exclude-any-issue-labels: keep-open
uses: esphome/workflows/.github/workflows/lock.yml@main
with:
since-days: 3650

View File

@@ -65,6 +65,10 @@ uint32_t APIConnection::get_batch_delay_ms_() const { return this->parent_->get_
void APIConnection::start() {
this->last_traffic_ = App.get_loop_component_start_time();
// Set next_ping_retry_ to prevent immediate ping
// This ensures the first ping happens after the keepalive period
this->next_ping_retry_ = this->last_traffic_ + KEEPALIVE_TIMEOUT_MS;
APIError err = this->helper_->init();
if (err != APIError::OK) {
on_fatal_error();
@@ -159,15 +163,20 @@ void APIConnection::loop() {
on_fatal_error();
ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str());
}
} else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS) {
} else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && now > this->next_ping_retry_) {
ESP_LOGVV(TAG, "Sending keepalive PING");
this->sent_ping_ = this->send_message(PingRequest());
if (!this->sent_ping_) {
// If we can't send the ping request directly (tx_buffer full),
// schedule it at the front of the batch so it will be sent with priority
ESP_LOGVV(TAG, "Failed to send ping directly, scheduling at front of batch");
this->schedule_message_front_(nullptr, &APIConnection::try_send_ping_request, PingRequest::MESSAGE_TYPE);
this->sent_ping_ = true; // Mark as sent to avoid scheduling multiple pings
this->next_ping_retry_ = now + PING_RETRY_INTERVAL;
this->ping_retries_++;
if (this->ping_retries_ >= MAX_PING_RETRIES) {
on_fatal_error();
ESP_LOGE(TAG, "%s: Ping failed %u times", this->get_client_combined_info().c_str(), this->ping_retries_);
} else if (this->ping_retries_ >= 10) {
ESP_LOGW(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_);
} else {
ESP_LOGD(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_);
}
}
}
@@ -267,6 +276,11 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes
// Encode directly into buffer
msg.encode(buffer);
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log the message for VV debugging
conn->log_send_message_(msg.message_name(), msg.dump());
#endif
// Calculate actual encoded size (not including header that was already added)
size_t actual_payload_size = shared_buf.size() - size_before_encode;
@@ -1743,11 +1757,6 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c
items.emplace_back(entity, std::move(creator), message_type);
}
void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type) {
// Insert at front for high priority messages (no deduplication check)
items.insert(items.begin(), BatchItem(entity, std::move(creator), message_type));
}
bool APIConnection::schedule_batch_() {
if (!this->deferred_batch_.batch_scheduled) {
this->deferred_batch_.batch_scheduled = true;
@@ -1929,12 +1938,6 @@ uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConne
return encode_message_to_buffer(req, DisconnectRequest::MESSAGE_TYPE, conn, remaining_size, is_single);
}
uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
PingRequest req;
return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single);
}
uint16_t APIConnection::get_estimated_message_size(uint16_t message_type) {
// Use generated ESTIMATED_SIZE constants from each message type
switch (message_type) {

View File

@@ -185,6 +185,7 @@ class APIConnection : public APIServerConnection {
void on_disconnect_response(const DisconnectResponse &value) override;
void on_ping_response(const PingResponse &value) override {
// we initiated ping
this->ping_retries_ = 0;
this->sent_ping_ = false;
}
void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override;
@@ -440,16 +441,13 @@ class APIConnection : public APIServerConnection {
// Helper function to get estimated message size for buffer pre-allocation
static uint16_t get_estimated_message_size(uint16_t message_type);
// Batch message method for ping requests
static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
// Pointers first (4 bytes each, naturally aligned)
std::unique_ptr<APIFrameHelper> helper_;
APIServer *parent_;
// 4-byte aligned types
uint32_t last_traffic_;
uint32_t next_ping_retry_{0};
int state_subs_at_ = -1;
// Strings (12 bytes each on 32-bit)
@@ -472,7 +470,8 @@ class APIConnection : public APIServerConnection {
bool sent_ping_{false};
bool service_call_subscription_{false};
bool next_close_ = false;
// 7 bytes used, 1 byte padding
uint8_t ping_retries_{0};
// 8 bytes used, no padding needed
// Larger objects at the end
InitialStateIterator initial_state_iterator_;
@@ -599,8 +598,6 @@ class APIConnection : public APIServerConnection {
// Add item to the batch
void add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type);
// Add item to the front of the batch (for high priority messages like ping)
void add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type);
void clear() {
items.clear();
batch_scheduled = false;
@@ -640,12 +637,6 @@ class APIConnection : public APIServerConnection {
bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) {
return schedule_message_(entity, MessageCreator(function_ptr), message_type);
}
// Helper function to schedule a high priority message at the front of the batch
bool schedule_message_front_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) {
this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type);
return this->schedule_batch_();
}
};
} // namespace api

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@ class APIServerConnectionBase : public ProtoService {
template<typename T> bool send_message(const T &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_send_message_(T::message_name(), msg.dump());
this->log_send_message_(msg.message_name(), msg.dump());
#endif
return this->send_message_(msg, T::MESSAGE_TYPE);
}

View File

@@ -526,8 +526,8 @@ void APIServer::on_shutdown() {
for (auto &c : this->clients_) {
if (!c->send_message(DisconnectRequest())) {
// If we can't send the disconnect request directly (tx_buffer full),
// schedule it at the front of the batch so it will be sent with priority
c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE);
// schedule it in the batch so it will be sent with the 5ms timer
c->schedule_message_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE);
}
}
}

View File

@@ -335,6 +335,7 @@ class ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
std::string dump() const;
virtual void dump_to(std::string &out) const = 0;
virtual const char *message_name() const { return "unknown"; }
#endif
protected:

View File

@@ -10,24 +10,11 @@ GPIOBinarySensor = gpio_ns.class_(
"GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component
)
CONF_USE_INTERRUPT = "use_interrupt"
CONF_INTERRUPT_TYPE = "interrupt_type"
INTERRUPT_TYPES = {
"RISING": gpio_ns.INTERRUPT_RISING_EDGE,
"FALLING": gpio_ns.INTERRUPT_FALLING_EDGE,
"ANY": gpio_ns.INTERRUPT_ANY_EDGE,
}
CONFIG_SCHEMA = (
binary_sensor.binary_sensor_schema(GPIOBinarySensor)
.extend(
{
cv.Required(CONF_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_USE_INTERRUPT, default=True): cv.boolean,
cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum(
INTERRUPT_TYPES, upper=True
),
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -40,7 +27,3 @@ async def to_code(config):
pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin))
cg.add(var.set_use_interrupt(config[CONF_USE_INTERRUPT]))
if config[CONF_USE_INTERRUPT]:
cg.add(var.set_interrupt_type(INTERRUPT_TYPES[config[CONF_INTERRUPT_TYPE]]))

View File

@@ -6,91 +6,17 @@ namespace gpio {
static const char *const TAG = "gpio.binary_sensor";
void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) {
bool new_state = arg->isr_pin_.digital_read();
if (new_state != arg->last_state_) {
arg->state_ = new_state;
arg->last_state_ = new_state;
arg->changed_ = true;
// Wake up the component from its disabled loop state
if (arg->component_ != nullptr) {
arg->component_->enable_loop_soon_any_context();
}
}
}
void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component) {
pin->setup();
this->isr_pin_ = pin->to_isr();
this->component_ = component;
// Read initial state
this->last_state_ = pin->digital_read();
this->state_ = this->last_state_;
// Attach interrupt - from this point on, any changes will be caught by the interrupt
pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, type);
}
void GPIOBinarySensor::setup() {
if (this->use_interrupt_ && !this->pin_->is_internal()) {
ESP_LOGD(TAG, "GPIO is not internal, falling back to polling mode");
this->use_interrupt_ = false;
}
if (this->use_interrupt_) {
auto *internal_pin = static_cast<InternalGPIOPin *>(this->pin_);
this->store_.setup(internal_pin, this->interrupt_type_, this);
this->publish_initial_state(this->store_.get_state());
} else {
this->pin_->setup();
this->publish_initial_state(this->pin_->digital_read());
}
this->pin_->setup();
this->publish_initial_state(this->pin_->digital_read());
}
void GPIOBinarySensor::dump_config() {
LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this);
LOG_PIN(" Pin: ", this->pin_);
const char *mode = this->use_interrupt_ ? "interrupt" : "polling";
ESP_LOGCONFIG(TAG, " Mode: %s", mode);
if (this->use_interrupt_) {
const char *interrupt_type;
switch (this->interrupt_type_) {
case gpio::INTERRUPT_RISING_EDGE:
interrupt_type = "RISING_EDGE";
break;
case gpio::INTERRUPT_FALLING_EDGE:
interrupt_type = "FALLING_EDGE";
break;
case gpio::INTERRUPT_ANY_EDGE:
interrupt_type = "ANY_EDGE";
break;
default:
interrupt_type = "UNKNOWN";
break;
}
ESP_LOGCONFIG(TAG, " Interrupt Type: %s", interrupt_type);
}
}
void GPIOBinarySensor::loop() {
if (this->use_interrupt_) {
if (this->store_.is_changed()) {
// Clear the flag immediately to minimize the window where we might miss changes
this->store_.clear_changed();
// Read the state and publish it
// Note: If the ISR fires between clear_changed() and get_state(), that's fine -
// we'll process the new change on the next loop iteration
bool state = this->store_.get_state();
this->publish_state(state);
} else {
// No changes, disable the loop until the next interrupt
this->disable_loop();
}
} else {
this->publish_state(this->pin_->digital_read());
}
}
void GPIOBinarySensor::loop() { this->publish_state(this->pin_->digital_read()); }
float GPIOBinarySensor::get_setup_priority() const { return setup_priority::HARDWARE; }

View File

@@ -2,51 +2,14 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome {
namespace gpio {
// Store class for ISR data (no vtables, ISR-safe)
class GPIOBinarySensorStore {
public:
void setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component);
static void gpio_intr(GPIOBinarySensorStore *arg);
bool get_state() const {
// No lock needed: state_ is atomically updated by ISR
// Volatile ensures we read the latest value
return this->state_;
}
bool is_changed() const {
// Simple read of volatile bool - no clearing here
return this->changed_;
}
void clear_changed() {
// Separate method to clear the flag
this->changed_ = false;
}
protected:
ISRInternalGPIOPin isr_pin_;
volatile bool state_{false};
volatile bool last_state_{false};
volatile bool changed_{false};
Component *component_{nullptr}; // Pointer to the component for enable_loop_soon_any_context()
};
class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component {
public:
// No destructor needed: ESPHome components are created at boot and live forever.
// Interrupts are only detached on reboot when memory is cleared anyway.
void set_pin(GPIOPin *pin) { pin_ = pin; }
void set_use_interrupt(bool use_interrupt) { use_interrupt_ = use_interrupt; }
void set_interrupt_type(gpio::InterruptType type) { interrupt_type_ = type; }
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
/// Setup pin
@@ -59,9 +22,6 @@ class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component {
protected:
GPIOPin *pin_;
bool use_interrupt_{true};
gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE};
GPIOBinarySensorStore store_;
};
} // namespace gpio

View File

@@ -1,26 +0,0 @@
"""
Runtime statistics component for ESPHome.
"""
import esphome.codegen as cg
import esphome.config_validation as cv
DEPENDENCIES = []
CONF_ENABLED = "enabled"
CONF_LOG_INTERVAL = "log_interval"
CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ENABLED, default=True): cv.boolean,
cv.Optional(
CONF_LOG_INTERVAL, default=60000
): cv.positive_time_period_milliseconds,
}
)
async def to_code(config):
"""Generate code for the runtime statistics component."""
cg.add(cg.App.set_runtime_stats_enabled(config[CONF_ENABLED]))
cg.add(cg.App.set_runtime_stats_log_interval(config[CONF_LOG_INTERVAL]))

View File

@@ -136,10 +136,6 @@ void Application::loop() {
this->in_loop_ = false;
this->app_state_ = new_app_state;
// Process any pending runtime stats printing after all components have run
// This ensures stats printing doesn't affect component timing measurements
runtime_stats.process_pending_stats(last_op_end_time);
// Use the last component's end time instead of calling millis() again
auto elapsed = last_op_end_time - this->last_loop_;
if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) {

View File

@@ -9,7 +9,6 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "esphome/core/runtime_stats.h"
#include "esphome/core/scheduler.h"
#ifdef USE_DEVICES
@@ -338,6 +337,9 @@ class Application {
* Each component can request a high frequency loop execution by using the HighFrequencyLoopRequester
* helper in helpers.h
*
* Note: This method is not called by ESPHome core code. It is only used by lambda functions
* in YAML configurations or by external components.
*
* @param loop_interval The interval in milliseconds to run the core loop at. Defaults to 16 milliseconds.
*/
void set_loop_interval(uint32_t loop_interval) {
@@ -346,18 +348,6 @@ class Application {
uint32_t get_loop_interval() const { return static_cast<uint32_t>(this->loop_interval_); }
/** Enable or disable runtime statistics collection.
*
* @param enable Whether to enable runtime statistics collection.
*/
void set_runtime_stats_enabled(bool enable) { runtime_stats.set_enabled(enable); }
/** Set the interval at which runtime statistics are logged.
*
* @param interval The interval in milliseconds between logging of runtime statistics.
*/
void set_runtime_stats_log_interval(uint32_t interval) { runtime_stats.set_log_interval(interval); }
void schedule_dump_config() { this->dump_config_at_ = 0; }
void feed_wdt(uint32_t time = 0);

View File

@@ -303,9 +303,6 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
uint32_t curr_time = millis();
uint32_t blocking_time = curr_time - this->started_;
// Record component runtime stats
runtime_stats.record_component_time(this->component_, blocking_time, curr_time);
bool should_warn;
if (this->component_ != nullptr) {
should_warn = this->component_->should_warn_of_blocking(blocking_time);

View File

@@ -6,7 +6,6 @@
#include <string>
#include "esphome/core/optional.h"
#include "esphome/core/runtime_stats.h"
namespace esphome {

View File

@@ -375,7 +375,7 @@ void ComponentIterator::advance() {
}
if (advance_platform) {
this->state_ = static_cast<IteratorState>(static_cast<uint16_t>(this->state_) + 1);
this->state_ = static_cast<IteratorState>(static_cast<uint32_t>(this->state_) + 1);
this->at_ = 0;
} else if (success) {
this->at_++;

View File

@@ -93,9 +93,7 @@ class ComponentIterator {
virtual bool on_end();
protected:
// Iterates over all ESPHome entities (sensors, switches, lights, etc.)
// Supports up to 256 entity types and up to 65,535 entities of each type
enum class IteratorState : uint8_t {
enum class IteratorState {
NONE = 0,
BEGIN,
#ifdef USE_BINARY_SENSOR
@@ -169,7 +167,7 @@ class ComponentIterator {
#endif
MAX,
} state_{IteratorState::NONE};
uint16_t at_{0}; // Supports up to 65,535 entities per type
size_t at_{0};
bool include_internal_{false};
};

View File

@@ -1,92 +0,0 @@
#include "esphome/core/runtime_stats.h"
#include "esphome/core/component.h"
#include <algorithm>
namespace esphome {
RuntimeStatsCollector runtime_stats;
void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time) {
if (!this->enabled_ || 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 {
// Use cached name - no string operations, just map lookup
this->component_stats_[name_it->second].record_time(duration_ms);
}
// If next_log_time_ is 0, initialize it
if (this->next_log_time_ == 0) {
this->next_log_time_ = current_time + this->log_interval_;
return;
}
// Don't print stats here anymore - let process_pending_stats handle it
}
void RuntimeStatsCollector::log_stats_() {
ESP_LOGI(RUNTIME_TAG, "Component Runtime Statistics");
ESP_LOGI(RUNTIME_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 std::string &source = it.name;
const ComponentRuntimeStats *stats = it.stats;
ESP_LOGI(RUNTIME_TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source.c_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(RUNTIME_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 std::string &source = it.name;
const ComponentRuntimeStats *stats = it.stats;
ESP_LOGI(RUNTIME_TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source.c_str(),
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->enabled_ || 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 esphome

View File

@@ -1,121 +0,0 @@
#pragma once
#include <map>
#include <string>
#include <vector>
#include <cstdint>
#include <algorithm>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
static const char *const RUNTIME_TAG = "runtime";
class Component; // Forward declaration
class ComponentRuntimeStats {
public:
ComponentRuntimeStats()
: period_count_(0),
total_count_(0),
period_time_ms_(0),
total_time_ms_(0),
period_max_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 {
std::string 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() : log_interval_(60000), next_log_time_(0), enabled_(true) {}
void set_log_interval(uint32_t log_interval) { this->log_interval_ = log_interval; }
uint32_t get_log_interval() const { return this->log_interval_; }
void set_enabled(bool enabled) { this->enabled_ = enabled; }
bool is_enabled() const { return this->enabled_; }
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();
}
}
// Back to string keys, but we'll cache the source name per component
std::map<std::string, ComponentRuntimeStats> component_stats_;
std::map<Component *, std::string> component_names_cache_;
uint32_t log_interval_;
uint32_t next_log_time_;
bool enabled_;
};
// Global instance for runtime stats collection
extern RuntimeStatsCollector runtime_stats;
} // namespace esphome

View File

@@ -886,7 +886,7 @@ def build_message_type(
public_content.append("#ifdef HAS_PROTO_MESSAGE_DUMP")
snake_name = camel_to_snake(desc.name)
public_content.append(
f'static constexpr const char *message_name() {{ return "{snake_name}"; }}'
f'const char *message_name() const override {{ return "{snake_name}"; }}'
)
public_content.append("#endif")
@@ -1356,7 +1356,7 @@ def main() -> None:
hpp += " template<typename T>\n"
hpp += " bool send_message(const T &msg) {\n"
hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
hpp += " this->log_send_message_(T::message_name(), msg.dump());\n"
hpp += " this->log_send_message_(msg.message_name(), msg.dump());\n"
hpp += "#endif\n"
hpp += " return this->send_message_(msg, T::MESSAGE_TYPE);\n"
hpp += " }\n\n"