mirror of
https://github.com/esphome/esphome.git
synced 2026-06-02 11:08:06 +08:00
[api] Replace std::vector<uint8_t> with APIBuffer to skip zero-fill (#14593)
This commit is contained in:
@@ -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
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#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, LibreTiny).
|
||||||
|
inline std::unique_ptr<uint8_t[]> make_buffer(size_t n) {
|
||||||
|
#if defined(USE_ESP8266) || defined(USE_LIBRETINY)
|
||||||
|
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.
|
||||||
|
///
|
||||||
|
/// Designed for bulk clear/resize/overwrite patterns. grow_() allocates
|
||||||
|
/// exactly the requested size (no growth factor) since callers resize to
|
||||||
|
/// known sizes rather than appending incrementally.
|
||||||
|
///
|
||||||
|
/// Safe because: callers always write exactly the number of bytes they
|
||||||
|
/// resize for. In the protobuf write path, 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
|
||||||
@@ -2016,7 +2016,7 @@ uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncode
|
|||||||
if (total_calculated_size > remaining_size)
|
if (total_calculated_size > remaining_size)
|
||||||
return 0; // Doesn't fit
|
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) {
|
if (conn->flags_.batch_first_message) {
|
||||||
// First message - buffer already prepared by caller, just clear flag
|
// First message - buffer already prepared by caller, just clear flag
|
||||||
@@ -2184,7 +2184,7 @@ void APIConnection::process_batch_() {
|
|||||||
|
|
||||||
// Separated from process_batch_() so the single-message fast path gets a minimal
|
// Separated from process_batch_() so the single-message fast path gets a minimal
|
||||||
// stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array.
|
// 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) {
|
uint8_t footer_size) {
|
||||||
// Ensure MessageInfo remains trivially destructible for our placement new approach
|
// Ensure MessageInfo remains trivially destructible for our placement new approach
|
||||||
static_assert(std::is_trivially_destructible<MessageInfo>::value,
|
static_assert(std::is_trivially_destructible<MessageInfo>::value,
|
||||||
|
|||||||
@@ -288,7 +288,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();
|
shared_buf.clear();
|
||||||
// Reserve space for header padding + message + footer
|
// Reserve space for header padding + message + footer
|
||||||
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
|
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
|
||||||
@@ -299,7 +299,7 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convenience overload - computes frame overhead internally
|
// 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 header_padding = this->helper_->frame_header_padding();
|
||||||
const uint8_t footer_size = this->helper_->frame_footer_size();
|
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);
|
this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size);
|
||||||
@@ -687,8 +687,8 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
|
|
||||||
bool schedule_batch_();
|
bool schedule_batch_();
|
||||||
void process_batch_();
|
void process_batch_();
|
||||||
void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
|
void process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding, uint8_t footer_size)
|
||||||
uint8_t footer_size) __attribute__((noinline));
|
__attribute__((noinline));
|
||||||
void clear_batch_() {
|
void clear_batch_() {
|
||||||
this->deferred_batch_.clear();
|
this->deferred_batch_.clear();
|
||||||
this->flags_.batch_scheduled = false;
|
this->flags_.batch_scheduled = false;
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <span>
|
#include <span>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
#ifdef USE_API
|
#ifdef USE_API
|
||||||
|
#include "esphome/components/api/api_buffer.h"
|
||||||
#include "esphome/components/socket/socket.h"
|
#include "esphome/components/socket/socket.h"
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
@@ -178,8 +178,7 @@ class APIFrameHelper {
|
|||||||
// rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame
|
// rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame
|
||||||
// and clearing would lose partially received data.
|
// and clearing would lose partially received data.
|
||||||
if (this->rx_buf_len_ == 0) {
|
if (this->rx_buf_len_ == 0) {
|
||||||
// Use swap trick since shrink_to_fit() is non-binding and may be ignored
|
this->rx_buf_.release();
|
||||||
std::vector<uint8_t>().swap(this->rx_buf_);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,9 +205,6 @@ class APIFrameHelper {
|
|||||||
|
|
||||||
// Common socket write error handling
|
// Common socket write error handling
|
||||||
APIError handle_socket_write_error_();
|
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)
|
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
|
||||||
std::unique_ptr<socket::Socket> socket_;
|
std::unique_ptr<socket::Socket> socket_;
|
||||||
@@ -245,7 +241,7 @@ class APIFrameHelper {
|
|||||||
|
|
||||||
// Containers (size varies, but typically 12+ bytes on 32-bit)
|
// Containers (size varies, but typically 12+ bytes on 32-bit)
|
||||||
std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
|
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
|
// Client name buffer - stores name from Hello message or initial peername
|
||||||
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
|
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
|
||||||
|
|||||||
@@ -207,9 +207,7 @@ APIError APINoiseFrameHelper::try_read_frame_() {
|
|||||||
// During handshake, rx_buf_.size() is used in prologue construction, so
|
// During handshake, rx_buf_.size() is used in prologue construction, so
|
||||||
// the buffer must be exactly msg_size to avoid prologue mismatch.)
|
// the buffer must be exactly msg_size to avoid prologue mismatch.)
|
||||||
uint16_t alloc_size = msg_size + (is_data ? RX_BUF_NULL_TERMINATOR : 0);
|
uint16_t alloc_size = msg_size + (is_data ? RX_BUF_NULL_TERMINATOR : 0);
|
||||||
if (this->rx_buf_.size() != alloc_size) {
|
this->rx_buf_.resize(alloc_size);
|
||||||
this->rx_buf_.resize(alloc_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rx_buf_len_ < msg_size) {
|
if (rx_buf_len_ < msg_size) {
|
||||||
// more data to read
|
// more data to read
|
||||||
@@ -571,8 +569,7 @@ APIError APINoiseFrameHelper::init_handshake_() {
|
|||||||
if (aerr != APIError::OK)
|
if (aerr != APIError::OK)
|
||||||
return aerr;
|
return aerr;
|
||||||
// set_prologue copies it into handshakestate, so we can get rid of it now
|
// 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)
|
prologue_.release();
|
||||||
std::vector<uint8_t>().swap(prologue_);
|
|
||||||
|
|
||||||
err = noise_handshakestate_start(handshake_);
|
err = noise_handshakestate_start(handshake_);
|
||||||
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED);
|
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)
|
// Reference to noise context (4 bytes on 32-bit)
|
||||||
APINoiseContext &ctx_;
|
APINoiseContext &ctx_;
|
||||||
|
|
||||||
// Vector (12 bytes on 32-bit)
|
// Buffer for noise handshake prologue (released after handshake)
|
||||||
std::vector<uint8_t> prologue_;
|
APIBuffer prologue_;
|
||||||
|
|
||||||
// NoiseProtocolId (size depends on implementation)
|
// NoiseProtocolId (size depends on implementation)
|
||||||
NoiseProtocolId nid_;
|
NoiseProtocolId nid_;
|
||||||
|
|||||||
@@ -165,9 +165,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
|
|||||||
|
|
||||||
// Reserve space for body (+ null terminator so protobuf StringRef fields
|
// Reserve space for body (+ null terminator so protobuf StringRef fields
|
||||||
// can be safely null-terminated in-place after decode)
|
// can be safely null-terminated in-place after decode)
|
||||||
if (this->rx_buf_.size() != this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR) {
|
this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR);
|
||||||
this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rx_buf_len_ < rx_header_parsed_len_) {
|
if (rx_buf_len_ < rx_header_parsed_len_) {
|
||||||
// more data to read
|
// more data to read
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
#ifdef USE_API
|
#ifdef USE_API
|
||||||
|
#include "api_buffer.h"
|
||||||
#include "api_noise_context.h"
|
#include "api_noise_context.h"
|
||||||
#include "api_pb2.h"
|
#include "api_pb2.h"
|
||||||
#include "api_pb2_service.h"
|
#include "api_pb2_service.h"
|
||||||
@@ -65,7 +66,7 @@ class APIServer : public Component,
|
|||||||
void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; }
|
void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; }
|
||||||
|
|
||||||
// Get reference to shared buffer for API 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
|
#ifdef USE_API_NOISE
|
||||||
bool save_noise_psk(psk_t psk, bool make_active = true);
|
bool save_noise_psk(psk_t psk, bool make_active = true);
|
||||||
@@ -276,7 +277,7 @@ class APIServer : public Component,
|
|||||||
// Not pre-allocated: all send paths call prepare_first_message_buffer() which
|
// Not pre-allocated: all send paths call prepare_first_message_buffer() which
|
||||||
// reserves the exact needed size. Pre-allocating here would cause heap fragmentation
|
// reserves the exact needed size. Pre-allocating here would cause heap fragmentation
|
||||||
// since the buffer would almost always reallocate on first use.
|
// 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
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
std::vector<HomeAssistantStateSubscription> state_subs_;
|
std::vector<HomeAssistantStateSubscription> state_subs_;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "api_pb2_defines.h"
|
#include "api_pb2_defines.h"
|
||||||
|
#include "api_buffer.h"
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
@@ -203,9 +204,8 @@ class Proto32Bit {
|
|||||||
|
|
||||||
class ProtoWriteBuffer {
|
class ProtoWriteBuffer {
|
||||||
public:
|
public:
|
||||||
ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {}
|
ProtoWriteBuffer(APIBuffer *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {}
|
||||||
ProtoWriteBuffer(std::vector<uint8_t> *buffer, size_t write_pos)
|
ProtoWriteBuffer(APIBuffer *buffer, size_t write_pos) : buffer_(buffer), pos_(buffer->data() + write_pos) {}
|
||||||
: buffer_(buffer), pos_(buffer->data() + write_pos) {}
|
|
||||||
inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint32_t value) {
|
inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint32_t value) {
|
||||||
if (value < 128) [[likely]] {
|
if (value < 128) [[likely]] {
|
||||||
this->debug_check_bounds_(1);
|
this->debug_check_bounds_(1);
|
||||||
@@ -340,7 +340,7 @@ class ProtoWriteBuffer {
|
|||||||
// Non-template core for encode_optional_sub_message.
|
// 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_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value,
|
||||||
void (*encode_fn)(const void *, ProtoWriteBuffer &));
|
void (*encode_fn)(const void *, ProtoWriteBuffer &));
|
||||||
std::vector<uint8_t> *get_buffer() const { return buffer_; }
|
APIBuffer *get_buffer() const { return buffer_; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Slow path for encode_varint_raw values >= 128, outlined to keep fast path small
|
// Slow path for encode_varint_raw values >= 128, outlined to keep fast path small
|
||||||
@@ -353,7 +353,7 @@ class ProtoWriteBuffer {
|
|||||||
void debug_check_bounds_([[maybe_unused]] size_t bytes) {}
|
void debug_check_bounds_([[maybe_unused]] size_t bytes) {}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
std::vector<uint8_t> *buffer_;
|
APIBuffer *buffer_;
|
||||||
uint8_t *pos_;
|
uint8_t *pos_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<<: !include common.yaml
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
ssid: MySSID
|
||||||
|
password: password1
|
||||||
Reference in New Issue
Block a user