This commit is contained in:
J. Nick Koston 2025-05-17 16:50:36 -04:00
parent 5c92367ca2
commit ab415eb3de
No known key found for this signature in database
2 changed files with 238 additions and 3 deletions

View File

@ -3,6 +3,9 @@
#include <cerrno> #include <cerrno>
#include <cinttypes> #include <cinttypes>
#include <utility> #include <utility>
#include <algorithm>
#include <map>
#include <string>
#include "esphome/components/network/util.h" #include "esphome/components/network/util.h"
#include "esphome/core/entity_base.h" #include "esphome/core/entity_base.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
@ -106,6 +109,9 @@ APIConnection::~APIConnection() {
} }
void APIConnection::loop() { void APIConnection::loop() {
// Measure total time for entire loop function
const uint32_t loop_start_time = millis();
if (this->remove_) if (this->remove_)
return; return;
@ -123,15 +129,30 @@ void APIConnection::loop() {
return; return;
} }
const uint32_t now = millis();
uint32_t start_time;
uint32_t duration;
// Section: Helper Loop
start_time = millis();
APIError err = this->helper_->loop(); APIError err = this->helper_->loop();
duration = millis() - start_time;
this->section_stats_["helper_loop"].record_time(duration);
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(), ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(),
api_error_to_str(err), errno); api_error_to_str(err), errno);
return; return;
} }
// Section: Read Packet
start_time = millis();
ReadPacketBuffer buffer; ReadPacketBuffer buffer;
err = this->helper_->read_packet(&buffer); err = this->helper_->read_packet(&buffer);
duration = millis() - start_time;
this->section_stats_["read_packet"].record_time(duration);
if (err == APIError::WOULD_BLOCK) { if (err == APIError::WOULD_BLOCK) {
// pass // pass
} else if (err != APIError::OK) { } else if (err != APIError::OK) {
@ -146,26 +167,41 @@ void APIConnection::loop() {
} }
return; return;
} else { } else {
this->last_traffic_ = millis(); this->last_traffic_ = now;
// read a packet
// Section: Process Message
start_time = millis();
this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
duration = millis() - start_time;
this->section_stats_["process_message"].record_time(duration);
if (this->remove_) if (this->remove_)
return; return;
} }
// Section: Process Queue
start_time = millis();
if (!this->deferred_message_queue_.empty() && this->helper_->can_write_without_blocking()) { if (!this->deferred_message_queue_.empty() && this->helper_->can_write_without_blocking()) {
this->deferred_message_queue_.process_queue(); this->deferred_message_queue_.process_queue();
} }
duration = millis() - start_time;
this->section_stats_["process_queue"].record_time(duration);
// Section: Iterator Advance
start_time = millis();
if (!this->list_entities_iterator_.completed()) if (!this->list_entities_iterator_.completed())
this->list_entities_iterator_.advance(); this->list_entities_iterator_.advance();
if (!this->initial_state_iterator_.completed() && this->list_entities_iterator_.completed()) if (!this->initial_state_iterator_.completed() && this->list_entities_iterator_.completed())
this->initial_state_iterator_.advance(); this->initial_state_iterator_.advance();
duration = millis() - start_time;
this->section_stats_["iterator_advance"].record_time(duration);
// Section: Keepalive
start_time = millis();
static uint32_t keepalive = 60000; static uint32_t keepalive = 60000;
static uint8_t max_ping_retries = 60; static uint8_t max_ping_retries = 60;
static uint16_t ping_retry_interval = 1000; static uint16_t ping_retry_interval = 1000;
const uint32_t now = millis();
if (this->sent_ping_) { if (this->sent_ping_) {
// Disconnect if not responded within 2.5*keepalive // Disconnect if not responded within 2.5*keepalive
if (now - this->last_traffic_ > (keepalive * 5) / 2) { if (now - this->last_traffic_ > (keepalive * 5) / 2) {
@ -191,8 +227,12 @@ void APIConnection::loop() {
} }
} }
} }
duration = millis() - start_time;
this->section_stats_["keepalive"].record_time(duration);
#ifdef USE_ESP32_CAMERA #ifdef USE_ESP32_CAMERA
// Section: Camera
start_time = millis();
if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) { if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) {
// Message will use 8 more bytes than the minimum size, and typical // Message will use 8 more bytes than the minimum size, and typical
// MTU is 1500. Sometimes users will see as low as 1460 MTU. // MTU is 1500. Sometimes users will see as low as 1460 MTU.
@ -231,8 +271,12 @@ void APIConnection::loop() {
this->image_reader_.return_image(); this->image_reader_.return_image();
} }
} }
duration = millis() - start_time;
this->section_stats_["camera"].record_time(duration);
#endif #endif
// Section: State Subscriptions
start_time = millis();
if (state_subs_at_ != -1) { if (state_subs_at_ != -1) {
const auto &subs = this->parent_->get_state_subs(); const auto &subs = this->parent_->get_state_subs();
if (state_subs_at_ >= (int) subs.size()) { if (state_subs_at_ >= (int) subs.size()) {
@ -248,6 +292,24 @@ void APIConnection::loop() {
} }
} }
} }
duration = millis() - start_time;
this->section_stats_["state_subs"].record_time(duration);
// Log stats periodically
if (this->stats_enabled_) {
// If next_stats_log_ is 0, initialize it
if (this->next_stats_log_ == 0) {
this->next_stats_log_ = now + this->stats_log_interval_;
} else if (now >= this->next_stats_log_) {
this->log_section_stats_();
this->reset_section_stats_();
this->next_stats_log_ = now + this->stats_log_interval_;
}
}
// Record total loop execution time
const uint32_t total_loop_duration = millis() - loop_start_time;
this->section_stats_["total_loop"].record_time(total_loop_duration);
} }
std::string get_default_unique_id(const std::string &component_type, EntityBase *entity) { std::string get_default_unique_id(const std::string &component_type, EntityBase *entity) {
@ -1624,8 +1686,14 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
return false; return false;
if (this->helper_->can_write_without_blocking()) if (this->helper_->can_write_without_blocking())
return true; return true;
// Track try_to_clear_buffer time
const uint32_t start_time = millis();
delay(0); delay(0);
APIError err = this->helper_->loop(); APIError err = this->helper_->loop();
const uint32_t duration = millis() - start_time;
this->section_stats_["try_to_clear_buffer"].record_time(duration);
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(), ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(),
@ -1640,11 +1708,18 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
return false; return false;
} }
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) { bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) {
// Track send_buffer time
const uint32_t start_time = millis();
if (!this->try_to_clear_buffer(message_type != 29)) { // SubscribeLogsResponse if (!this->try_to_clear_buffer(message_type != 29)) { // SubscribeLogsResponse
return false; return false;
} }
uint32_t write_start = millis();
APIError err = this->helper_->write_packet(message_type, buffer.get_buffer()->data(), buffer.get_buffer()->size()); APIError err = this->helper_->write_packet(message_type, buffer.get_buffer()->data(), buffer.get_buffer()->size());
uint32_t write_duration = millis() - write_start;
this->section_stats_["write_packet"].record_time(write_duration);
if (err == APIError::WOULD_BLOCK) if (err == APIError::WOULD_BLOCK)
return false; return false;
if (err != APIError::OK) { if (err != APIError::OK) {
@ -1657,6 +1732,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type)
} }
return false; return false;
} }
// Measure total send_buffer function time
uint32_t total_duration = millis() - start_time;
this->section_stats_["send_buffer_total"].record_time(total_duration);
// Do not set last_traffic_ on send // Do not set last_traffic_ on send
return true; return true;
} }
@ -1673,6 +1753,90 @@ void APIConnection::on_fatal_error() {
this->remove_ = true; this->remove_ = true;
} }
void APIConnection::log_section_stats_() {
const char *STATS_TAG = "api.stats";
ESP_LOGI(STATS_TAG, "Logging API section stats now (current time: %" PRIu32 ", scheduled time: %" PRIu32 ")",
millis(), this->next_stats_log_);
ESP_LOGI(STATS_TAG, "Stats collection status: enabled=%d, sections=%zu", this->stats_enabled_,
this->section_stats_.size());
// Check if we have minimal data
bool has_data = false;
for (const auto &it : this->section_stats_) {
if (it.second.get_period_count() > 0) {
has_data = true;
break;
}
}
if (has_data) {
size_t helper_count = 0;
size_t read_count = 0;
size_t total_count = 0;
if (this->section_stats_.count("helper_loop") > 0)
helper_count = this->section_stats_["helper_loop"].get_period_count();
if (this->section_stats_.count("read_packet") > 0)
read_count = this->section_stats_["read_packet"].get_period_count();
if (this->section_stats_.count("total_loop") > 0)
total_count = this->section_stats_["total_loop"].get_period_count();
ESP_LOGI(STATS_TAG, "Record count for key sections: helper_loop=%zu, read_packet=%zu, total_loop=%zu", helper_count,
read_count, total_count);
}
ESP_LOGI(STATS_TAG, "API Connection Section Runtime Statistics");
ESP_LOGI(STATS_TAG, "Period stats (last %" PRIu32 "ms):", this->stats_log_interval_);
// First collect stats we want to display
std::vector<std::pair<std::string, const APISectionStats *>> stats_to_display;
for (const auto &it : this->section_stats_) {
const APISectionStats &stats = it.second;
if (stats.get_period_count() > 0) {
stats_to_display.push_back({it.first, &stats});
}
}
// Sort by period runtime (descending)
std::sort(stats_to_display.begin(), stats_to_display.end(), [](const auto &a, const auto &b) {
return a.second->get_period_time_ms() > b.second->get_period_time_ms();
});
// Log top components by period runtime
for (const auto &it : stats_to_display) {
const std::string &section = it.first;
const APISectionStats *stats = it.second;
ESP_LOGI(STATS_TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", section.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(STATS_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 auto &a, const auto &b) { return a.second->get_total_time_ms() > b.second->get_total_time_ms(); });
for (const auto &it : stats_to_display) {
const std::string &section = it.first;
const APISectionStats *stats = it.second;
ESP_LOGI(STATS_TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", section.c_str(),
stats->get_total_count(), stats->get_total_avg_time_ms(), stats->get_total_max_time_ms(),
stats->get_total_time_ms());
}
ESP_LOGD(STATS_TAG, "Resetting API section stats, sections count: %zu", this->section_stats_.size());
}
void APIConnection::reset_section_stats_() {
for (auto &it : this->section_stats_) {
it.second.reset_period_stats();
}
}
} // namespace api } // namespace api
} // namespace esphome } // namespace esphome
#endif #endif

View File

@ -9,8 +9,12 @@
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/entity_base.h" #include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <vector> #include <vector>
#include <map>
#include <string>
namespace esphome { namespace esphome {
namespace api { namespace api {
@ -61,6 +65,65 @@ class APIConnection : public APIServerConnection {
APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent); APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent);
virtual ~APIConnection(); virtual ~APIConnection();
// Performance statistics class for API loop sections
class APISectionStats {
public:
APISectionStats()
: 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_;
};
void start(); void start();
void loop(); void loop();
@ -546,6 +609,14 @@ class APIConnection : public APIServerConnection {
InitialStateIterator initial_state_iterator_; InitialStateIterator initial_state_iterator_;
ListEntitiesIterator list_entities_iterator_; ListEntitiesIterator list_entities_iterator_;
int state_subs_at_ = -1; int state_subs_at_ = -1;
// API loop section performance statistics
std::map<std::string, APISectionStats> section_stats_;
uint32_t stats_log_interval_{60000}; // 60 seconds default
uint32_t next_stats_log_{0};
bool stats_enabled_{true};
void log_section_stats_();
void reset_section_stats_();
}; };
} // namespace api } // namespace api