Compare commits

...

10 Commits

Author SHA1 Message Date
J. Nick Koston
1532b8a545 more time 2025-05-13 02:05:03 -05:00
J. Nick Koston
f1cbd78aa0 log 2025-05-13 01:59:40 -05:00
J. Nick Koston
83db3eddd9 revert ota 2025-05-13 01:07:43 -05:00
J. Nick Koston
cc2c5a544e revert ota 2025-05-13 01:07:38 -05:00
J. Nick Koston
8fba8c2800 revert ota 2025-05-13 01:05:37 -05:00
J. Nick Koston
51d1da8460 revert ota 2025-05-13 01:04:09 -05:00
J. Nick Koston
2f1257056d revert 2025-05-13 01:02:00 -05:00
J. Nick Koston
2f8f6967bf fix ota 2025-05-13 00:55:19 -05:00
J. Nick Koston
246527e618 runtime stats 2025-05-13 00:54:05 -05:00
J. Nick Koston
3857cc9c83 runtime stats 2025-05-13 00:51:14 -05:00
9 changed files with 435 additions and 2 deletions

2
.gitignore vendored
View File

@@ -143,3 +143,5 @@ sdkconfig.*
/components
/managed_components
**/.claude/settings.local.json

View File

@@ -17,6 +17,10 @@
#include <nvs_flash.h>
#include <cinttypes>
#ifdef USE_BLUETOOTH_PROXY
#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h"
#endif
#ifdef USE_OTA
#include "esphome/components/ota/ota_backend.h"
#endif
@@ -75,7 +79,90 @@ void ESP32BLETracker::setup() {
#endif
}
void ESP32BLETracker::log_timing_stats_() {
// Only log if we have data to report
if (this->timing_stats_.loop_processing_count == 0)
return;
ESP_LOGI(TAG, "BLE Tracker Timing Statistics (last %dms):", this->timing_stats_.log_interval_ms);
if (this->timing_stats_.loop_processing_count > 0) {
float avg_loop_time = this->timing_stats_.loop_processing_time / (float) this->timing_stats_.loop_processing_count;
ESP_LOGI(TAG, " BLE Scan Processing: count=%d, avg=%.2fms, total=%dms", this->timing_stats_.loop_processing_count,
avg_loop_time, this->timing_stats_.loop_processing_time);
// Log scan results processed
ESP_LOGI(TAG, " BLE Scan Results Processed: count=%d", this->timing_stats_.scan_result_count);
}
// Log raw advertisement processing statistics
if (this->timing_stats_.raw_adv_count > 0) {
float avg_raw_time = this->timing_stats_.raw_adv_time / (float) this->timing_stats_.raw_adv_count;
ESP_LOGI(TAG, " Raw Advertisement Processing: count=%d, avg=%.2fms, total=%dms", this->timing_stats_.raw_adv_count,
avg_raw_time, this->timing_stats_.raw_adv_time);
}
// Log parsed advertisement processing statistics
if (this->timing_stats_.parse_adv_count > 0) {
float avg_parse_time = this->timing_stats_.parse_adv_time / (float) this->timing_stats_.parse_adv_count;
ESP_LOGI(TAG, " Parsed Advertisement Processing: count=%d, avg=%.2fms, total=%dms",
this->timing_stats_.parse_adv_count, avg_parse_time, this->timing_stats_.parse_adv_time);
}
// Log individual device parsing statistics
if (this->timing_stats_.parse_device_count > 0) {
float avg_device_time = this->timing_stats_.parse_device_time / (float) this->timing_stats_.parse_device_count;
ESP_LOGI(TAG, " Device Parsing: count=%d, avg=%.2fms, total=%dms", this->timing_stats_.parse_device_count,
avg_device_time, this->timing_stats_.parse_device_time);
}
// Log component-specific timings
if (this->timing_stats_.bluetooth_proxy_count > 0) {
float avg_proxy_time = this->timing_stats_.bluetooth_proxy_time / (float) this->timing_stats_.bluetooth_proxy_count;
ESP_LOGI(TAG, " Bluetooth Proxy: count=%d, avg=%.2fms, total=%dms", this->timing_stats_.bluetooth_proxy_count,
avg_proxy_time, this->timing_stats_.bluetooth_proxy_time);
}
if (this->timing_stats_.client_listeners_count > 0) {
float avg_client_time =
this->timing_stats_.client_listeners_time / (float) this->timing_stats_.client_listeners_count;
ESP_LOGI(TAG, " Client Listeners: count=%d, avg=%.2fms, total=%dms", this->timing_stats_.client_listeners_count,
avg_client_time, this->timing_stats_.client_listeners_time);
}
// Log gap scan result processing statistics
if (this->timing_stats_.gap_scan_result_count > 0) {
float avg_gap_time = this->timing_stats_.gap_scan_result_time / (float) this->timing_stats_.gap_scan_result_count;
ESP_LOGI(TAG, " GAP Scan Result Processing: count=%d, avg=%.2fms, total=%dms",
this->timing_stats_.gap_scan_result_count, avg_gap_time, this->timing_stats_.gap_scan_result_time);
}
// Reset timers for next period
this->timing_stats_.loop_processing_time = 0;
this->timing_stats_.loop_processing_count = 0;
this->timing_stats_.bluetooth_proxy_time = 0;
this->timing_stats_.bluetooth_proxy_count = 0;
this->timing_stats_.client_listeners_time = 0;
this->timing_stats_.client_listeners_count = 0;
this->timing_stats_.scan_result_count = 0;
this->timing_stats_.raw_adv_time = 0;
this->timing_stats_.raw_adv_count = 0;
this->timing_stats_.parse_adv_time = 0;
this->timing_stats_.parse_adv_count = 0;
this->timing_stats_.parse_device_time = 0;
this->timing_stats_.parse_device_count = 0;
this->timing_stats_.gap_scan_result_time = 0;
this->timing_stats_.gap_scan_result_count = 0;
}
void ESP32BLETracker::loop() {
// Check if we need to log stats first
uint32_t now = millis();
if (now - this->timing_stats_.last_log_time >= this->timing_stats_.log_interval_ms) {
this->log_timing_stats_();
this->timing_stats_.last_log_time = now;
}
if (!this->parent_->is_active()) {
this->ble_was_disabled_ = true;
return;
@@ -123,25 +210,75 @@ void ESP32BLETracker::loop() {
if (this->scanner_state_ == ScannerState::RUNNING &&
this->scan_result_index_ && // if it looks like we have a scan result we will take the lock
xSemaphoreTake(this->scan_result_lock_, 5L / portTICK_PERIOD_MS)) {
// Start timing the BLE scan processing
uint32_t processing_start = millis();
uint32_t index = this->scan_result_index_;
if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) {
ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up.");
}
if (this->raw_advertisements_) {
this->timing_stats_.scan_result_count += this->scan_result_index_;
// Track overall raw advertisement processing time
uint32_t raw_adv_start = millis();
// Time each listener type separately
for (auto *listener : this->listeners_) {
// Check if this is the bluetooth_proxy component
bool is_bluetooth_proxy = false;
#ifdef USE_BLUETOOTH_PROXY
// Since RTTI is disabled (typeid can't be used), check if the address is the global bluetooth proxy
// This is a safe pointer comparison since we're not calling any methods on it
if (esphome::bluetooth_proxy::global_bluetooth_proxy != nullptr) {
is_bluetooth_proxy =
(listener == static_cast<ESPBTDeviceListener *>(esphome::bluetooth_proxy::global_bluetooth_proxy));
}
#endif
uint32_t listener_start = millis();
listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
uint32_t listener_time = millis() - listener_start;
// Track times for specific components
if (is_bluetooth_proxy) {
this->timing_stats_.bluetooth_proxy_time += listener_time;
this->timing_stats_.bluetooth_proxy_count++;
}
}
// Time the client listeners
uint32_t clients_start = millis();
for (auto *client : this->clients_) {
client->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
}
uint32_t clients_time = millis() - clients_start;
this->timing_stats_.client_listeners_time += clients_time;
this->timing_stats_.client_listeners_count++;
// Calculate total raw advertisement processing time
uint32_t raw_adv_time = millis() - raw_adv_start;
this->timing_stats_.raw_adv_time += raw_adv_time;
this->timing_stats_.raw_adv_count++;
}
if (this->parse_advertisements_) {
// Track overall time for parsed advertisements
uint32_t parse_adv_start = millis();
for (size_t i = 0; i < index; i++) {
// Time individual device parsing
uint32_t device_start = millis();
ESPBTDevice device;
device.parse_scan_rst(this->scan_result_buffer_[i]);
uint32_t device_parse_time = millis() - device_start;
this->timing_stats_.parse_device_time += device_parse_time;
this->timing_stats_.parse_device_count++;
bool found = false;
for (auto *listener : this->listeners_) {
if (listener->parse_device(device))
@@ -161,9 +298,19 @@ void ESP32BLETracker::loop() {
this->print_bt_device_info(device);
}
}
// Record total time for parsed advertisements processing
uint32_t parse_adv_time = millis() - parse_adv_start;
this->timing_stats_.parse_adv_time += parse_adv_time;
this->timing_stats_.parse_adv_count++;
}
this->scan_result_index_ = 0;
xSemaphoreGive(this->scan_result_lock_);
// Record timing for the BLE scan processing
uint32_t processing_time = millis() - processing_start;
this->timing_stats_.loop_processing_time += processing_time;
this->timing_stats_.loop_processing_count++;
}
if (this->scanner_state_ == ScannerState::STOPPED) {
this->end_of_scan_(); // Change state to IDLE
@@ -445,6 +592,8 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_
}
void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param) {
uint32_t start_time = millis();
ESP_LOGV(TAG, "gap_scan_result - event %d", param.search_evt);
if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
if (xSemaphoreTake(this->scan_result_lock_, 0L)) {
@@ -470,6 +619,18 @@ void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_re
}
this->set_scanner_state_(ScannerState::STOPPED);
}
// Record timing statistics
uint32_t duration = millis() - start_time;
this->timing_stats_.gap_scan_result_time += duration;
this->timing_stats_.gap_scan_result_count++;
// Periodically log statistics
uint32_t now = millis();
if (now - this->timing_stats_.last_log_time >= this->timing_stats_.log_interval_ms) {
this->log_timing_stats_();
this->timing_stats_.last_log_time = now;
}
}
void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,

View File

@@ -247,6 +247,9 @@ class ESP32BLETracker : public Component,
}
ScannerState get_scanner_state() const { return this->scanner_state_; }
// Logs timing statistics for BLE operations
void log_timing_stats_();
protected:
void stop_scan_();
/// Start a single scan by setting up the parameters and doing some esp-idf calls.
@@ -299,6 +302,39 @@ class ESP32BLETracker : public Component,
int discovered_{0};
int searching_{0};
int disconnecting_{0};
// Timing statistics
struct {
// Loop processing timing
uint32_t loop_processing_time{0};
uint32_t loop_processing_count{0};
// Detailed component-level timings
uint32_t bluetooth_proxy_time{0};
uint32_t bluetooth_proxy_count{0};
uint32_t client_listeners_time{0};
uint32_t client_listeners_count{0};
uint32_t scan_result_count{0};
// Raw advertisement processing stats
uint32_t raw_adv_time{0};
uint32_t raw_adv_count{0};
// Parsed advertisement processing stats
uint32_t parse_adv_time{0};
uint32_t parse_adv_count{0};
uint32_t parse_device_time{0};
uint32_t parse_device_count{0};
// Gap scan result stats
uint32_t gap_scan_result_time{0};
uint32_t gap_scan_result_count{0};
// Logging control
uint32_t last_log_time{0};
uint32_t log_interval_ms{30000}; // 30 seconds
} timing_stats_;
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
bool coex_prefer_ble_{false};
#endif

View File

@@ -0,0 +1,26 @@
"""
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

@@ -7,6 +7,7 @@
#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_BINARY_SENSOR
@@ -234,6 +235,18 @@ class Application {
uint32_t get_loop_interval() const { return 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();

View File

@@ -243,7 +243,13 @@ void PollingComponent::set_update_interval(uint32_t update_interval) { this->upd
WarnIfComponentBlockingGuard::WarnIfComponentBlockingGuard(Component *component)
: started_(millis()), component_(component) {}
WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {
uint32_t blocking_time = millis() - this->started_;
uint32_t current_time = millis();
uint32_t blocking_time = current_time - this->started_;
// Record component runtime stats
runtime_stats.record_component_time(this->component_, blocking_time, current_time);
// Original blocking check logic
bool should_warn;
if (this->component_ != nullptr) {
should_warn = this->component_->should_warn_of_blocking(blocking_time);
@@ -254,7 +260,6 @@ WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {
const char *src = component_ == nullptr ? "<null>" : component_->get_component_source();
ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms).", src, blocking_time);
ESP_LOGW(TAG, "Components should block for at most 30 ms.");
;
}
}

View File

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

View File

@@ -0,0 +1,28 @@
#include "esphome/core/runtime_stats.h"
#include "esphome/core/component.h"
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;
const char *component_source = component->get_component_source();
this->component_stats_[component_source].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;
}
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

@@ -0,0 +1,161 @@
#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);
protected:
void 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 reset_stats_() {
for (auto &it : this->component_stats_) {
it.second.reset_period_stats();
}
}
std::map<std::string, ComponentRuntimeStats> component_stats_;
uint32_t log_interval_;
uint32_t next_log_time_;
bool enabled_;
};
// Global instance for runtime stats collection
extern RuntimeStatsCollector runtime_stats;
} // namespace esphome