ESPHome 2026.8.0
Loading...
Searching...
No Matches
event_pool.h
Go to the documentation of this file.
1#pragma once
2
3#if defined(USE_ESP32) || defined(USE_ZEPHYR) || defined(USE_LIBRETINY) || defined(USE_RP2) || defined(USE_HOST)
4
5#include <atomic>
6#include <cstddef>
9
10namespace esphome {
11
12// Event Pool - On-demand pool of objects to avoid heap fragmentation
13// Events are allocated on first use and reused thereafter, growing to peak
14// usage; warm() pre-creates every entry up front for malloc-free producers
15// @tparam T The type of objects managed by the pool (must have a release() method)
16// @tparam SIZE The maximum number of objects in the pool (1-254, limited by uint8_t and the +1 free-list slot)
17//
18// SIZING: When paired with a LockFreeQueue<T, Q_SIZE>, the pool SIZE should be
19// Q_SIZE - 1 (the queue's actual capacity, since the ring buffer reserves one slot).
20// This ensures allocate() returns nullptr before push() can fail, which:
21// - Prevents the allocate-succeeds-but-push-fails mismatch that permanently
22// leaks a pool slot (the element is never returned to the pool)
23// - Avoids needing release() on the producer path after a failed push(),
24// preserving the SPSC contract on the internal free list
25template<class T, uint8_t SIZE> class EventPool {
26 // The free list ring must hold all SIZE objects at once (a fully drained
27 // pool), and LockFreeQueue reserves one slot — so it is sized SIZE + 1,
28 // which caps SIZE at 254.
29 static_assert(SIZE < 255, "EventPool SIZE must be at most 254");
30
31 public:
32 EventPool() : total_created_(0) {}
33
35 // Clean up any remaining events in the free list
36 // IMPORTANT: This destructor assumes no concurrent access. The EventPool must not
37 // be destroyed while any thread might still call allocate() or release().
38 // In practice, this is typically ensured by destroying the pool only during
39 // component shutdown when all producer/consumer threads have been stopped.
40 T *event;
42 while ((event = this->free_list_.pop()) != nullptr) {
43 // Call destructor
44 event->~T();
45 // Deallocate using RAMAllocator
46 allocator.deallocate(event, 1);
47 }
48 }
49
50 // Allocate an event from the pool
51 // Returns nullptr if pool is full
52 T *allocate() {
53 // Try to get from free list first
54 T *event = this->free_list_.pop();
55 if (event != nullptr)
56 return event;
57 // Need to create a new event
58 return this->create_();
59 }
60
61 // Return an event to the pool for reuse
62 void release(T *event) {
63 if (event != nullptr) {
64 // Clean up the event's allocated memory
65 event->release();
66 this->free_list_.push(event);
67 }
68 }
69
70 // Pre-create every pool entry so allocate() is always a free-list pop
71 // (for producers that must never malloc, e.g. IRQ-context handlers).
72 // Call from setup(); on false the heap could not supply every entry and
73 // the caller should mark_failed() — an incomplete warm puts malloc()
74 // back on the producer path. Tops the pool up from any quiescent state
75 // (entries that already exist are counted, not re-created); must not run
76 // concurrently with allocate()/release().
77 bool warm() {
78 // NOLINTNEXTLINE(clang-analyzer-unix.Malloc) -- ownership transfers to the free list
79 while (this->total_created_ < SIZE) {
80 T *event = this->create_();
81 if (event == nullptr)
82 return false;
83 this->free_list_.push(event);
84 }
85 return true;
86 }
87
88 private:
89 // Create and count one new object (shared by allocate() and warm()).
90 // Returns nullptr at capacity or when the heap is exhausted.
91 T *create_() {
92 if (this->total_created_ >= SIZE) {
93 // Pool is at capacity
94 return nullptr;
95 }
96 // Use internal RAM for better performance
97 RAMAllocator<T> allocator(RAMAllocator<T>::ALLOC_INTERNAL);
98 T *event = allocator.allocate(1);
99 if (event == nullptr) {
100 // Memory allocation failed
101 return nullptr;
102 }
103 // Placement new to construct the object
104 new (event) T();
105 this->total_created_++;
106 return event;
107 }
108
109 // SIZE + 1 slots so all SIZE objects fit when the pool is fully drained
110 // (the ring reserves one slot); otherwise the last release() of a
111 // completely returned pool would drop, permanently orphaning one object.
112 LockFreeQueue<T, static_cast<uint8_t>(SIZE + 1)> free_list_; // Free events ready for reuse
113 uint8_t total_created_; // Total events created (high water mark, max 254)
114};
115
116} // namespace esphome
117
118#endif // defined(USE_ESP32) || defined(USE_ZEPHYR) || defined(USE_LIBRETINY) || defined(USE_RP2) || defined(USE_HOST)
void release(T *event)
Definition event_pool.h:62
bool push(T *element)
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2099
void deallocate(T *p, size_t n)
Definition helpers.h:2156