The ESP_LOGD calls in SelectCall::perform() and Switch::turn_on/turn_off/toggle
are redundant with aioesphomeapi's state_log_formatter which already logs both
commands and state changes on the HA side. Benchmarks show these ESP_LOGD calls
dominate the control path cost (~13M ops/s for SelectCall vs ~180M ops/s for
publish_state).
This aligns select and switch with climate and fan which already use ESP_LOGV
for their call perform() paths.
Clamp the __builtin_ctz result to MAX_BITS to match the original loop
semantics if out-of-range bits are ever present. Add ctzll branch for
hypothetical >32-bit bitmask types.
Pass the already-fetched LightTraits from validate_() to
compute_color_mode_() and transform_parameters_() instead of each
calling get_traits() independently. This eliminates 2 redundant
virtual calls through output_->get_traits().
As a side effect, the compiler inlines both functions into validate_(),
eliminating their call overhead. Total hot path shrinks by 451 bytes.
Replace 3 software float divides (__divsf3) with 1 divide + 3 native
mul.s instructions. Xtensa has no FPU divide, so __divsf3 is a ~20-30
cycle software routine. This saves ~40-60 cycles per normalize_color
call for +8 bytes of flash.
Replace the linear bit-scanning loop in find_next_set_bit with
__builtin_ctz (compiles to single-cycle NSAU on Xtensa). Also rename
to find_lowest_set_bit and drop the unused start_bit parameter since
all call sites pass 0.
This eliminates the standalone find_next_set_bit function and replaces
3 function calls + loops in compute_color_mode_ with inline NSAU
instructions.
Two optimizations to reduce overhead in the scheduler hot path:
1. Skip cancel_item_locked_ entirely for anonymous items (STATIC_STRING
with nullptr name) in set_timer_common_. These can never match any
existing item, so the scan is pure waste. This is the common path
for Component::defer(func).
2. Split mark_matching_items_removed_locked_ into an inline wrapper
that checks for empty containers and a noinline slow path. This
avoids the function call overhead when containers are empty, which
is the common case for defer_queue_ and to_add_ after draining.
UniqueID benchmark was unrealistic — unique IDs still ran
cancel_item_locked_ scanning all containers for matches that never
exist. Replace with just two meaningful defer variants:
- Defer: anonymous (nullptr name, skips cancel entirely)
- Defer_SameID: fixed ID (cancel-and-replace coalescing pattern)
- Scheduler_Defer: use nullptr name matching Component::defer(func)
production pattern (skips cancel_item_locked_ entirely)
- Scheduler_Defer_SameID: fixed ID 0 measuring cancel-and-replace
pattern for coalescing rapid updates
- Scheduler_Defer_UniqueID: unique IDs measuring cancel scan overhead
when no match is found
Change kInnerIterations from 2000 to 2100 (divisible by batch sizes 3
and 10) to prevent pool imbalance at iteration boundaries that caused
spurious malloc. Add static_assert to each benchmark to catch this at
compile time.
Replace duplicated pool warmup blocks with a shared warm_pool() helper
that registers and replaces items twice to populate the recycling pool
before the benchmark loop begins.
Add Scheduler_SetTimeout_ExceedPool with batch size 10 (exceeding
MAX_POOL_SIZE=5) to measure the performance impact when the recycling
pool is exhausted and items must be malloc'd/freed each cycle.
Instead of draining after every single registration or batching 5,
use a batch size of 3 which represents a realistic worst case where
multiple components schedule in the same loop iteration while staying
within the recycling pool (MAX_POOL_SIZE=5).
Scheduler registration benchmarks (SetTimeout, SetInterval, Defer) were
not calling scheduler.call() periodically to drain and clean up cancelled
items. In production, call() runs every loop iteration, keeping the
scheduler containers small. Without draining, cancelled items accumulated
causing O(n²) scan cost in cancel_item_locked_ that doesn't reflect
real-world behavior.
- SetTimeout: was only calling process_to_add() (no cleanup), now calls
call() every kKeyCount iterations
- SetInterval: was calling process_to_add() (no cleanup of items_), now
calls call() for proper cleanup
- Defer: was never draining the defer queue, now calls call() to process
deferred items as production does
- All three now advance time (++now) to match production loop behavior
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@koston.org>