Refactor the constant-size varint pattern into a reusable helper
method on the TypeInfo base class, used by both UInt32Type (max_value)
and FixedArrayBytesType (fixed_array_size < 128).
Add a max_value field option to api_options.proto that tells the code
generator the maximum value a field can have. When max_value < 128,
the generated calculate_size() uses constant arithmetic instead of
calling varint size functions, and encode() uses direct byte writes
instead of varint encoding.
Also optimize FixedArrayBytesType: when fixed_array_size < 128, the
length varint is always 1 byte, so calculate_size() uses constant
arithmetic and encode() uses write_raw_byte for the length.
Applied to BluetoothLERawAdvertisement.address_type (max_value=4).
Measured on ESP32 (upstairsdesk89proxy):
- BluetoothLERawAdvertisement::calculate_size: 88 → 71 bytes (-19%)
- BluetoothLERawAdvertisement::encode: 199 → 179 bytes (-10%)
- Total BLE proxy hot path: 1807 → 1770 bytes (-37 bytes)
Replace empty() + pop() with pop() directly as the fast-path check:
- LockFreeQueue: pop() costs 1 memw (acquire on tail_) vs empty()'s
2 memw (acquire on both head_ and tail_) on Xtensa
- FreeRTOSQueue: pop() is 1 critical section vs empty() + pop() = 2
Also move dropped count check after the drain loop since drops can
only occur when the queue was full, and only this loop drains it.
On Xtensa (dual-core ESP32), even relaxed atomic loads emit a memw
barrier instruction. The dropped_count_ check in get_and_reset_dropped_count()
was executing every loop() iteration even when the queue was empty.
Since drops only occur when the queue is full, and only the main loop
drains the queue, an empty queue guarantees no new drops since the last
reset. Restructure the loop to early-return when pop() returns nullptr,
skipping the unnecessary memw.
Also moves advertising_->loop() before the queue drain so it runs
unconditionally without duplication, and converts while to do/while
since the first element is known non-null after the nullptr check.
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.