[api] Add single-buffer write_raw_ overload for single-message path

The existing write_raw_ takes iovec array + count, but single-message
writes (87-100% of traffic) always pass iovcnt=1. Adding a dedicated
overload that takes (data, len) eliminates iovec construction, the
iovcnt==1 branch, and pointer indirection on the hot path.
This commit is contained in:
J. Nick Koston
2026-03-29 10:42:20 -10:00
parent 98b7f5a571
commit 5d8f67c819
4 changed files with 61 additions and 16 deletions
+48 -2
View File
@@ -112,8 +112,54 @@ APIError APIFrameHelper::drain_overflow_and_handle_errors_() {
}
// Write data to socket, overflow to backlog buffer if LWIP TCP send buffer is full.
// Returns OK if all data was sent or successfully queued.
// Returns SOCKET_WRITE_FAILED on hard error (sets state to FAILED).
// Single-buffer write path — avoids iovec setup for the common single-message case.
APIError APIFrameHelper::write_raw_(const void *data, uint16_t len) {
#ifdef HELPER_LOG_PACKETS
LOG_PACKET_SENDING(reinterpret_cast<const uint8_t *>(data), len);
#endif
// Drain any existing backlog first
if (!this->overflow_buf_.empty()) [[unlikely]] {
APIError err = this->drain_overflow_and_handle_errors_();
if (err != APIError::OK)
return err;
}
// If backlog is clear, try direct send
if (this->overflow_buf_.empty()) [[likely]] {
ssize_t sent = this->socket_->write(data, len);
if (sent == -1) [[unlikely]] {
int err = errno;
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
HELPER_LOG("Socket write failed with errno %d", err);
return APIError::SOCKET_WRITE_FAILED;
}
} else if (static_cast<uint16_t>(sent) >= len) [[likely]] {
return APIError::OK;
} else {
// Partial write — queue remainder into overflow buffer
struct iovec iov = {const_cast<void *>(data), len};
if (!this->overflow_buf_.enqueue_iov(&iov, 1, len, static_cast<uint16_t>(sent))) {
HELPER_LOG("Overflow buffer full, dropping connection");
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
}
return APIError::OK;
}
}
// Socket not ready — queue all data into overflow buffer
struct iovec iov = {const_cast<void *>(data), len};
if (!this->overflow_buf_.enqueue_iov(&iov, 1, len, 0)) {
HELPER_LOG("Overflow buffer full, dropping connection");
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
}
return APIError::OK;
}
// Multi-buffer write path for batched messages.
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
#ifdef HELPER_LOG_PACKETS
for (int i = 0; i < iovcnt; i++) {
+3 -1
View File
@@ -190,7 +190,9 @@ class APIFrameHelper {
// Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors.
APIError drain_overflow_and_handle_errors_();
// Common implementation for writing raw data to socket
// Write a single contiguous buffer to the socket
APIError write_raw_(const void *data, uint16_t len);
// Write multiple iovec buffers to the socket in one writev call
APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
// Check if a socket write errno is a hard error (not WOULD_BLOCK/EAGAIN).
@@ -499,11 +499,12 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuff
MessageInfo msg{type, 0,
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
uint8_t *buf_start = buffer.get_buffer()->data();
struct iovec iov;
aerr = this->encrypt_noise_message_(buffer.get_buffer()->data(), msg, iov);
aerr = this->encrypt_noise_message_(buf_start, msg, iov);
if (aerr != APIError::OK)
return aerr;
return this->write_raw_(&iov, 1, static_cast<uint16_t>(iov.iov_len));
return this->write_raw_(iov.iov_base, static_cast<uint16_t>(iov.iov_len));
}
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
@@ -538,12 +539,12 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
header[1] = (uint8_t) (len >> 8);
header[2] = (uint8_t) len;
if (len == 0) {
return this->write_raw_(header, 3); // Just header
}
struct iovec iov[2];
iov[0].iov_base = header;
iov[0].iov_len = 3;
if (len == 0) {
return this->write_raw_(iov, 1, 3); // Just header
}
iov[1].iov_base = const_cast<uint8_t *>(data);
iov[1].iov_len = len;
@@ -205,7 +205,6 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
// Make sure to tell the remote that we don't
// understand the indicator byte so it knows
// we do not support it.
struct iovec iov[1];
// The \x00 first byte is the marker for plaintext.
//
// The remote will know how to handle the indicator byte,
@@ -220,14 +219,12 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
"Bad indicator byte";
char msg[INDICATOR_MSG_SIZE];
memcpy_P(msg, MSG_PROGMEM, INDICATOR_MSG_SIZE);
iov[0].iov_base = (void *) msg;
this->write_raw_(msg, INDICATOR_MSG_SIZE);
#else
static const char MSG[] = "\x00"
"Bad indicator byte";
iov[0].iov_base = (void *) MSG;
this->write_raw_(MSG, INDICATOR_MSG_SIZE);
#endif
iov[0].iov_len = INDICATOR_MSG_SIZE;
this->write_raw_(iov, 1, INDICATOR_MSG_SIZE);
}
return aerr;
}
@@ -294,9 +291,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWrite
uint8_t *buffer_data = buffer.get_buffer()->data();
uint8_t *msg_start = write_plaintext_header(buffer_data, msg, frame_header_padding_);
uint8_t msg_header_len = static_cast<uint8_t>(buffer_data + frame_header_padding_ - msg_start);
size_t msg_len = static_cast<size_t>(msg_header_len + msg.payload_size);
struct iovec iov = {msg_start, msg_len};
return write_raw_(&iov, 1, static_cast<uint16_t>(msg_len));
uint16_t msg_len = static_cast<uint16_t>(msg_header_len + msg.payload_size);
return write_raw_(msg_start, msg_len);
}
APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer,