[zwave_proxy] Inline loop() hot-path fast-paths for response_handler_ and process_uart_

Split response_handler_ and process_uart_ into tiny inline wrappers in the
header that short-circuit the common case, plus _slow_ bodies in the .cpp.

- response_handler_: most loop() ticks have parsing_state_ outside the three
  SEND_* states; inline the range check and skip the call8 entirely.
- process_uart_: most ticks have no UART bytes pending; inline an available()
  check and skip the out-of-line call + its stack frame. Inside the slow
  path, switch the while() to do/while() since the caller has already
  confirmed available() > 0.

ESPHOME_ALWAYS_INLINE is required — with -Os gcc otherwise clones the
wrapper into a shared \$isra\$ outline and keeps the call8.

Measured on ESP32-S3 zwave-proxy-seeedw5500 build: idle-tick out-of-line
calls in ZWaveProxy::loop() drop from 4 (response_handler_, api_is_connected,
virtual parent_->is_connected, process_uart_ which nested available()) to 3
(api_is_connected, virtual is_connected, available).
This commit is contained in:
J. Nick Koston
2026-04-21 11:09:57 +02:00
parent a8bd035b62
commit f74c3c0489
2 changed files with 28 additions and 6 deletions
@@ -101,8 +101,10 @@ void ZWaveProxy::loop() {
this->status_clear_warning();
}
void ZWaveProxy::process_uart_() {
while (this->available()) {
void ZWaveProxy::process_uart_slow_() {
// Caller (inline process_uart_) has already confirmed available() > 0, so use do/while to
// drain bytes — available() is still checked at the tail, but not redundantly on entry.
do {
uint8_t byte;
if (!this->read_byte(&byte)) {
this->status_set_warning(LOG_STR("UART read failed"));
@@ -137,7 +139,7 @@ void ZWaveProxy::process_uart_() {
this->api_connection_->send_message(this->outgoing_proto_msg_);
}
}
}
} while (this->available());
}
void ZWaveProxy::dump_config() {
@@ -414,7 +416,7 @@ void ZWaveProxy::parse_start_(uint8_t byte) {
}
}
bool ZWaveProxy::response_handler_() {
bool ZWaveProxy::response_handler_slow_() {
switch (this->parsing_state_) {
case ZWAVE_PARSING_STATE_SEND_ACK:
this->last_response_ = ZWAVE_FRAME_TYPE_ACK;
+22 -2
View File
@@ -72,8 +72,28 @@ class ZWaveProxy : public uart::UARTDevice, public Component {
void send_simple_command_(uint8_t command_id);
bool parse_byte_(uint8_t byte); // Returns true if frame parsing was completed (a frame is ready in the buffer)
void parse_start_(uint8_t byte);
bool response_handler_();
void process_uart_(); // Process all available UART data
// Inline fast-path: most calls happen with parsing_state_ outside the SEND_* range, so skip the
// out-of-line call entirely in the hot path (e.g. every loop() tick) and only pay for the real
// work when a response is actually pending. ESPHOME_ALWAYS_INLINE is required because with -Os
// gcc otherwise clones the wrapper into a shared $isra$ outline and keeps the call8.
ESPHOME_ALWAYS_INLINE bool response_handler_() {
if (this->parsing_state_ < ZWAVE_PARSING_STATE_SEND_ACK || this->parsing_state_ > ZWAVE_PARSING_STATE_SEND_NAK) {
return false;
}
return this->response_handler_slow_();
}
bool response_handler_slow_();
// Inline fast-path: UART::available() is cheap (ring-buffer head/tail compare on most backends).
// On an idle loop tick we want to skip the call to process_uart_ entirely. When bytes are
// pending we fall into the slow path, which drains the UART with a do/while so available() is
// only checked once per byte — no redundant re-check on entry.
ESPHOME_ALWAYS_INLINE void process_uart_() {
if (!this->available()) {
return;
}
this->process_uart_slow_();
}
void process_uart_slow_(); // Drain all available UART data
// Pre-allocated message - always ready to send
api::ZWaveProxyFrame outgoing_proto_msg_;