Skip to content

Add max_read_size: cap the bytes taken from the socket per read - #118

Open
oliver-zehentleitner-aigent wants to merge 1 commit into
tarasko:masterfrom
oliver-zehentleitner-aigent:feature/max-read-size
Open

oliver-zehentleitner-aigent wants to merge 1 commit into
tarasko:masterfrom
oliver-zehentleitner-aigent:feature/max-read-size

Conversation

@oliver-zehentleitner-aigent

Copy link
Copy Markdown

Follow-up to #117.

What

ws_connect() and ws_create_server() get a new max_read_size argument (default 0 = today's behaviour). WSProtocol.get_buffer() offers the transport at most that many bytes per read; the read buffer growth logic is untouched, a frame larger than the cap is still reassembled across reads. picows.websockets.connect() passes it through via **kwargs like the other ws_connect options.

Why

When the peer sends faster than the application consumes, the socket receive buffer holds megabytes (Linux autotunes tcp_rmem up to 6 MB) and a single read pulls all of it into the read buffer at once, which doubles to fit and never shrinks. The frames are then copied out (get_payload_as_bytes) and decoded after their bytes have left the CPU cache, and that costs more per byte than the parser saves on large frames. strace on the UBWA replay benchmark: 79 recvfrom calls of ~3.4 MB for 600 x 450 KB messages, versus 1,060 reads of 256 KB with the asyncio transport websockets uses. read_buffer_init_size does not help here (a 4 MiB initial buffer measures the same, 313 vs. 229 µs/msg), the cost is per byte read in one go, not per realloc. Capping SO_RCVBUF on the client socket to 128 KB flipped the result, this does the same inside picows without touching the socket.

Numbers

unicorn-binance-websocket-api 2.16.1 replay benchmark (dev/test_websocket_library_benchmark.py), 450 KB !ticker@arr frames from a local picows server, consumer slower than the loopback wire, median of 4, same box:

picows msgs/s CPU µs/msg
2.3.1 unchanged 3,780 297
max_read_size=256*1024 8,381 146
max_read_size=64*1024 8,684 141

(websockets on the same run: 4,546-4,873 msgs/s, 227-240 µs.) 9 KB frames: 117k -> 173k msgs/s. The bare echo loop (consumer faster than the wire) does not change, as expected.

Test

test_max_read_size_reassembles_large_frames: 512 KB frame round trip with a 16 KB cap on both sides, all use_aiofastnet x TLS combinations. Full suite: 802 passed; test_autoping.py::test_no_ping_when_data_is_present fails intermittently on this box on unpatched master as well (0.1 s timings), unrelated.

Open for you

  • Default: left at 0 so nothing changes for existing users. A default in the range of 256 KB (the asyncio transport's per-read size) would give the improvement to everyone; your call.
  • HISTORY.rst not touched since I do not know the next version heading, happy to add a line.

When the peer sends faster than the application consumes, the socket
receive buffer holds megabytes and a single read pulls all of it into the
read buffer at once (the buffer doubles to fit and never shrinks). The
frames are then copied out and decoded after their bytes have left the CPU
cache, which costs more per byte than the parser saves on large frames.

`ws_connect()` and `ws_create_server()` get `max_read_size` (default 0 =
unchanged behaviour): `get_buffer()` offers at most that many bytes per
read, so each frame's bytes stay cache-resident while they are processed.
The buffer growth logic is untouched, a frame larger than the cap is still
reassembled across reads.

Measured with a 450 KB frame stream on loopback and a consumer slower
than the wire (the unicorn-binance-websocket-api replay benchmark,
tarasko#117): 3,780 -> 8,381 msgs/s and 297 -> 146 us CPU per
message with max_read_size=256 KB, 9 KB frames 117k -> 173k msgs/s; the
raw echo case does not change. Test: a 512 KB frame round trip with a
16 KB cap on both sides, all transport/TLS combinations.
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