[api] Replace std::vector<uint8_t> with APIBuffer to skip zero-fill

Replace std::vector<uint8_t> with a minimal APIBuffer class for the
shared protobuf write buffer, frame helper receive buffer (rx_buf_),
and noise handshake prologue buffer.

std::vector::resize() zero-fills new bytes via memset. Every byte is
immediately overwritten by the protobuf encoder or socket reads,
making the zero-fill pure waste. APIBuffer skips zero-initialization
on resize() and uses make_unique_for_overwrite where available.

Also removes a dead write_raw_ template overload from api_frame_helper.h.
This commit is contained in:
J. Nick Koston
2026-03-07 19:45:50 -10:00
parent 5e842a8b20
commit f7aeaad929
10 changed files with 99 additions and 23 deletions
+13
View File
@@ -0,0 +1,13 @@
#include "api_buffer.h"
namespace esphome::api {
void APIBuffer::grow_(size_t n) {
auto new_data = make_buffer(n);
if (this->size_)
std::memcpy(new_data.get(), this->data_.get(), this->size_);
this->data_ = std::move(new_data);
this->capacity_ = n;
}
} // namespace esphome::api
+62
View File
@@ -0,0 +1,62 @@
#pragma once
#include <cstdint>
#include <cstring>
#include <memory>
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
namespace esphome::api {
/// Helper to use make_unique_for_overwrite where available (skips zero-fill),
/// falling back to make_unique on older GCC (ESP8266, BK72xx, LN882x).
inline std::unique_ptr<uint8_t[]> make_buffer(size_t n) {
#if defined(USE_ESP8266) || defined(USE_BK72XX) || defined(USE_LN882X)
return std::make_unique<uint8_t[]>(n);
#else
return std::make_unique_for_overwrite<uint8_t[]>(n);
#endif
}
/// Byte buffer that skips zero-initialization on resize().
///
/// std::vector<uint8_t>::resize() zero-fills new bytes via memset. For the
/// shared protobuf write buffer, every byte is overwritten by the encoder,
/// making the zero-fill pure waste. For the receive buffer, bytes are
/// overwritten by socket reads.
///
/// Safe because: callers always write exactly the number of bytes they
/// resize for, and debug_check_bounds_ validates writes in debug builds.
class APIBuffer {
public:
void clear() { this->size_ = 0; }
inline void reserve(size_t n) ESPHOME_ALWAYS_INLINE {
if (n > this->capacity_)
this->grow_(n);
}
inline void resize(size_t n) ESPHOME_ALWAYS_INLINE {
this->reserve(n);
this->size_ = n; // no zero-fill
}
uint8_t *data() { return this->data_.get(); }
const uint8_t *data() const { return this->data_.get(); }
size_t size() const { return this->size_; }
bool empty() const { return this->size_ == 0; }
uint8_t &operator[](size_t i) { return this->data_[i]; }
const uint8_t &operator[](size_t i) const { return this->data_[i]; }
/// Release all memory (equivalent to std::vector swap trick).
void release() {
this->data_.reset();
this->size_ = 0;
this->capacity_ = 0;
}
protected:
void grow_(size_t n);
std::unique_ptr<uint8_t[]> data_;
size_t size_{0};
size_t capacity_{0};
};
} // namespace esphome::api
+2 -2
View File
@@ -1915,7 +1915,7 @@ uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncode
if (total_calculated_size > remaining_size)
return 0; // Doesn't fit
std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref();
auto &shared_buf = conn->parent_->get_shared_buffer_ref();
if (conn->flags_.batch_first_message) {
// First message - buffer already prepared by caller, just clear flag
@@ -2083,7 +2083,7 @@ void APIConnection::process_batch_() {
// Separated from process_batch_() so the single-message fast path gets a minimal
// stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array.
void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding,
uint8_t footer_size) {
// Ensure MessageInfo remains trivially destructible for our placement new approach
static_assert(std::is_trivially_destructible<MessageInfo>::value,
+4 -4
View File
@@ -278,7 +278,7 @@ class APIConnection final : public APIServerConnectionBase {
}
}
void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t header_padding, size_t total_size) {
void prepare_first_message_buffer(APIBuffer &shared_buf, size_t header_padding, size_t total_size) {
shared_buf.clear();
// Reserve space for header padding + message + footer
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
@@ -289,7 +289,7 @@ class APIConnection final : public APIServerConnectionBase {
}
// Convenience overload - computes frame overhead internally
void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t payload_size) {
void prepare_first_message_buffer(APIBuffer &shared_buf, size_t payload_size) {
const uint8_t header_padding = this->helper_->frame_header_padding();
const uint8_t footer_size = this->helper_->frame_footer_size();
this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size);
@@ -669,8 +669,8 @@ class APIConnection final : public APIServerConnectionBase {
bool schedule_batch_();
void process_batch_();
void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
uint8_t footer_size) __attribute__((noinline));
void process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding, uint8_t footer_size)
__attribute__((noinline));
void clear_batch_() {
this->deferred_batch_.clear();
this->flags_.batch_scheduled = false;
+3 -6
View File
@@ -5,10 +5,10 @@
#include <memory>
#include <span>
#include <utility>
#include <vector>
#include "esphome/core/defines.h"
#ifdef USE_API
#include "esphome/components/api/api_buffer.h"
#include "esphome/components/socket/socket.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
@@ -179,7 +179,7 @@ class APIFrameHelper {
// and clearing would lose partially received data.
if (this->rx_buf_len_ == 0) {
// Use swap trick since shrink_to_fit() is non-binding and may be ignored
std::vector<uint8_t>().swap(this->rx_buf_);
this->rx_buf_.release();
}
}
@@ -206,9 +206,6 @@ class APIFrameHelper {
// Common socket write error handling
APIError handle_socket_write_error_();
template<typename StateEnum>
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
const std::string &info, StateEnum &state, StateEnum failed_state);
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
std::unique_ptr<socket::Socket> socket_;
@@ -234,7 +231,7 @@ class APIFrameHelper {
// Containers (size varies, but typically 12+ bytes on 32-bit)
std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
std::vector<uint8_t> rx_buf_;
APIBuffer rx_buf_;
// Client name buffer - stores name from Hello message or initial peername
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
@@ -579,8 +579,7 @@ APIError APINoiseFrameHelper::init_handshake_() {
if (aerr != APIError::OK)
return aerr;
// set_prologue copies it into handshakestate, so we can get rid of it now
// Use swap idiom to actually release memory (= {} only clears size, not capacity)
std::vector<uint8_t>().swap(prologue_);
prologue_.release();
err = noise_handshakestate_start(handshake_);
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED);
@@ -43,8 +43,8 @@ class APINoiseFrameHelper final : public APIFrameHelper {
// Reference to noise context (4 bytes on 32-bit)
APINoiseContext &ctx_;
// Vector (12 bytes on 32-bit)
std::vector<uint8_t> prologue_;
// Buffer for noise handshake prologue (released after handshake)
APIBuffer prologue_;
// NoiseProtocolId (size depends on implementation)
NoiseProtocolId nid_;
+2 -2
View File
@@ -65,7 +65,7 @@ class APIServer : public Component,
void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; }
// Get reference to shared buffer for API connections
std::vector<uint8_t> &get_shared_buffer_ref() { return shared_write_buffer_; }
APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
#ifdef USE_API_NOISE
bool save_noise_psk(psk_t psk, bool make_active = true);
@@ -276,7 +276,7 @@ class APIServer : public Component,
// Not pre-allocated: all send paths call prepare_first_message_buffer() which
// reserves the exact needed size. Pre-allocating here would cause heap fragmentation
// since the buffer would almost always reallocate on first use.
std::vector<uint8_t> shared_write_buffer_;
APIBuffer shared_write_buffer_;
#ifdef USE_API_HOMEASSISTANT_STATES
std::vector<HomeAssistantStateSubscription> state_subs_;
#endif
+5 -5
View File
@@ -1,6 +1,7 @@
#pragma once
#include "api_pb2_defines.h"
#include "api_buffer.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -237,9 +238,8 @@ class Proto32Bit {
class ProtoWriteBuffer {
public:
ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {}
ProtoWriteBuffer(std::vector<uint8_t> *buffer, size_t write_pos)
: buffer_(buffer), pos_(buffer->data() + write_pos) {}
ProtoWriteBuffer(APIBuffer *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {}
ProtoWriteBuffer(APIBuffer *buffer, size_t write_pos) : buffer_(buffer), pos_(buffer->data() + write_pos) {}
void encode_varint_raw(uint32_t value) {
while (value > 0x7F) {
this->debug_check_bounds_(1);
@@ -375,7 +375,7 @@ class ProtoWriteBuffer {
// Non-template core for encode_optional_sub_message.
void encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value,
void (*encode_fn)(const void *, ProtoWriteBuffer &));
std::vector<uint8_t> *get_buffer() const { return buffer_; }
APIBuffer *get_buffer() const { return buffer_; }
protected:
#ifdef ESPHOME_DEBUG_API
@@ -385,7 +385,7 @@ class ProtoWriteBuffer {
void debug_check_bounds_([[maybe_unused]] size_t bytes) {}
#endif
std::vector<uint8_t> *buffer_;
APIBuffer *buffer_;
uint8_t *pos_;
};
@@ -0,0 +1,5 @@
<<: !include common.yaml
wifi:
ssid: MySSID
password: password1