[core] Split feed_wdt into hot and cold entries

Separate Application::feed_wdt() into two entry points so the hot path
callers stop paying for the time==0 check they never trigger:

- feed_wdt_with_time(time): inline, hot path. Rate-limit check in 3
  Xtensa instructions (load + sub + branch). [[unlikely]] tells the
  compiler the slow branch is rare so the common path stays
  fall-through.
- feed_wdt(): cold, out of line. Fetches millis() and forwards through
  the same rate limit. Used by setup loops, upload helpers, yield(),
  and any other non-hot caller.

feed_wdt_slow_() is now pure worker code — 11 bytes. It just calls
arch_feed_wdt(), updates last_wdt_feed_, and runs the status LED
re-dispatch. Both entries have already confirmed the rate limit was
exceeded before calling.

Hot call sites updated:
- Application::loop() per-component feed
- Scheduler::execute_item_() after each scheduled item runs
- Application::teardown_components() inner loop (already has 'now')
This commit is contained in:
J. Nick Koston
2026-04-11 15:19:35 -10:00
parent dc5626eb85
commit 5926ca5369
3 changed files with 30 additions and 24 deletions
+18 -13
View File
@@ -196,21 +196,26 @@ void Application::process_dump_config_() {
this->dump_config_at_++;
}
void HOT Application::feed_wdt_slow_(uint32_t time) {
// Use provided time if available, otherwise get current time
uint32_t now = time ? time : millis();
// The inline wrapper already performs this check when time != 0;
// repeat it here for the time == 0 entry and as a safety net.
void Application::feed_wdt() {
// Cold entry: callers without a millis() timestamp in hand. Fetches the
// time and takes the same rate-limit path as feed_wdt_with_time().
uint32_t now = millis();
if (now - this->last_wdt_feed_ > WDT_FEED_INTERVAL_MS) {
arch_feed_wdt();
this->last_wdt_feed_ = now;
#ifdef USE_STATUS_LED
if (status_led::global_status_led != nullptr) {
status_led::global_status_led->call();
}
#endif
this->feed_wdt_slow_(now);
}
}
void HOT Application::feed_wdt_slow_(uint32_t time) {
// Callers (both feed_wdt() and feed_wdt_with_time()) have already
// confirmed the 3 ms rate limit was exceeded.
arch_feed_wdt();
this->last_wdt_feed_ = time;
#ifdef USE_STATUS_LED
if (status_led::global_status_led != nullptr) {
status_led::global_status_led->call();
}
#endif
}
void Application::reboot() {
ESP_LOGI(TAG, "Forcing a reboot");
for (auto &component : std::ranges::reverse_view(this->components_)) {
@@ -299,7 +304,7 @@ void Application::teardown_components(uint32_t timeout_ms) {
while (pending_count > 0 && (now - start_time) < timeout_ms) {
// Feed watchdog during teardown to prevent triggering
this->feed_wdt(now);
this->feed_wdt_with_time(now);
// Process components and compact the array, keeping only those still pending
size_t still_pending = 0;
+11 -10
View File
@@ -390,15 +390,16 @@ class Application {
/// watchdog timeout (seconds) has orders of magnitude of safety margin.
static constexpr uint32_t WDT_FEED_INTERVAL_MS = 3;
/// Feed the task watchdog. Hot-path inline rate-limit check: callers that
/// already have a timestamp in hand pay only a load + sub + branch on the
/// common (no-op) path. The actual arch feed + status LED update live in
/// feed_wdt_slow_ to keep this small enough to inline freely.
///
/// Pass time==0 to request millis() be read for you (low-frequency callers
/// only — always takes the slow path).
void ESPHOME_ALWAYS_INLINE feed_wdt(uint32_t time = 0) {
if (time == 0 || static_cast<uint32_t>(time - this->last_wdt_feed_) > WDT_FEED_INTERVAL_MS) {
/// Feed the task watchdog. Cold entry — callers without a millis()
/// timestamp in hand. Out of line to keep call sites tiny.
void feed_wdt();
/// Feed the task watchdog, hot entry. Callers that already have a
/// millis() timestamp pay only a load + sub + branch on the common
/// (no-op) path. The actual arch feed + status LED update live in
/// feed_wdt_slow_.
void ESPHOME_ALWAYS_INLINE feed_wdt_with_time(uint32_t time) {
if (static_cast<uint32_t>(time - this->last_wdt_feed_) > WDT_FEED_INTERVAL_MS) [[unlikely]] {
this->feed_wdt_slow_(time);
}
}
@@ -882,7 +883,7 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() {
}
new_app_state |= component->get_component_state();
this->app_state_ |= new_app_state;
this->feed_wdt(last_op_end_time);
this->feed_wdt_with_time(last_op_end_time);
}
this->after_loop_tasks_();
+1 -1
View File
@@ -744,7 +744,7 @@ uint32_t HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) {
// queue paths go through here). A run of back-to-back callbacks cannot
// starve the wdt. The inline fast path is a load + sub + branch — nearly
// free when the 3 ms rate limit hasn't elapsed.
App.feed_wdt(end);
App.feed_wdt_with_time(end);
return end;
}