[api] Reduce memory usage by eliminating duplicate client info strings (#9740)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
J. Nick Koston 2025-07-20 13:34:59 -10:00 committed by GitHub
parent ecd310dae1
commit 5b5982cfdd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 53 additions and 38 deletions

View File

@ -79,14 +79,16 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *pa
#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE) #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
auto noise_ctx = parent->get_noise_ctx(); auto noise_ctx = parent->get_noise_ctx();
if (noise_ctx->has_psk()) { if (noise_ctx->has_psk()) {
this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx)}; this->helper_ =
std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)};
} else { } else {
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))}; this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)};
} }
#elif defined(USE_API_PLAINTEXT) #elif defined(USE_API_PLAINTEXT)
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))}; this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)};
#elif defined(USE_API_NOISE) #elif defined(USE_API_NOISE)
this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; this->helper_ = std::unique_ptr<APIFrameHelper>{
new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx(), &this->client_info_)};
#else #else
#error "No frame helper defined" #error "No frame helper defined"
#endif #endif
@ -109,9 +111,8 @@ void APIConnection::start() {
errno); errno);
return; return;
} }
this->client_info_ = helper_->getpeername(); this->client_info_.peername = helper_->getpeername();
this->client_peername_ = this->client_info_; this->client_info_.name = this->client_info_.peername;
this->helper_->set_log_info(this->client_info_);
} }
APIConnection::~APIConnection() { APIConnection::~APIConnection() {
@ -1374,7 +1375,7 @@ void APIConnection::complete_authentication_() {
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED); this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str());
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER #ifdef USE_API_CLIENT_CONNECTED_TRIGGER
this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->client_info_.peername);
#endif #endif
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
if (homeassistant::global_homeassistant_time != nullptr) { if (homeassistant::global_homeassistant_time != nullptr) {
@ -1384,13 +1385,12 @@ void APIConnection::complete_authentication_() {
} }
HelloResponse APIConnection::hello(const HelloRequest &msg) { HelloResponse APIConnection::hello(const HelloRequest &msg) {
this->client_info_ = msg.client_info; this->client_info_.name = msg.client_info;
this->client_peername_ = this->helper_->getpeername(); this->client_info_.peername = this->helper_->getpeername();
this->helper_->set_log_info(this->get_client_combined_info());
this->client_api_version_major_ = msg.api_version_major; this->client_api_version_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor; this->client_api_version_minor_ = msg.api_version_minor;
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.c_str(), ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.name.c_str(),
this->client_peername_.c_str(), this->client_api_version_major_, this->client_api_version_minor_); this->client_info_.peername.c_str(), this->client_api_version_major_, this->client_api_version_minor_);
HelloResponse resp; HelloResponse resp;
resp.api_version_major = 1; resp.api_version_major = 1;

View File

@ -16,6 +16,20 @@
namespace esphome { namespace esphome {
namespace api { namespace api {
// Client information structure
struct ClientInfo {
std::string name; // Client name from Hello message
std::string peername; // IP:port from socket
std::string get_combined_info() const {
if (name == peername) {
// Before Hello message, both are the same
return name;
}
return name + " (" + peername + ")";
}
};
// Keepalive timeout in milliseconds // Keepalive timeout in milliseconds
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
// Maximum number of entities to process in a single batch during initial state/info sending // Maximum number of entities to process in a single batch during initial state/info sending
@ -270,13 +284,7 @@ class APIConnection : public APIServerConnection {
bool try_to_clear_buffer(bool log_out_of_space); bool try_to_clear_buffer(bool log_out_of_space);
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
std::string get_client_combined_info() const { std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); }
if (this->client_info_ == this->client_peername_) {
// Before Hello message, both are the same (just IP:port)
return this->client_info_;
}
return this->client_info_ + " (" + this->client_peername_ + ")";
}
// Buffer allocator methods for batch processing // Buffer allocator methods for batch processing
ProtoWriteBuffer allocate_single_message_buffer(uint16_t size); ProtoWriteBuffer allocate_single_message_buffer(uint16_t size);
@ -482,9 +490,8 @@ class APIConnection : public APIServerConnection {
std::unique_ptr<camera::CameraImageReader> image_reader_; std::unique_ptr<camera::CameraImageReader> image_reader_;
#endif #endif
// Group 3: Strings (12 bytes each on 32-bit, 4-byte aligned) // Group 3: Client info struct (24 bytes on 32-bit: 2 strings × 12 bytes each)
std::string client_info_; ClientInfo client_info_;
std::string client_peername_;
// Group 4: 4-byte types // Group 4: 4-byte types
uint32_t last_traffic_; uint32_t last_traffic_;

View File

@ -1,5 +1,6 @@
#include "api_frame_helper.h" #include "api_frame_helper.h"
#ifdef USE_API #ifdef USE_API
#include "api_connection.h" // For ClientInfo struct
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
@ -13,6 +14,8 @@ namespace api {
static const char *const TAG = "api.socket"; static const char *const TAG = "api.socket";
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__)
const char *api_error_to_str(APIError err) { const char *api_error_to_str(APIError err) {
// not using switch to ensure compiler doesn't try to build a big table out of it // not using switch to ensure compiler doesn't try to build a big table out of it
if (err == APIError::OK) { if (err == APIError::OK) {
@ -81,7 +84,7 @@ APIError APIFrameHelper::handle_socket_write_error_() {
if (errno == EWOULDBLOCK || errno == EAGAIN) { if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", this->info_.c_str(), errno); HELPER_LOG("Socket write failed with errno %d", errno);
this->state_ = State::FAILED; this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED; return APIError::SOCKET_WRITE_FAILED;
} }
@ -198,13 +201,13 @@ APIError APIFrameHelper::try_send_tx_buf_() {
APIError APIFrameHelper::init_common_() { APIError APIFrameHelper::init_common_() {
if (state_ != State::INITIALIZE || this->socket_ == nullptr) { if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
ESP_LOGVV(TAG, "%s: Bad state for init %d", this->info_.c_str(), (int) state_); HELPER_LOG("Bad state for init %d", (int) state_);
return APIError::BAD_STATE; return APIError::BAD_STATE;
} }
int err = this->socket_->setblocking(false); int err = this->socket_->setblocking(false);
if (err != 0) { if (err != 0) {
state_ = State::FAILED; state_ = State::FAILED;
ESP_LOGVV(TAG, "%s: Setting nonblocking failed with errno %d", this->info_.c_str(), errno); HELPER_LOG("Setting nonblocking failed with errno %d", errno);
return APIError::TCP_NONBLOCKING_FAILED; return APIError::TCP_NONBLOCKING_FAILED;
} }
@ -212,14 +215,12 @@ APIError APIFrameHelper::init_common_() {
err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) { if (err != 0) {
state_ = State::FAILED; state_ = State::FAILED;
ESP_LOGVV(TAG, "%s: Setting nodelay failed with errno %d", this->info_.c_str(), errno); HELPER_LOG("Setting nodelay failed with errno %d", errno);
return APIError::TCP_NODELAY_FAILED; return APIError::TCP_NODELAY_FAILED;
} }
return APIError::OK; return APIError::OK;
} }
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->info_.c_str(), ##__VA_ARGS__)
APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) { APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) {
if (received == -1) { if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) { if (errno == EWOULDBLOCK || errno == EAGAIN) {

View File

@ -19,6 +19,9 @@
namespace esphome { namespace esphome {
namespace api { namespace api {
// Forward declaration
struct ClientInfo;
class ProtoWriteBuffer; class ProtoWriteBuffer;
struct ReadPacketBuffer { struct ReadPacketBuffer {
@ -68,7 +71,8 @@ const char *api_error_to_str(APIError err);
class APIFrameHelper { class APIFrameHelper {
public: public:
APIFrameHelper() = default; APIFrameHelper() = default;
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_owned_(std::move(socket)) { explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
: socket_owned_(std::move(socket)), client_info_(client_info) {
socket_ = socket_owned_.get(); socket_ = socket_owned_.get();
} }
virtual ~APIFrameHelper() = default; virtual ~APIFrameHelper() = default;
@ -94,8 +98,6 @@ class APIFrameHelper {
} }
return APIError::OK; return APIError::OK;
} }
// Give this helper a name for logging
void set_log_info(std::string info) { info_ = std::move(info); }
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0; virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
// Write multiple protobuf packets in a single operation // Write multiple protobuf packets in a single operation
// packets contains (message_type, offset, length) for each message in the buffer // packets contains (message_type, offset, length) for each message in the buffer
@ -160,10 +162,13 @@ class APIFrameHelper {
// Containers (size varies, but typically 12+ bytes on 32-bit) // Containers (size varies, but typically 12+ bytes on 32-bit)
std::deque<SendBuffer> tx_buf_; std::deque<SendBuffer> tx_buf_;
std::string info_;
std::vector<struct iovec> reusable_iovs_; std::vector<struct iovec> reusable_iovs_;
std::vector<uint8_t> rx_buf_; std::vector<uint8_t> rx_buf_;
// Pointer to client info (4 bytes on 32-bit)
// Note: The pointed-to ClientInfo object must outlive this APIFrameHelper instance.
const ClientInfo *client_info_{nullptr};
// Group smaller types together // Group smaller types together
uint16_t rx_buf_len_ = 0; uint16_t rx_buf_len_ = 0;
State state_{State::INITIALIZE}; State state_{State::INITIALIZE};
@ -181,8 +186,9 @@ class APIFrameHelper {
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
class APINoiseFrameHelper : public APIFrameHelper { class APINoiseFrameHelper : public APIFrameHelper {
public: public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx) APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx,
: APIFrameHelper(std::move(socket)), ctx_(std::move(ctx)) { const ClientInfo *client_info)
: APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) {
// Noise header structure: // Noise header structure:
// Pos 0: indicator (0x01) // Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian) // Pos 1-2: encrypted payload size (16-bit big-endian)
@ -238,7 +244,8 @@ class APINoiseFrameHelper : public APIFrameHelper {
#ifdef USE_API_PLAINTEXT #ifdef USE_API_PLAINTEXT
class APIPlaintextFrameHelper : public APIFrameHelper { class APIPlaintextFrameHelper : public APIFrameHelper {
public: public:
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) { APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
: APIFrameHelper(std::move(socket), client_info) {
// Plaintext header structure (worst case): // Plaintext header structure (worst case):
// Pos 0: indicator (0x00) // Pos 0: indicator (0x00)
// Pos 1-3: payload size varint (up to 3 bytes) // Pos 1-3: payload size varint (up to 3 bytes)

View File

@ -184,9 +184,9 @@ void APIServer::loop() {
// Rare case: handle disconnection // Rare case: handle disconnection
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_); this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername);
#endif #endif
ESP_LOGV(TAG, "Remove connection %s", client->client_info_.c_str()); ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name.c_str());
// Swap with the last element and pop (avoids expensive vector shifts) // Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) { if (client_index < this->clients_.size() - 1) {