-
-
Notifications
You must be signed in to change notification settings - Fork 11
[blog] Add breaking change blog post for Light entity memory optimizations #70
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
Merged
Merged
Changes from 2 commits
Commits
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
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
372 changes: 372 additions & 0 deletions
372
docs/blog/posts/2025-11-07-light-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,372 @@ | ||
| --- | ||
| date: 2025-11-07 | ||
| authors: | ||
| - bdraco | ||
| comments: true | ||
| --- | ||
|
|
||
| # Light Entity Class: Memory Optimizations | ||
|
|
||
| ESPHome 2025.11.0 introduces memory optimizations to the `Light` entity class that reduce both heap and flash usage. These changes affect external components implementing custom light devices and effects. | ||
|
|
||
| <!-- more --> | ||
|
|
||
| ## Background | ||
|
|
||
| ### Motivation | ||
|
|
||
| Light components are among the most commonly used in ESPHome, and their memory footprint directly impacts available heap for runtime operations. On ESP8266 devices with only ~40KB of usable heap, every optimization matters: | ||
|
|
||
| - **Effect names** were stored as `std::string` objects on the heap, consuming allocation overhead (typically 24+ bytes) plus the string data for each effect | ||
| - **Color mode sets** used `std::set<ColorMode>` with red-black tree structure, consuming ~80 bytes of base overhead plus additional overhead for each member (2-4 modes typical) | ||
| - Light components often have multiple effects (5-10 common) and support several color modes, making this overhead multiply across each light entity | ||
|
|
||
| Moving effect names to flash and color modes to a 2-byte bitmask frees heap memory for runtime operations like network buffers, API operations, and component state. The bitmask also provides much faster O(1) lookups compared to the red-black tree's more expensive O(log n) operations. | ||
|
|
||
| ### Changes | ||
|
|
||
| Two optimization PRs improved Light entity memory usage: | ||
|
|
||
| **[PR #11487](https://github.com/esphome/esphome/pull/11487): Store Effect Names in Flash** | ||
| Changes effect name storage from `std::string` (heap) to `const char*` (flash). Saves string allocation overhead plus string data for each effect. | ||
|
|
||
| **[PR #11348](https://github.com/esphome/esphome/pull/11348): Use Bitmask for Color Modes** | ||
| Changes color mode storage from `std::set<ColorMode>` (red-black tree on heap) to `ColorModeMask` (uint16_t bitmask, 2 bytes). Saves ~80 bytes of base `std::set` overhead plus per-member overhead. Provides much faster O(1) lookups. | ||
|
|
||
| ## What's Changing | ||
|
|
||
| ### For ESPHome 2025.11.0 and Later | ||
|
|
||
| **Effect Name Storage Changes (Breaking - [PR #11487](https://github.com/esphome/esphome/pull/11487)):** | ||
| ```cpp | ||
| // OLD - std::string in heap | ||
| class MyEffect : public LightEffect { | ||
| public: | ||
| explicit MyEffect(const std::string &name) : LightEffect(name) {} | ||
| }; | ||
|
|
||
| // NEW - const char* in flash | ||
| class MyEffect : public LightEffect { | ||
| public: | ||
| explicit MyEffect(const char *name) : LightEffect(name) {} | ||
| }; | ||
| ``` | ||
|
|
||
| **Color Mode Storage Changes (Mostly Backward Compatible - [PR #11348](https://github.com/esphome/esphome/pull/11348)):** | ||
| ```cpp | ||
| // OLD - std::set in heap | ||
| #include <set> | ||
| std::set<ColorMode> modes = {ColorMode::RGB, ColorMode::WHITE}; | ||
| traits.set_supported_color_modes(modes); | ||
| bool supports = traits.get_supported_color_modes().count(ColorMode::RGB); | ||
|
|
||
| // NEW - ColorModeMask bitmask (but old syntax still works!) | ||
| #include "color_mode.h" | ||
| traits.set_supported_color_modes({ColorMode::RGB, ColorMode::WHITE}); | ||
| bool supports = traits.get_supported_color_modes().count(ColorMode::RGB); // .count() still works | ||
| ``` | ||
|
|
||
| ## Who This Affects | ||
|
|
||
| **Effect Name Changes (PR #11487) - External components likely affected:** | ||
| - Any custom `LightEffect` subclasses need constructor updates from `std::string` to `const char*` | ||
| - Any code that calls `get_name()` on effects | ||
|
|
||
| **Color Mode Changes (PR #11348) - External components mostly backward compatible:** | ||
| - `ColorModeMask` provides backward compatibility with `std::set` API (has `.count()`, `.insert()`, `.erase()`, `.size()`, `.empty()`) | ||
| - Most components already pass initializer lists like `{ColorMode::RGB, ColorMode::WHITE}` which continue to work | ||
| - No core components needed changes, so external components are unlikely to need changes | ||
| - **Breaking edge case:** Explicitly passing `std::set<ColorMode>` to `set_supported_color_modes()` will fail to compile (but this pattern was not used in any core components) | ||
|
|
||
| **Standard YAML configurations** work without code changes. | ||
|
|
||
| ## Migration Guide for External Components | ||
|
|
||
| ### Part 1: Effect Name Changes | ||
|
|
||
| #### 1. Update Effect Constructors (Required Now) | ||
|
|
||
| ```cpp | ||
| // OLD | ||
| #include <string> | ||
|
|
||
| class MyEffect : public LightEffect { | ||
| public: | ||
| explicit MyEffect(const std::string &name) : LightEffect(name) {} | ||
| }; | ||
|
|
||
| // NEW - use const char* | ||
| class MyEffect : public LightEffect { | ||
| public: | ||
| explicit MyEffect(const char *name) : LightEffect(name) {} | ||
| }; | ||
| ``` | ||
|
|
||
| #### 2. Update Effect get_name() Usage (Required Now) | ||
|
|
||
| ```cpp | ||
| // OLD - returns const std::string& | ||
| const std::string &name = effect->get_name(); | ||
| if (name == "My Effect") { } | ||
|
|
||
| // NEW - returns const char* | ||
| const char *name = effect->get_name(); | ||
| if (strcmp(name, "My Effect") == 0) { } | ||
| ``` | ||
|
|
||
| #### 3. Remove Unnecessary String Includes | ||
|
|
||
| ```cpp | ||
| // Remove if only used for effect names: | ||
| #include <string> | ||
| ``` | ||
|
|
||
| ### Part 2: Color Mode Changes | ||
|
|
||
| #### 1. Update Container Types (Required Now) | ||
|
|
||
| ```cpp | ||
| // OLD | ||
| #include <set> | ||
| std::set<ColorMode> modes_; | ||
|
|
||
| // NEW | ||
| #include "esphome/components/light/color_mode.h" | ||
| ColorModeMask modes_; | ||
| ``` | ||
|
|
||
| #### 2. Update Setter Calls (Only If You Explicitly Created std::set Variables) | ||
|
|
||
| **Note:** Most components already pass initializer lists directly and don't need changes. This only affects code that explicitly creates `std::set` variables (which no core components did). | ||
|
|
||
| ```cpp | ||
| // OLD - explicitly creating std::set (uncommon, will fail to compile) | ||
| std::set<ColorMode> modes = {ColorMode::RGB, ColorMode::WHITE}; | ||
| traits.set_supported_color_modes(modes); // No longer has std::set overload | ||
|
|
||
| // NEW - initializer list (most components already did this) | ||
| traits.set_supported_color_modes({ColorMode::RGB, ColorMode::WHITE}); | ||
|
|
||
| // OR - change std::set to ColorModeMask | ||
| ColorModeMask modes({ColorMode::RGB, ColorMode::WHITE}); | ||
| traits.set_supported_color_modes(modes); // Works | ||
| ``` | ||
|
|
||
| #### 3. Lookups Work Without Changes | ||
|
|
||
| **Note:** `ColorModeMask` provides `.count()` for backward compatibility, so no changes are required. | ||
|
|
||
| ```cpp | ||
| // Works exactly as before - no changes needed | ||
| if (traits.get_supported_color_modes().count(ColorMode::RGB)) { | ||
| // mode is supported | ||
| } | ||
| ``` | ||
|
|
||
| #### 4. Update Capability Checks (Optional - But Recommended) | ||
|
|
||
| **Note:** The old manual loop still works due to iterator compatibility, but using `supports_color_capability()` is cleaner. | ||
|
|
||
| ```cpp | ||
| // OLD - manual loop (still works) | ||
| bool has_brightness = false; | ||
| for (auto mode : traits.get_supported_color_modes()) { | ||
| if (mode & ColorCapability::BRIGHTNESS) { | ||
| has_brightness = true; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| // NEW - recommended approach | ||
| bool has_brightness = traits.supports_color_capability(ColorCapability::BRIGHTNESS); | ||
| ``` | ||
|
|
||
| ## Complete Migration Examples | ||
|
|
||
| ### Example 1: Custom Effect | ||
|
|
||
| **Before:** | ||
| ```cpp | ||
| #include <string> | ||
|
|
||
| class RainbowEffect : public LightEffect { | ||
| public: | ||
| explicit RainbowEffect(const std::string &name) : LightEffect(name) {} | ||
|
|
||
| void apply() override { | ||
| const std::string &name = this->get_name(); | ||
| ESP_LOGD("effect", "Applying %s", name.c_str()); | ||
| // Effect implementation | ||
| } | ||
| }; | ||
| ``` | ||
|
|
||
| **After:** | ||
| ```cpp | ||
| class RainbowEffect : public LightEffect { | ||
| public: | ||
| explicit RainbowEffect(const char *name) : LightEffect(name) {} | ||
|
|
||
| void apply() override { | ||
| const char *name = this->get_name(); | ||
| ESP_LOGD("effect", "Applying %s", name); | ||
| // Effect implementation | ||
| } | ||
| }; | ||
| ``` | ||
|
|
||
| ### Example 2: Custom Light Component | ||
|
|
||
| **Before:** | ||
| ```cpp | ||
| #include <set> | ||
|
|
||
| class MyLight : public LightOutput { | ||
| public: | ||
| LightTraits get_traits() override { | ||
| auto traits = LightTraits(); | ||
| std::set<ColorMode> modes = {ColorMode::RGB, ColorMode::WHITE}; | ||
| traits.set_supported_color_modes(modes); | ||
| return traits; | ||
| } | ||
|
|
||
| void check_mode_support(ColorMode mode) { | ||
| auto modes = this->get_traits().get_supported_color_modes(); | ||
| if (modes.count(mode)) { | ||
| // Mode is supported | ||
| } | ||
| } | ||
| }; | ||
| ``` | ||
|
|
||
| **After:** | ||
| ```cpp | ||
| #include "esphome/components/light/color_mode.h" | ||
|
|
||
| class MyLight : public LightOutput { | ||
| public: | ||
| LightTraits get_traits() override { | ||
| auto traits = LightTraits(); | ||
| traits.set_supported_color_modes({ColorMode::RGB, ColorMode::WHITE}); | ||
| return traits; | ||
| } | ||
|
|
||
| void check_mode_support(ColorMode mode) { | ||
| auto modes = this->get_traits().get_supported_color_modes(); | ||
| if (modes.count(mode)) { | ||
| // Mode is supported (no changes needed - .count() still works!) | ||
| } | ||
| } | ||
| }; | ||
| ``` | ||
|
|
||
| ## Lifetime Safety for Effect Names | ||
|
|
||
| All `const char*` pointers must point to memory that lives for the effect's lifetime: | ||
|
|
||
| **Safe patterns:** | ||
| ```cpp | ||
| // 1. String literals (preferred) - stored in flash | ||
| auto effect = new MyEffect("Rainbow"); | ||
|
|
||
| // 2. Static constants | ||
| static const char *const EFFECT_NAME = "Rainbow"; | ||
| auto effect = new MyEffect(EFFECT_NAME); | ||
|
|
||
| // 3. C arrays | ||
| static constexpr const char *const EFFECT_NAMES[] = {"Rainbow", "Pulse"}; | ||
| auto effect = new MyEffect(EFFECT_NAMES[0]); | ||
| ``` | ||
|
|
||
| **Unsafe patterns (DO NOT USE):** | ||
| ```cpp | ||
| // WRONG - temporary string | ||
| std::string temp = "Rainbow"; | ||
| auto effect = new MyEffect(temp.c_str()); // Dangling pointer! | ||
|
|
||
| // WRONG - local array | ||
| const char name[] = "Rainbow"; | ||
| auto effect = new MyEffect(name); // Array destroyed! | ||
| ``` | ||
|
|
||
| ## ColorModeMask Details | ||
|
|
||
| ### Performance Benefits | ||
|
|
||
| `ColorModeMask` uses a 2-byte (uint16_t) bitmask instead of a red-black tree: | ||
|
|
||
| - **Memory**: 2 bytes vs ~80 bytes base overhead + per-member overhead | ||
| - **Lookup speed**: O(1) single bitwise AND operation vs O(log n) tree traversal | ||
| - **Cache efficiency**: Fits in CPU cache vs pointer chasing through tree nodes | ||
|
|
||
| For typical light use cases (2-4 color modes, frequent lookups), this provides significant performance and memory improvements. | ||
|
|
||
| ### Iterator Support | ||
|
|
||
| `ColorModeMask` provides iterator support for compatibility with API encoding: | ||
|
|
||
| ```cpp | ||
| ColorModeMask modes({ColorMode::RGB, ColorMode::WHITE}); | ||
|
|
||
| // Range-based for loop | ||
| for (auto mode : modes) { | ||
| ESP_LOGD("light", "Supported mode: %d", static_cast<int>(mode)); | ||
| } | ||
|
|
||
| // Manual iteration | ||
| for (auto it = modes.begin(); it != modes.end(); ++it) { | ||
| ColorMode mode = *it; | ||
| } | ||
| ``` | ||
|
|
||
| ### Size and Emptiness Checks | ||
|
|
||
| ```cpp | ||
| ColorModeMask modes({ColorMode::RGB, ColorMode::WHITE}); | ||
|
|
||
| size_t count = modes.size(); // Returns 2 | ||
| bool is_empty = modes.empty(); // Returns false | ||
| ``` | ||
|
|
||
| ### Raw Bitmask Access | ||
|
|
||
| For advanced use cases (e.g., API encoding): | ||
|
|
||
| ```cpp | ||
| ColorModeMask modes({ColorMode::RGB, ColorMode::WHITE}); | ||
| uint16_t raw_mask = modes.get_mask(); // Get raw bitmask value | ||
| ``` | ||
|
|
||
| ## Timeline | ||
|
|
||
| - **ESPHome 2025.11.0 (November 2025):** | ||
| - Effect name changes are active (breaking for custom effects) | ||
| - Color mode changes are active (backward compatible - no changes required) | ||
|
|
||
| ## Finding Code That Needs Updates | ||
|
|
||
| Search your external component code for these patterns: | ||
|
|
||
| ```bash | ||
| # Find std::string usage for effect names | ||
| grep -r 'LightEffect.*std::string' --include='*.cpp' --include='*.h' | ||
|
|
||
| # Find std::set usage for color modes | ||
| grep -r 'std::set<ColorMode>' --include='*.cpp' --include='*.h' | ||
|
|
||
| # Find .count() method calls on color modes | ||
| grep -r 'get_supported_color_modes.*count' --include='*.cpp' --include='*.h' | ||
|
|
||
| # Find set_supported_color_modes calls | ||
| grep -r 'set_supported_color_modes' --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 | ||
|
|
||
| - [Light Component Documentation](https://esphome.io/components/light/index.html) | ||
| - [PR #11487: Store Effect Names in Flash](https://github.com/esphome/esphome/pull/11487) | ||
| - [PR #11348: Use Bitmask for Color Modes](https://github.com/esphome/esphome/pull/11348) | ||
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 declaration uses both 'static constexpr' and 'const' keywords redundantly. For a constexpr array of string literals, 'static constexpr const char* const EFFECT_NAMES[]' is correct, but the outer 'const' after the asterisk is unnecessary since 'constexpr' already implies 'const'. Consider simplifying to 'static constexpr const char* EFFECT_NAMES[]'.