Add a comment to EventPool documenting that when paired with a
LockFreeQueue<T, N>, the pool should be sized to N-1 (the queue's
actual capacity) to prevent slot leaks and SPSC violations.
LockFreeQueue<T,N> is a ring buffer that holds N-1 elements (one slot
is reserved to distinguish full from empty). With the pool also sized
to N, the Nth allocate() succeeds but push() fails — permanently
leaking one pool slot since the element is never returned.
Size both receive and send pools to N-1 to match queue capacity.
LockFreeQueue<T,N> is a ring buffer that holds N-1 elements (one slot
is reserved to distinguish full from empty). With the pool also sized
to N, the Nth allocate() succeeds but push() fails — permanently
leaking one pool slot since the element is never returned.
Size the pool to N-1 to match queue capacity. This guarantees
allocate() returns nullptr before push() can fail.
The ESP-IDF MQTT client dispatches events from its own task, which
pushed to a std::queue while the main loop popped from it. std::queue
is not thread-safe; concurrent access can corrupt its internal state.
Replace with EventPool + LockFreeQueue (SPSC ring buffer) already
used elsewhere in the codebase. The pool is sized to queue capacity
(SIZE-1) so allocate() fails before push() can, which prevents both
a slot leak and an SPSC violation on the pool's free list.
Also rename the outbound pool from mqtt_event_pool_ to
mqtt_outbound_pool_ to avoid confusion with the new inbound pool.
On ESPHOME_THREAD_SINGLE there are no concurrent writers, so
to_add_empty_() checks to_add_.empty() directly. The counter
field and its increment/clear operations are compiled out.
cleanup_() was computing items_.size() on every call even on the
fast path where nothing was removed. All callers only check if
items remain (== 0), so return bool and use items_.empty() instead.
The mutex already provides all necessary memory ordering for the
counter operations. acquire/release fences on the fast-path reads
were causing unnecessary cache flushes, regressing
Scheduler_NextScheduleIn by ~10%.
The compiler constant-folds ProtoSize::varint(42) to 1 and optimizes
the entire inner loop to a single addition, causing ~62ns jitter-dominated
measurements. Use varying inputs (i & 0x7F for small, 0xFFFF0000 | i for
large) so each call computes a real result.
- Use BENCHMARK_BINARY= marker for reliable binary path extraction
instead of fragile tail -1 (PlatformIO can print warnings after path)
- Fix Scheduler_Defer: defer() is protected on Component, use
set_timeout(delay=0) directly on Scheduler instead
Measures the cost of registering scheduler items:
- Scheduler_SetTimeout: set_timeout with 1s delay (heap path)
- Scheduler_SetInterval: set_interval with 1s period (heap path + offset calc)
- Scheduler_Defer: Component::defer (set_timeout with delay=0)
Uses i%5 for IDs to exercise the cancel-existing-timer path that
happens when re-registering with the same ID.
Add fast-path checks in process_to_add(), cleanup_(), and
process_defer_queue_() to skip mutex acquisition when there is
nothing to process.
Uses std::atomic<uint32_t> counters on platforms with atomics support
(ESP32, host), falls back to always taking the lock on platforms
without atomics (LibreTiny). Single-threaded platforms (ESP8266,
RP2040, nRF52) check directly since there are no concurrent writers.
Also migrates existing to_remove_ to the same atomic pattern and
moves the defer_queue_.size() snapshot under the lock — both were
previously plain reads without the lock while being modified
cross-thread.
Add USE_BENCHMARK define to benchmark build flags. Guard the
warn_blocking call in finish() with #ifndef USE_BENCHMARK so
scheduler benchmarks using fake monotonic time don't trigger
the underflow (fake now > real millis()).
Remove BenchComponent — no longer needed with the ifdef.
The warn_blocking underflow only happens with fake time in benchmarks,
not in production (millis() is monotonic). Accept the consistent
overhead from one warning per call — CodSpeed regression detection
works on relative changes, not absolute values.
When millis() < started_ (e.g. scheduler passes a now value slightly
ahead of real millis()), the uint32_t subtraction in finish() wraps to
~4 billion. This caused warn_blocking to fire on every call since the
underflowed value always exceeds the uint16_t threshold max (65535).
Fix by clamping blocking_time to uint16_t max in the cold warn_blocking
path. After one warning, should_warn_of_blocking() saturates the
threshold to 65535 and subsequent clamped values (65535) don't exceed it.
Zero cost on the hot path — the clamp is in the noinline cold function.
When curr_time (from millis()) is less than started_ (the `now` passed
to scheduler.call()), the subtraction wraps to a huge value (~4 billion).
This triggers spurious blocking warnings with nonsensical times.
This can happen when the scheduler's execute_item_() returns a millis()
value that subsequent items use as their guard start, but the next
scheduler.call() passes a `now` value from a slightly different source.
Fix by skipping the blocking check when curr_time < started_ (underflow).
Also restore the scheduler firing benchmark to use intervals with
monotonically increasing fake time, now that the guard handles underflow.
- Revert component.h change (no core changes for benchmarks)
- Remove -DWARN_IF_BLOCKING_OVER_MS from build flags (can't shadow constexpr)
- Drop inner loop — 5 heap pops + callbacks + pushes per call is well
above CodSpeed's 60ns instrumentation overhead
- Add warm-up call before benchmark loop to trigger the blocking guard
once and ramp the threshold
- interval=0 causes infinite loop, must use interval=1 with fake time
interval=0 causes infinite loop (reschedules at same time, never breaks).
interval=1 with millis() doesn't work (real time doesn't advance fast
enough between inner iterations for intervals to re-fire).
Solution: use interval=1 with monotonically increasing fake time (now++)
and disable WarnIfComponentBlockingGuard at compile time via
-DWARN_IF_BLOCKING_OVER_MS=UINT32_MAX in benchmark build flags. This
prevents the guard's (millis() - started_) underflow when fake time
exceeds real millis().
Use interval=0 so all 5 intervals fire unconditionally every call().
Pass real millis() to scheduler.call() so WarnIfComponentBlockingGuard
doesn't see fake time ahead of wall clock (which causes uint32_t
underflow in the blocking time calculation).
WarnIfComponentBlockingGuard compares the `now` passed to
scheduler.call() against real millis() in finish(). Using fake time
ahead of real millis() caused uint32_t underflow in the guard, triggering
blocking warnings. Fix by reading real millis() at the start of each
outer iteration so fake time stays close to wall clock.