Skip to content

WebSocket sends: no copy into a frame, encoded buffers taken rather than copied - #10

Merged
robdobsn merged 2 commits into
mainfrom
perf/ws-publish-copies
Oct 25, 2026
Merged

robdobsn merged 2 commits into
mainfrom
perf/ws-publish-copies

Conversation

@robdobsn

@robdobsn robdobsn commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Less main-loop time sending large WebSocket messages, such as published camera frames.

Changes

  1. No copy into a frame. A server's message is sent as the header followed by the data, rather than copied into a frame buffer first. Clients' messages are still built into a frame, as they have to be masked. What the connection can't send at once is queued in order, as before. If it can't be queued, the send fails and the connection is closed, so a frame is never left half-sent.
  2. The connection takes the encoded buffer. It does this when the message's buffer may be taken (RaftCore's CommsChannelMsg::isBufTakeable(), set on a RICREST message just encoded for sending).
    • The take path goes through: the WebServer SysMod, RaftWebServer, RaftWebConnManager, RaftWebResponderWS, RaftWebSocketLink, RaftWebConnection.
    • What the socket can't take at once is kept by moving the buffer into the empty send queue, instead of being copied into it. That is everything after the first 5.7 KB of a camera frame, the TCP send buffer's size.
    • The buffer is taken only when one connection is on the channel. Other messages and other senders (SSE, HTTP) work as before.
  3. The send queue keeps a read position. Sending from it no longer erases the sent bytes from the front, which moved everything left each time (about 57 KB of copying over the following loops for a 29 KB frame).

Measured

ESP32-S3 with quad PSRAM at 80 MHz, camera frames published over a WebSocket at 2 Hz, longest Publish hold on the main loop:

Frame Before 1 only 1-3
QVGA, about 7 KB 6.5-8 ms 6.7-8 ms 6.6-7.1 ms
VGA, about 18 KB 13-16 ms about 12 ms 10.7-11.2 ms
SVGA, about 28 KB 21-22 ms 17-19 ms 13.4 ms

Every frame arrived as a valid JPEG, and more got through at SVGA: 1.8 a second, against 1.4.

Most of what's left is the HDLC encode (about 9-10 ms at SVGA). The rest is lwIP copying the first 5.7 KB.

A 32 KB TCP send buffer was also tried. It made the hold longer (20-21 ms at SVGA), because lwIP's copy costs more than the queue's.

Needs RaftCore with CommsChannelMsg::isBufTakeable() (robdobsn/RaftCore#31, merged).

The can-send check change (lwip_select()) was pushed after this was merged, so it is in #11.

🤖 Generated with Claude Code

robdobsn and others added 2 commits October 25, 2026 20:55
A server's (unmasked) message is sent as the header then the data, rather than copied into a frame buffer first.
What the connection can't send at once is queued in order as before, and if it can't be queued the send fails
and the connection is closed, so a frame is never left part sent. A client's (masked) message is still built
into a frame, as it has to be masked.

Measured publishing camera frames from an ESP32-S3 with quad PSRAM: building the frame took up to 4-6 ms for a
29 KB frame and now takes nothing, and the longest publish went from 21-22 ms to 17-19 ms (13-16 ms to about 12
ms for 19 KB frames).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…her than copying it

- A message whose buffer may be taken (RaftCore's CommsChannelMsg::isBufTakeable(), set on a RICREST message just
  encoded for sending) goes through a take path (WebServer SysMod, RaftWebServer, RaftWebConnManager, the
  responder, RaftWebSocketLink, RaftWebConnection): what the socket can't take at once (all but the first 5.7 KB
  of a camera frame) is kept by taking the buffer into the empty send queue, rather than copied into it. The
  buffer is only taken when one connection is on the channel; other messages and senders are unchanged.
- The send queue keeps a read position: sending from it no longer erases what was sent from the front, which
  moved everything left each time (about 57 KB of copying in later loops for a 29 KB frame).

Measured publishing camera frames from an ESP32-S3 with quad PSRAM: longest publish 13.4 ms for 28 KB frames
(17-19 ms with the previous commit, 21-22 ms before), 10.7-11.2 ms for 18 KB frames, and more frames got through
(1.8 a second at 2 Hz, was 1.4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@robdobsn robdobsn changed the title WebSocket server sends: no copy of the message into a frame WebSocket sends: no copy into a frame, encoded buffers taken rather than copied Oct 25, 2026
@robdobsn
robdobsn merged commit 88a13d9 into main Oct 25, 2026
@robdobsn
robdobsn deleted the perf/ws-publish-copies branch October 25, 2026 20:05
@robdobsn robdobsn changed the title WebSocket sends: no copy into a frame, encoded buffers taken rather than copied WebSocket and HTTP sends: fewer copies, lwIP select Oct 25, 2026
@robdobsn robdobsn changed the title WebSocket and HTTP sends: fewer copies, lwIP select WebSocket sends: no copy into a frame, encoded buffers taken rather than copied Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant