From d203a46ef808f76c22018eec570bab9f29b999a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 18:17:37 -1000 Subject: [PATCH 1/7] [api] Enable HAVE_WEAK_SYMBOLS and HAVE_INLINE_ASM for libsodium (#15038) --- esphome/components/api/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 4c3cf81927d..84589d540d3 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -455,6 +455,9 @@ async def to_code(config: ConfigType) -> None: cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_NOISE") cg.add_library("esphome/noise-c", "0.1.11") + # Enable optimized memzero/memcmp in libsodium instead of volatile byte loops + cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1") + cg.add_build_flag("-DHAVE_INLINE_ASM=1") else: cg.add_define("USE_API_PLAINTEXT") From 34345b86fa6ca0252c8029a14bc8f7a1e5859499 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 18:36:07 -1000 Subject: [PATCH 2/7] [core] Reduce automation call chain stack depth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Force-inline the Trigger→Automation→ActionList forwarding chain and override play_complex() in leaf action classes to skip the virtual play() dispatch, reducing the button→lambda call stack from 8 frames to ~4. --- esphome/core/automation.h | 6 +++--- esphome/core/base_automation.h | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 7934fdbec97..4bc4bdf71e0 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -322,7 +322,7 @@ template class Automation; template class Trigger { public: /// Inform the parent automation that the event has triggered. - void trigger(const Ts &...x) { + __attribute__((always_inline)) void trigger(const Ts &...x) { if (this->automation_parent_ == nullptr) return; this->automation_parent_->trigger(x...); @@ -429,7 +429,7 @@ template class ActionList { this->add_action(action); } } - void play(const Ts &...x) { + __attribute__((always_inline)) void play(const Ts &...x) { if (this->actions_begin_ != nullptr) this->actions_begin_->play_complex(x...); } @@ -473,7 +473,7 @@ template class Automation { void stop() { this->actions_.stop(); } - void trigger(const Ts &...x) { this->actions_.play(x...); } + __attribute__((always_inline)) void trigger(const Ts &...x) { this->actions_.play(x...); } bool is_running() { return this->actions_.is_running(); } diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 985f26e7116..6690c7d236e 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -217,6 +217,11 @@ template class LambdaAction : public Action { public: explicit LambdaAction(std::function &&f) : f_(std::move(f)) {} + void play_complex(const Ts &...x) override { + this->num_running_++; + this->f_(x...); + this->play_next_(x...); + } void play(const Ts &...x) override { this->f_(x...); } protected: @@ -230,6 +235,11 @@ template class StatelessLambdaAction : public Action { public: explicit StatelessLambdaAction(void (*f)(Ts...)) : f_(f) {} + void play_complex(const Ts &...x) override { + this->num_running_++; + this->f_(x...); + this->play_next_(x...); + } void play(const Ts &...x) override { this->f_(x...); } protected: @@ -243,6 +253,11 @@ template class ContinuationAction : public Action { public: explicit ContinuationAction(Action *parent) : parent_(parent) {} + void play_complex(const Ts &...x) override { + this->num_running_++; + this->parent_->play_next_(x...); + this->play_next_(x...); + } void play(const Ts &...x) override { this->parent_->play_next_(x...); } protected: From 536093acfa481b26fb4db245e7232cd9a5cdd12b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 18:46:46 -1000 Subject: [PATCH 3/7] [core] Drop always_inline attributes to avoid flash bloat The always_inline on Trigger::trigger(), ActionList::play(), and Automation::trigger() duplicates code at every trigger call site, adding ~384 bytes of flash. Revert to compiler-managed inlining and keep only the play_complex overrides (phase 2). --- esphome/core/automation.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 4bc4bdf71e0..7934fdbec97 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -322,7 +322,7 @@ template class Automation; template class Trigger { public: /// Inform the parent automation that the event has triggered. - __attribute__((always_inline)) void trigger(const Ts &...x) { + void trigger(const Ts &...x) { if (this->automation_parent_ == nullptr) return; this->automation_parent_->trigger(x...); @@ -429,7 +429,7 @@ template class ActionList { this->add_action(action); } } - __attribute__((always_inline)) void play(const Ts &...x) { + void play(const Ts &...x) { if (this->actions_begin_ != nullptr) this->actions_begin_->play_complex(x...); } @@ -473,7 +473,7 @@ template class Automation { void stop() { this->actions_.stop(); } - __attribute__((always_inline)) void trigger(const Ts &...x) { this->actions_.play(x...); } + void trigger(const Ts &...x) { this->actions_.play(x...); } bool is_running() { return this->actions_.is_running(); } From 884db8060a345a0401205b0deb0206a3d987a591 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 18:48:51 -1000 Subject: [PATCH 4/7] [core] Use qualified calls and add comments explaining overrides --- esphome/core/base_automation.h | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 6690c7d236e..b809c56f1ab 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -217,9 +217,11 @@ template class LambdaAction : public Action { public: explicit LambdaAction(std::function &&f) : f_(std::move(f)) {} + // Override play_complex to call play() non-virtually (qualified call), + // eliminating one virtual dispatch frame from the call stack. void play_complex(const Ts &...x) override { this->num_running_++; - this->f_(x...); + LambdaAction::play(x...); this->play_next_(x...); } void play(const Ts &...x) override { this->f_(x...); } @@ -235,9 +237,11 @@ template class StatelessLambdaAction : public Action { public: explicit StatelessLambdaAction(void (*f)(Ts...)) : f_(f) {} + // Override play_complex to call play() non-virtually (qualified call), + // eliminating one virtual dispatch frame from the call stack. void play_complex(const Ts &...x) override { this->num_running_++; - this->f_(x...); + StatelessLambdaAction::play(x...); this->play_next_(x...); } void play(const Ts &...x) override { this->f_(x...); } @@ -253,9 +257,11 @@ template class ContinuationAction : public Action { public: explicit ContinuationAction(Action *parent) : parent_(parent) {} + // Override play_complex to call play() non-virtually (qualified call), + // eliminating one virtual dispatch frame from the call stack. void play_complex(const Ts &...x) override { this->num_running_++; - this->parent_->play_next_(x...); + ContinuationAction::play(x...); this->play_next_(x...); } void play(const Ts &...x) override { this->parent_->play_next_(x...); } From aacc7e54d2aa54ed0650be5321b757ce3dfda8b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 18:52:12 -1000 Subject: [PATCH 5/7] [core] Limit play_complex override to StatelessLambdaAction only LambdaAction and ContinuationAction overrides caused flash bloat by replacing shared base class play_complex instantiations with per-class copies. StatelessLambdaAction is the most common automation action and its no-args variant is only 24 bytes. --- esphome/core/base_automation.h | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index b809c56f1ab..f0b3898278c 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -217,13 +217,6 @@ template class LambdaAction : public Action { public: explicit LambdaAction(std::function &&f) : f_(std::move(f)) {} - // Override play_complex to call play() non-virtually (qualified call), - // eliminating one virtual dispatch frame from the call stack. - void play_complex(const Ts &...x) override { - this->num_running_++; - LambdaAction::play(x...); - this->play_next_(x...); - } void play(const Ts &...x) override { this->f_(x...); } protected: @@ -257,13 +250,6 @@ template class ContinuationAction : public Action { public: explicit ContinuationAction(Action *parent) : parent_(parent) {} - // Override play_complex to call play() non-virtually (qualified call), - // eliminating one virtual dispatch frame from the call stack. - void play_complex(const Ts &...x) override { - this->num_running_++; - ContinuationAction::play(x...); - this->play_next_(x...); - } void play(const Ts &...x) override { this->parent_->play_next_(x...); } protected: From 9227d8f2dde7e8acd99a1aaf29d1f3da94a75132 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 18:52:48 -1000 Subject: [PATCH 6/7] [core] Restore play_complex overrides for LambdaAction and ContinuationAction The +384 flash was from the always_inline attributes (already removed), not from the play_complex overrides. --- esphome/core/base_automation.h | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index f0b3898278c..b809c56f1ab 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -217,6 +217,13 @@ template class LambdaAction : public Action { public: explicit LambdaAction(std::function &&f) : f_(std::move(f)) {} + // Override play_complex to call play() non-virtually (qualified call), + // eliminating one virtual dispatch frame from the call stack. + void play_complex(const Ts &...x) override { + this->num_running_++; + LambdaAction::play(x...); + this->play_next_(x...); + } void play(const Ts &...x) override { this->f_(x...); } protected: @@ -250,6 +257,13 @@ template class ContinuationAction : public Action { public: explicit ContinuationAction(Action *parent) : parent_(parent) {} + // Override play_complex to call play() non-virtually (qualified call), + // eliminating one virtual dispatch frame from the call stack. + void play_complex(const Ts &...x) override { + this->num_running_++; + ContinuationAction::play(x...); + this->play_next_(x...); + } void play(const Ts &...x) override { this->parent_->play_next_(x...); } protected: From bfa446a28ea27d3ee5bfc9d104036fd233ec4f40 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2026 18:59:45 -1000 Subject: [PATCH 7/7] [core] Switch to always_inline only, drop play_complex overrides The play_complex overrides cost flash per template instantiation across every automation. The always_inline on the forwarding chain (Trigger::trigger, Automation::trigger, ActionList::play) is a fixed cost that collapses 3 frames into 1. --- esphome/core/automation.h | 12 +++++++++--- esphome/core/base_automation.h | 21 --------------------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 7934fdbec97..954565aca0a 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -322,7 +322,9 @@ template class Automation; template class Trigger { public: /// Inform the parent automation that the event has triggered. - void trigger(const Ts &...x) { + // Force-inline: collapses the Trigger→Automation→ActionList forwarding + // chain into a single frame, reducing automation call stack depth. + __attribute__((always_inline)) void trigger(const Ts &...x) { if (this->automation_parent_ == nullptr) return; this->automation_parent_->trigger(x...); @@ -429,7 +431,9 @@ template class ActionList { this->add_action(action); } } - void play(const Ts &...x) { + // Force-inline: part of the Trigger→Automation→ActionList forwarding + // chain collapsed to reduce automation call stack depth. + __attribute__((always_inline)) void play(const Ts &...x) { if (this->actions_begin_ != nullptr) this->actions_begin_->play_complex(x...); } @@ -473,7 +477,9 @@ template class Automation { void stop() { this->actions_.stop(); } - void trigger(const Ts &...x) { this->actions_.play(x...); } + // Force-inline: part of the Trigger→Automation→ActionList forwarding + // chain collapsed to reduce automation call stack depth. + __attribute__((always_inline)) void trigger(const Ts &...x) { this->actions_.play(x...); } bool is_running() { return this->actions_.is_running(); } diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index b809c56f1ab..985f26e7116 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -217,13 +217,6 @@ template class LambdaAction : public Action { public: explicit LambdaAction(std::function &&f) : f_(std::move(f)) {} - // Override play_complex to call play() non-virtually (qualified call), - // eliminating one virtual dispatch frame from the call stack. - void play_complex(const Ts &...x) override { - this->num_running_++; - LambdaAction::play(x...); - this->play_next_(x...); - } void play(const Ts &...x) override { this->f_(x...); } protected: @@ -237,13 +230,6 @@ template class StatelessLambdaAction : public Action { public: explicit StatelessLambdaAction(void (*f)(Ts...)) : f_(f) {} - // Override play_complex to call play() non-virtually (qualified call), - // eliminating one virtual dispatch frame from the call stack. - void play_complex(const Ts &...x) override { - this->num_running_++; - StatelessLambdaAction::play(x...); - this->play_next_(x...); - } void play(const Ts &...x) override { this->f_(x...); } protected: @@ -257,13 +243,6 @@ template class ContinuationAction : public Action { public: explicit ContinuationAction(Action *parent) : parent_(parent) {} - // Override play_complex to call play() non-virtually (qualified call), - // eliminating one virtual dispatch frame from the call stack. - void play_complex(const Ts &...x) override { - this->num_running_++; - ContinuationAction::play(x...); - this->play_next_(x...); - } void play(const Ts &...x) override { this->parent_->play_next_(x...); } protected: