-
-
Notifications
You must be signed in to change notification settings - Fork 10
[blog] Add breaking change blog post for Event entity memory optimizations #71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
bdraco
wants to merge
7
commits into
main
Choose a base branch
from
event_changes
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
6097aa9
[blog] Add breaking change blog post for Event entity memory optimiza…
bdraco 03c81e6
fixes
bdraco f17ef08
fixes
bdraco c02cc6c
fixes
bdraco 900ac8b
fixes
bdraco 6ca1b58
fixes
bdraco d943bfc
Merge branch 'main' into event_changes
bdraco File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
322 changes: 322 additions & 0 deletions
322
docs/blog/posts/2025-11-07-event-entity-optimizations.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,322 @@ | ||
| --- | ||
| date: 2025-11-07 | ||
| authors: | ||
| - bdraco | ||
| comments: true | ||
| --- | ||
|
|
||
| # Event Entity Class: Memory Optimizations | ||
|
|
||
| ESPHome 2025.11.0 introduces memory optimizations to the `Event` entity class that reduce heap usage and store event type strings in flash. These changes affect external components implementing custom event devices. | ||
|
|
||
| <!-- more --> | ||
|
|
||
| ## Background | ||
|
|
||
| ### Motivation | ||
|
|
||
| Event components use a set of valid event type strings to validate triggers. Using `std::set<std::string>` for this small list (typically 1-5 event types) wastes significant memory: | ||
|
|
||
| - **std::set overhead**: ~80 bytes of base red-black tree structure | ||
| - **Per-member overhead**: Each string requires tree node allocation (~24+ bytes) | ||
| - **std::string overhead**: Each string allocates on heap (~24 bytes overhead + string content) | ||
| - **Performance overhead**: O(log n) tree traversal for lookups, even for tiny datasets | ||
|
|
||
| For typical event use cases with 1-5 event types, a simple vector with linear search is both faster and uses far less memory. Linear search on 1-5 items is faster than tree traversal due to better cache locality. | ||
|
|
||
| Additionally, storing event type strings as `const char *` pointers to string literals moves the data from heap to flash (ESP32) or rodata (ESP8266), further reducing memory pressure. | ||
|
|
||
| ### Changes | ||
|
|
||
| Event type storage has been optimized through two PRs to eliminate memory overhead and move strings to flash: | ||
|
|
||
| **[PR #11463](https://github.com/esphome/esphome/pull/11463): `std::set<std::string>` → `FixedVector<std::string>`** | ||
| - Eliminated ~80 bytes std::set base overhead | ||
| - Eliminated per-member tree node overhead (~24+ bytes per event type) | ||
| - Single allocation, no reallocation overhead | ||
|
|
||
| **[PR #11767](https://github.com/esphome/esphome/pull/11767): `FixedVector<std::string>` → `FixedVector<const char *>`** | ||
| - Eliminates std::string heap allocations (~24 bytes overhead + content per event type) | ||
| - **ESP32**: Event type strings stored in flash (ROM), freeing heap | ||
| - **ESP8266**: Event type strings in rodata section (still RAM, but eliminates std::string overhead) | ||
| - Uses `FixedVector<const char *>` (same as Select entity pattern - returned by const reference, never by value) | ||
|
|
||
| **Combined result:** Event types changed from `std::set<std::string>` → `FixedVector<const char *>` | ||
|
|
||
| **Performance:** O(n) linear search with strcmp, faster than O(log n) tree traversal for typical 1-5 event types due to cache locality. | ||
|
|
||
| ## What's Changing | ||
|
|
||
| ### For ESPHome 2025.11.0 and Later | ||
|
|
||
| **Event Type Storage Changes (Breaking - two PRs in same release):** | ||
|
|
||
| ```cpp | ||
| // OLD (before 2025.11.0) - std::set<std::string> in heap | ||
| #include <set> | ||
| std::set<std::string> event_types = {"button_press", "button_release"}; | ||
| event->set_event_types(event_types); | ||
| std::set<std::string> types = event->get_event_types(); | ||
|
|
||
| // NEW (2025.11.0) - FixedVector<const char *> (initializer list still works!) | ||
| event->set_event_types({"button_press", "button_release"}); | ||
| const FixedVector<const char *> &types = event->get_event_types(); | ||
| ``` | ||
|
|
||
| **Note:** If your external component was already updated for the intermediate `FixedVector<std::string>` change, you'll need to update again for `FixedVector<const char *>`. Most components using initializer list syntax require minimal changes. | ||
|
|
||
| ## Who This Affects | ||
|
|
||
| **Event Type Changes (PR #11767) - External components need updates:** | ||
|
|
||
| - **Breaking change:** `set_event_types()` now requires `const char *` strings (string literals) | ||
| - **Breaking change:** `get_event_types()` now returns `const FixedVector<const char *> &` instead of `std::set<std::string>` | ||
| - **Breaking change:** `last_event_type` is now private - use `get_last_event_type()` accessor instead | ||
| - Most components already pass string literal initializer lists like `{"button_press", "button_release"}` which continue to work | ||
| - Setter accepts `initializer_list`, `FixedVector`, or `std::vector` of `const char *` (same as Select options pattern) | ||
| - Loop variables must change from `const std::string &` to `const char *` | ||
| - Event type comparisons must use `strcmp()` instead of `==` operator | ||
| - No core ESPHome components needed changes (all used initializer list syntax) | ||
|
|
||
| **Standard YAML configurations** work without code changes. | ||
|
|
||
| ## Migration Guide for External Components | ||
|
|
||
| ### 1. Update Container Types (If Storing Event Types) | ||
|
|
||
| ```cpp | ||
| // OLD | ||
| #include <set> | ||
| std::set<std::string> event_types_; | ||
|
|
||
| // NEW - Use FixedVector (most common) | ||
| FixedVector<const char *> event_types_; | ||
|
|
||
| // OR - Use std::vector if you need copyability | ||
| #include <vector> | ||
| std::vector<const char *> event_types_; | ||
| ``` | ||
|
|
||
| ### 2. Update Setter Calls | ||
|
|
||
| **Most components already use string literals and won't need changes:** | ||
|
|
||
| ```cpp | ||
| // STILL WORKS - string literal initializer list (most common, no changes needed) | ||
| event->set_event_types({"button_press", "button_release"}); | ||
|
|
||
| // OLD - explicitly creating std::set<std::string> (will fail to compile) | ||
| std::set<std::string> types = {"button_press", "button_release"}; | ||
| event->set_event_types(types); // Deleted overload, no longer supported | ||
| ``` | ||
|
|
||
| **Note:** Accepts initializer list, `FixedVector<const char *>`, or `std::vector<const char *>`. Same pattern as Select options. | ||
|
|
||
| ### 3. Update Getter Usage (Required If You Call get_event_types()) | ||
|
|
||
| ```cpp | ||
| // OLD - returns std::set<std::string> by value | ||
| std::set<std::string> types = event->get_event_types(); | ||
| for (const std::string &type : types) { | ||
| ESP_LOGD("event", "Type: %s", type.c_str()); | ||
| } | ||
|
|
||
| // NEW - returns const FixedVector<const char *> & | ||
| const FixedVector<const char *> &types = event->get_event_types(); | ||
| for (const char *type : types) { | ||
| ESP_LOGD("event", "Type: %s", type); | ||
| } | ||
|
|
||
| // Or use auto | ||
| const auto &types = event->get_event_types(); | ||
| for (const char *type : types) { | ||
| ESP_LOGD("event", "Type: %s", type); | ||
| } | ||
| ``` | ||
|
|
||
| ### 4. Update last_event_type Access (If You Access This Field) | ||
|
|
||
| ```cpp | ||
| // OLD - direct field access | ||
| if (event->last_event_type != nullptr) { | ||
| ESP_LOGD("event", "Last: %s", event->last_event_type); | ||
| } | ||
|
|
||
| // NEW - use getter | ||
| if (event->get_last_event_type() != nullptr) { | ||
| ESP_LOGD("event", "Last: %s", event->get_last_event_type()); | ||
| } | ||
| ``` | ||
|
|
||
| **Why this change?** Moving `last_event_type` to private with controlled access ensures pointer lifetime safety. The field is now guaranteed to always point to a string in `types_` (or be nullptr), preventing dangling pointer bugs when event types are reconfigured. | ||
|
|
||
| ### 5. Update Event Type Lookups (If You Check Valid Types Manually) | ||
|
|
||
| ```cpp | ||
| // OLD - std::set::find with std::string | ||
| auto types = event->get_event_types(); | ||
| if (types.find("button_press") != types.end()) { | ||
| // Type is valid | ||
| } | ||
|
|
||
| // NEW - linear search with strcmp | ||
| const auto &types = event->get_event_types(); | ||
| bool found = false; | ||
| for (const char *type : types) { | ||
| if (strcmp(type, "button_press") == 0) { | ||
| found = true; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| // OR - use std::find_if with strcmp | ||
| #include <algorithm> | ||
| const auto &types = event->get_event_types(); | ||
| auto it = std::find_if(types.begin(), types.end(), | ||
| [](const char *t) { return strcmp(t, "button_press") == 0; }); | ||
| if (it != types.end()) { | ||
| // Type is valid | ||
| } | ||
| ``` | ||
|
|
||
| **Note:** For typical event use cases (1-5 event types), linear search with strcmp is faster than std::set due to better cache locality. | ||
|
|
||
| ## Complete Migration Example | ||
|
|
||
| ### Custom Event Component | ||
|
|
||
| **Before:** | ||
| ```cpp | ||
| #include <set> | ||
|
|
||
| class MyEvent : public event::Event { | ||
| public: | ||
| void setup() override { | ||
| std::set<std::string> types = {"motion_detected", "motion_cleared"}; | ||
| this->set_event_types(types); | ||
| } | ||
|
|
||
| void check_valid_type(const std::string &type) { | ||
| auto types = this->get_event_types(); | ||
| if (types.find(type) != types.end()) { | ||
| ESP_LOGD("event", "Valid type: %s", type.c_str()); | ||
| } | ||
| } | ||
|
|
||
| void iterate_types() { | ||
| for (const std::string &type : this->get_event_types()) { | ||
| ESP_LOGD("event", "Type: %s", type.c_str()); | ||
| } | ||
| } | ||
|
|
||
| void log_last_event() { | ||
| if (this->last_event_type != nullptr) { | ||
| ESP_LOGD("event", "Last event: %s", this->last_event_type); | ||
| } | ||
| } | ||
| }; | ||
| ``` | ||
|
|
||
| **After:** | ||
| ```cpp | ||
| #include <cstring> | ||
|
|
||
| class MyEvent : public event::Event { | ||
| public: | ||
| void setup() override { | ||
| // String literals work directly (most common pattern) | ||
| this->set_event_types({"motion_detected", "motion_cleared"}); | ||
| } | ||
|
|
||
| void check_valid_type(const std::string &type) { | ||
| const auto &types = this->get_event_types(); | ||
| bool found = false; | ||
| for (const char *t : types) { | ||
| if (strcmp(t, type.c_str()) == 0) { | ||
| found = true; | ||
| break; | ||
| } | ||
| } | ||
| if (found) { | ||
| ESP_LOGD("event", "Valid type: %s", type.c_str()); | ||
| } | ||
| } | ||
|
|
||
| void iterate_types() { | ||
| // Loop variable changed to const char * | ||
| for (const char *type : this->get_event_types()) { | ||
| ESP_LOGD("event", "Type: %s", type); // No .c_str() needed | ||
| } | ||
| } | ||
|
|
||
| void log_last_event() { | ||
| // Use getter instead of direct field access | ||
| if (this->get_last_event_type() != nullptr) { | ||
| ESP_LOGD("event", "Last event: %s", this->get_last_event_type()); | ||
| } | ||
| } | ||
| }; | ||
| ``` | ||
|
|
||
| ## `FixedVector<const char *>` Benefits | ||
|
|
||
| For typical event use cases (1-5 event types): | ||
|
|
||
| - **Memory**: Eliminates ~80 bytes std::set overhead + per-node overhead + std::string overhead | ||
| - **Lookup speed**: O(n) linear search with strcmp, faster than O(log n) tree traversal for small n due to cache locality | ||
| - **Flash storage**: String literals stored in flash (ESP32) or rodata (ESP8266), not heap | ||
| - **Single allocation**: FixedVector allocates once at runtime initialization, no reallocation overhead | ||
|
|
||
| ### Platform-Specific Benefits | ||
|
|
||
| **ESP32 (IDF and Arduino):** | ||
| - Event type strings stored in flash memory (ROM) | ||
| - Zero heap usage for event type strings | ||
| - Maximum heap available for other operations | ||
|
|
||
| **ESP8266:** | ||
| - Event type strings in rodata section (still occupies RAM) | ||
| - Eliminates std::string heap overhead (~24 bytes per string + content) | ||
| - Reduces heap fragmentation | ||
|
|
||
| ## Timeline | ||
|
|
||
| - **ESPHome 2025.11.0 (November 2025):** | ||
| - Both optimizations active in same release | ||
| - [PR #11463](https://github.com/esphome/esphome/pull/11463): `std::set<std::string>` → `FixedVector<std::string>` | ||
| - [PR #11767](https://github.com/esphome/esphome/pull/11767): `FixedVector<std::string>` → `FixedVector<const char *>` | ||
| - External components only need to migrate once (from `std::set<std::string>` to `FixedVector<const char *>`) | ||
|
|
||
| ## Finding Code That Needs Updates | ||
|
|
||
| Search your external component code for these patterns: | ||
|
|
||
| ```bash | ||
| # Find std::set usage for event types | ||
| grep -r 'std::set<.*string>.*event' --include='*.cpp' --include='*.h' | ||
|
|
||
| # Find set_event_types calls | ||
| grep -r 'set_event_types' --include='*.cpp' --include='*.h' | ||
|
|
||
| # Find get_event_types calls | ||
| grep -r 'get_event_types' --include='*.cpp' --include='*.h' | ||
|
|
||
| # Find event type lookups with std::string | ||
| grep -r 'for.*std::string.*get_event_types' --include='*.cpp' --include='*.h' | ||
|
|
||
| # Find std::string comparisons that might need strcmp | ||
| grep -r 'event_type.*==' --include='*.cpp' --include='*.h' | ||
|
|
||
| # Find direct last_event_type field access | ||
| grep -r '->last_event_type\|\.last_event_type' --include='*.cpp' --include='*.h' | ||
| ``` | ||
|
|
||
| ## Questions? | ||
|
|
||
| If you have questions about these changes or need help migrating your external component, please ask in the [ESPHome Discord](https://discord.gg/KhAMKrd) or open a [discussion on GitHub](https://github.com/esphome/esphome/discussions). | ||
|
|
||
| ## Related Documentation | ||
|
|
||
| - [Event Component Documentation](https://esphome.io/components/event/index.html) | ||
| - [PR #11463: Replace std::set with FixedVector for event type storage](https://github.com/esphome/esphome/pull/11463) | ||
| - [PR #11767: Store event types in flash memory](https://github.com/esphome/esphome/pull/11767) | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The date 'November 2025' appears to be incorrect. The file is named '2025-11-07-event-entity-optimizations.md' which indicates November 7, 2025, but the current date context suggests this may be a future date that hasn't occurred yet. Please verify this is the intended release date.