mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 18:06:34 +00:00
[core] Instrument fast_select pre-sleep socket scan to prove it is unused
Adds three debug atomic counters around the pre-sleep socket scan in
yield_with_select_():
- fast_select_scan_total_ every scan
- fast_select_scan_found_data_ scan saw a socket with pending data
- fast_select_scan_load_bearing_ scan saw pending data AND the task
notification counter was 0 at scan start
Only the third counter represents a case the scan actually rescues: had
the scan not been present, ulTaskNotifyTake would have stalled up to
loop_interval ms. The other two cases are harmless (Take would have
returned immediately).
The notification value is peeked with ulTaskNotifyValueClear(nullptr, 0)
(a pure read — zero bits cleared, state untouched) BEFORE the scan loop.
Peeking before the scan makes the measurement TOCTOU-free: the value we
compare against is the value at the moment Take would have been called,
exactly the counterfactual we want to measure. Peeking after has_data
would race with the lwip callback firing during the scan.
Stats are logged via ESP_LOGD every 30s from Application::loop().
Background: PR #14475 removed the scan and was reverted because the API
connection's MAX_MESSAGES_PER_LOOP=5 throttle violated the ready()
contract (see #15589, #15590). With #15590 the contract is now
documented and honored, so the scan may now be removable. This PR
gathers evidence; if load_bearing stays 0 across ESP32/LibreTiny under
real workloads, the scan and these counters will be removed in a
follow-up.
This commit is contained in:
@@ -450,6 +450,18 @@ void Application::enable_pending_loops_() {
|
||||
}
|
||||
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
std::atomic<uint32_t> Application::fast_select_scan_total_{0};
|
||||
std::atomic<uint32_t> Application::fast_select_scan_found_data_{0};
|
||||
std::atomic<uint32_t> Application::fast_select_scan_load_bearing_{0};
|
||||
|
||||
void Application::log_fast_select_scan_stats_() {
|
||||
uint32_t total = fast_select_scan_total_.load(std::memory_order_relaxed);
|
||||
uint32_t found = fast_select_scan_found_data_.load(std::memory_order_relaxed);
|
||||
uint32_t load_bearing = fast_select_scan_load_bearing_.load(std::memory_order_relaxed);
|
||||
ESP_LOGD(TAG, "fast_select scan: total=%" PRIu32 " found_data=%" PRIu32 " load_bearing=%" PRIu32, total, found,
|
||||
load_bearing);
|
||||
}
|
||||
|
||||
bool Application::register_socket(struct lwip_sock *sock) {
|
||||
// It modifies monitored_sockets_ without locking — must only be called from the main loop.
|
||||
if (sock == nullptr)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <ctime>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
@@ -655,6 +656,14 @@ class Application {
|
||||
FixedVector<Component *> looping_components_{};
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
std::vector<struct lwip_sock *> monitored_sockets_; // Cached lwip_sock pointers for direct rcvevent read
|
||||
// Stats to verify whether the pre-sleep socket scan in yield_with_select_() is ever load-bearing.
|
||||
// If fast_select_scan_load_bearing_ stays 0 under real workloads, the scan can be removed.
|
||||
// These are static because yield_with_select_() is inlined at every call site.
|
||||
static std::atomic<uint32_t> fast_select_scan_total_;
|
||||
static std::atomic<uint32_t> fast_select_scan_found_data_;
|
||||
static std::atomic<uint32_t> fast_select_scan_load_bearing_;
|
||||
uint32_t fast_select_scan_stats_last_log_{0};
|
||||
void log_fast_select_scan_stats_();
|
||||
#elif defined(USE_HOST)
|
||||
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors
|
||||
#endif
|
||||
@@ -889,6 +898,14 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() {
|
||||
this->yield_with_select_(delay_time);
|
||||
this->last_loop_ = last_op_end_time;
|
||||
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
// Periodic fast-select scan stats (debug). Remove once the scan is proven unneeded.
|
||||
if (last_op_end_time - this->fast_select_scan_stats_last_log_ >= 30000) {
|
||||
this->fast_select_scan_stats_last_log_ = last_op_end_time;
|
||||
this->log_fast_select_scan_stats_();
|
||||
}
|
||||
#endif
|
||||
|
||||
if (this->dump_config_at_ < this->components_.size()) {
|
||||
this->process_dump_config_();
|
||||
}
|
||||
@@ -909,8 +926,22 @@ inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay
|
||||
// If a socket still has unread data (rcvevent > 0) but the task notification was already
|
||||
// consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency.
|
||||
// This scan preserves select() semantics: return immediately when any fd is ready.
|
||||
//
|
||||
// Debug stats: peek the task notification value BEFORE scanning. This answers the
|
||||
// counterfactual "if the scan did not exist and we called ulTaskNotifyTake right now,
|
||||
// would it stall?". ulTaskNotifyValueClear(nullptr, 0) is a pure read — it returns the
|
||||
// current value and clears zero bits, leaving the notification state untouched. Reading
|
||||
// before the loop (rather than after finding data) makes the answer TOCTOU-free: the
|
||||
// value we compare against is the value at the moment Take would have been called.
|
||||
uint32_t fast_select_notify_value_before_scan = ulTaskNotifyValueClear(nullptr, 0);
|
||||
fast_select_scan_total_.fetch_add(1, std::memory_order_relaxed);
|
||||
for (struct lwip_sock *sock : this->monitored_sockets_) {
|
||||
if (esphome_lwip_socket_has_data(sock)) {
|
||||
fast_select_scan_found_data_.fetch_add(1, std::memory_order_relaxed);
|
||||
if (fast_select_notify_value_before_scan == 0) {
|
||||
// Scan was load-bearing: no notification pending, so Take would have stalled.
|
||||
fast_select_scan_load_bearing_.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
yield();
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user