Repository navigation
Add max_read_size: cap the bytes taken from the socket per read - #118
Open
oliver-zehentleitner-aigent wants to merge 1 commit into
Open
oliver-zehentleitner-aigent wants to merge 1 commit into
oliver-zehentleitner-aigent wants to merge 1 commit into
Conversation
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.
oliver-zehentleitner-aigent
added a commit
to oliver-zehentleitner-aigent/picows
that referenced
this pull request
Sep 21, 2026
oliver-zehentleitner-aigent
added a commit
to oliver-zehentleitner-aigent/picows
that referenced
this pull request
Oct 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #117.
What
ws_connect()andws_create_server()get a newmax_read_sizeargument (default0= 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**kwargslike the otherws_connectoptions.Why
When the peer sends faster than the application consumes, the socket receive buffer holds megabytes (Linux autotunes
tcp_rmemup 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: 79recvfromcalls of ~3.4 MB for 600 x 450 KB messages, versus 1,060 reads of 256 KB with the asyncio transportwebsocketsuses.read_buffer_init_sizedoes 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. CappingSO_RCVBUFon 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@arrframes from a local picows server, consumer slower than the loopback wire, median of 4, same box:max_read_size=256*1024max_read_size=64*1024(
websocketson 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, alluse_aiofastnetx TLS combinations. Full suite: 802 passed;test_autoping.py::test_no_ping_when_data_is_presentfails intermittently on this box on unpatched master as well (0.1 s timings), unrelated.Open for you
0so 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.rstnot touched since I do not know the next version heading, happy to add a line.