You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
assignee=Noneclosed_at=Nonecreated_at=<Date2020-07-11.16:23:40.362>labels= ['expert-asyncio']
title='Add a StreamReaderBufferedProtocol'updated_at=<Date2020-08-29.21:19:22.551>user='https://github.com/tontinton'
@webknjaz FTR, we do not link PRs to issues in general because of backports or if we need to do more things afterwards. We usually leave the responsibility of closing the issue to the core dev who merged the PR.
cc @kumaraditya303 for the validity and relevence of this FR.
@picnixz ah, I didn't realize, reverted that. Thanks! I was going through a bunch of issues and noticed incorrect links in the OP, I corrected that by commenting one out (left a note in the OP to see if you click Edit) and assumed this was a part of the breakage. I'll stop linking PRs, then (there may be one or two others that I linked left, though).
I found a severe Linux allocator/pathological-latency case that makes the BufferedProtocol / recv_into() direction here more than a steady-state micro-optimization. It is reproducible without any third-party library.
Environment where it reproduces reliably enough to characterize:
CPython 3.14.7
Linux aarch64, Raspberry Pi 5 (Cortex-A76)
glibc 2.43
selector event loop
The selector transport still has max_size = 256 * 1024 and the ordinary Protocol path calls sock.recv(self.max_size). With small socket arrivals, socket.recv(262144) can enter a process-layout-dependent allocator regime where each small read effectively causes mmap -> recvfrom -> mremap -> munmap.
A minimal raw-socket test is enough to reproduce it. The important shape is one small payload outstanding at a time and a fresh client process per repetition:
# run this client as a fresh process repeatedlyimportresource, socket, times=socket.create_connection(("127.0.0.1", 9999))
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
for_inrange(2000):
assertlen(s.recv(262144)) ==256s.sendall(b"A")
a=resource.getrusage(resource.RUSAGE_SELF)
t0=time.perf_counter()
for_inrange(20000):
assertlen(s.recv(262144)) ==256s.sendall(b"A")
dt=time.perf_counter() -t0b=resource.getrusage(resource.RUSAGE_SELF)
print("us/read", dt*1e6/20000)
print("minor_faults/read", (b.ru_minflt-a.ru_minflt) /20000)
print("stime_us/read", (b.ru_stime-a.ru_stime) *1e6/20000)
On the same host, 8 fresh-process repetitions of raw socket.recv(262144) split into 5 fast processes at ~12.45-12.57 us/read with 0 minor faults/read and 3 slow processes at ~14.87-15.05 us/read with exactly ~2.0 minor faults/read. Major faults were always 0.
Controls:
socket.recv(65536): 8/8 at 0 minor faults/read, ~12.49 us/read median.
reusable bytearray(262144) + socket.recv_into(): 8/8 at 0 minor faults/read and ~12.27 us/read median.
asyncio StreamReader / selector with the default max_size=262144: 2/16 fresh processes at exactly ~2.0 minor faults/read, ~35.5-36.0 us/read vs ~22.2-23.0 us/read in the fast regime.
the same asyncio path with transport.max_size=65536: 16/16 at 0 minor faults/read, ~22.36 us/read median.
setarch aarch64 -R with the 256 KiB read: 16/16 at 0 minor faults/read.
GLIBC_TUNABLES=glibc.malloc.mmap_threshold=1048576 with the 256 KiB read: 16/16 at 0 minor faults/read.
A slow raw recv(262144) run under strace -c for 6000 measured reads produced:
6000 mremap
6079 mmap
6049 munmap
6000 recvfrom
The recv_into() and 64 KiB variants had no per-read mremap; only normal startup mappings remained.
This looks closely related to #121313: recv(n) has to allocate a bytes for the requested size before it knows how much the kernel will return, then shrink it. At 256 KiB this crosses glibc's mmap territory, and depending on process/allocator layout the allocate-short-read-resize-free cycle can become dramatically more expensive. The process-layout dependence explains why it may be invisible on many machines; I could not reproduce the bimodality on an x86-64 KVM host under the same application-level workload.
The full controlled run and raw evidence are public here: https://github.com/yoch/mqttium/actions/runs/34188738358 . Although that repository is MQTT-related, this particular run contains a standalone socket reproducer and does not import MQTTium or use an MQTT broker.
So there seem to be two possible upstream remedies worth considering:
the structural solution proposed by this issue/PR: use a reusable buffer + recv_into() for streams; or
independently, reconsider the selector transport's 256 KiB default read size for ordinary Protocols, since a 64 KiB cap completely eliminated this allocator regime in the experiment without requiring a persistent per-stream 256 KiB buffer.
I can provide a smaller CPython-repo benchmark/test if useful.
Activity
tontinton commentedon Jul 11, 2020
This will greatly increase performance, from my internal tests it was about 150% on linux.
Using read_into instead of read will make it so we do not allocate a new buffer each time data is received.
tontinton commentedon Aug 29, 2020
bump
[-]Convert StreamReaderProtocol to a BufferedProtocol[/-][+]Add a StreamReaderBufferedProtocol[/+][-]Convert StreamReaderProtocol to a BufferedProtocol[/-][+]Add a StreamReaderBufferedProtocol[/+]picnixz commentedon Jan 18, 2026
@webknjaz FTR, we do not link PRs to issues in general because of backports or if we need to do more things afterwards. We usually leave the responsibility of closing the issue to the core dev who merged the PR.
cc @kumaraditya303 for the validity and relevence of this FR.
webknjaz commentedon Jan 18, 2026
@picnixz ah, I didn't realize, reverted that. Thanks! I was going through a bunch of issues and noticed incorrect links in the OP, I corrected that by commenting one out (left a note in the OP to see if you click Edit) and assumed this was a part of the breakage. I'll stop linking PRs, then (there may be one or two others that I linked left, though).
yoch commentedon Sep 8, 2026
I found a severe Linux allocator/pathological-latency case that makes the
BufferedProtocol/recv_into()direction here more than a steady-state micro-optimization. It is reproducible without any third-party library.Environment where it reproduces reliably enough to characterize:
The selector transport still has
max_size = 256 * 1024and the ordinaryProtocolpath callssock.recv(self.max_size). With small socket arrivals,socket.recv(262144)can enter a process-layout-dependent allocator regime where each small read effectively causesmmap -> recvfrom -> mremap -> munmap.A minimal raw-socket test is enough to reproduce it. The important shape is one small payload outstanding at a time and a fresh client process per repetition:
On the same host, 8 fresh-process repetitions of raw
socket.recv(262144)split into 5 fast processes at ~12.45-12.57 us/read with 0 minor faults/read and 3 slow processes at ~14.87-15.05 us/read with exactly ~2.0 minor faults/read. Major faults were always 0.Controls:
socket.recv(65536): 8/8 at 0 minor faults/read, ~12.49 us/read median.bytearray(262144)+socket.recv_into(): 8/8 at 0 minor faults/read and ~12.27 us/read median.StreamReader/ selector with the defaultmax_size=262144: 2/16 fresh processes at exactly ~2.0 minor faults/read, ~35.5-36.0 us/read vs ~22.2-23.0 us/read in the fast regime.transport.max_size=65536: 16/16 at 0 minor faults/read, ~22.36 us/read median.setarch aarch64 -Rwith the 256 KiB read: 16/16 at 0 minor faults/read.GLIBC_TUNABLES=glibc.malloc.mmap_threshold=1048576with the 256 KiB read: 16/16 at 0 minor faults/read.A slow raw
recv(262144)run understrace -cfor 6000 measured reads produced:The
recv_into()and 64 KiB variants had no per-readmremap; only normal startup mappings remained.This looks closely related to #121313:
recv(n)has to allocate abytesfor the requested size before it knows how much the kernel will return, then shrink it. At 256 KiB this crosses glibc's mmap territory, and depending on process/allocator layout the allocate-short-read-resize-free cycle can become dramatically more expensive. The process-layout dependence explains why it may be invisible on many machines; I could not reproduce the bimodality on an x86-64 KVM host under the same application-level workload.The full controlled run and raw evidence are public here: https://github.com/yoch/mqttium/actions/runs/34188738358 . Although that repository is MQTT-related, this particular run contains a standalone socket reproducer and does not import MQTTium or use an MQTT broker.
So there seem to be two possible upstream remedies worth considering:
recv_into()for streams; orProtocols, since a 64 KiB cap completely eliminated this allocator regime in the experiment without requiring a persistent per-stream 256 KiB buffer.I can provide a smaller CPython-repo benchmark/test if useful.