From 09bd0464d885b4034df7a96bad1c833a09415164 Mon Sep 17 00:00:00 2001 From: Louis-Philippe Gauthier Date: Sat, 15 Aug 2026 16:44:24 -0400 Subject: [PATCH 1/5] erts: Adapt the socket receive buffer size to traffic Reads that fill the read buffer double it (up to 1 MB) and reads that stop using it shrink it back towards the default, so bulk transfers no longer run at the default 8 KB per recv call. Setting the rcvbuf option explicitly pins the size as before, since it bounds the chunks a length 0 recv may return. Draining a local bulk stream goes from 4.2 to 6.8 GB/s. --- erts/emulator/nifs/common/prim_socket_int.h | 8 +++++ erts/emulator/nifs/common/prim_socket_nif.c | 13 ++++++-- erts/emulator/nifs/unix/unix_socket_syncio.c | 31 ++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/erts/emulator/nifs/common/prim_socket_int.h b/erts/emulator/nifs/common/prim_socket_int.h index cb77a1003735..dbde1f988cb6 100644 --- a/erts/emulator/nifs/common/prim_socket_int.h +++ b/erts/emulator/nifs/common/prim_socket_int.h @@ -504,6 +504,14 @@ typedef struct { ESockCounter accFails; /* +++ Config stuff +++ */ size_t rBufSz; // Read buffer size (when data length = 0) + /* While rcvbuf has not been set explicitly, rBufSz adapts to the + * traffic (grows when reads fill the buffer, shrinks back towards + * the default when they stop). An explicit rcvbuf pins the size, + * since it bounds the chunks a length 0 recv may return. + */ + BOOLEAN_T rBufAdapt; + size_t rBufSzCfg; + unsigned int rBufShrinkCnt; /* rNum and rNumCnt are used (together with rBufSz) when calling the recv * function with the Length argument set to 0 (zero). * If rNum is 0 (zero), then rNumCnt is not used and only *one* read will diff --git a/erts/emulator/nifs/common/prim_socket_nif.c b/erts/emulator/nifs/common/prim_socket_nif.c index db477e17bb93..f2550574fb47 100644 --- a/erts/emulator/nifs/common/prim_socket_nif.c +++ b/erts/emulator/nifs/common/prim_socket_nif.c @@ -8224,6 +8224,9 @@ ERL_NIF_TERM esock_setopt_otp_rcvbuf(ErlNifEnv* env, descP->rBufSz = ESOCK_RECV_BUFFER_SIZE_MIN; else descP->rBufSz = bufSz; + descP->rBufAdapt = FALSE; + descP->rBufSzCfg = descP->rBufSz; + descP->rBufShrinkCnt = 0; SSDBG( descP, ("SOCKET", "esock_setopt_otp_rcvbuf {%d} -> ok" @@ -11833,14 +11836,15 @@ ERL_NIF_TERM esock_getopt_otp_rcvbuf(ErlNifEnv* env, } #ifdef __WIN32__ - eVal = MKUL(env, (unsigned long) descP->rBufSz); + eVal = MKUL(env, (unsigned long) descP->rBufSzCfg); #else + /* The current (adapted) size is internal; report the configured one */ if (descP->rNum == 0) { - eVal = MKUL(env, (unsigned long) descP->rBufSz); + eVal = MKUL(env, (unsigned long) descP->rBufSzCfg); } else { eVal = MKT2(env, MKI(env, descP->rNum), - MKUL(env, (unsigned long) descP->rBufSz)); + MKUL(env, (unsigned long) descP->rBufSzCfg)); } #endif @@ -17059,6 +17063,9 @@ ESockDescriptor* esock_alloc_descriptor(SOCKET sock) /* *** Config section *** */ // sprintf(buf, "esock.cfg[" SOCKET_FORMAT_STR "]", sock); descP->rBufSz = ESOCK_RECV_BUFFER_SIZE_DEFAULT; + descP->rBufAdapt = TRUE; + descP->rBufSzCfg = ESOCK_RECV_BUFFER_SIZE_DEFAULT; + descP->rBufShrinkCnt = 0; #ifndef __WIN32__ descP->rNum = ESOCK_RECV_BUFFER_COUNT_DEFAULT; descP->rNumCnt = 0; diff --git a/erts/emulator/nifs/unix/unix_socket_syncio.c b/erts/emulator/nifs/unix/unix_socket_syncio.c index 3049d287f775..16ad49d991e2 100644 --- a/erts/emulator/nifs/unix/unix_socket_syncio.c +++ b/erts/emulator/nifs/unix/unix_socket_syncio.c @@ -209,6 +209,13 @@ if (ctrl.sctp.bindx == NULL) \ return enif_raise_exception((e), MKA((e), "notsup")); #define sock_close(s) close((s)) + +/* Adaptive read buffer: double on a filled buffer, halve back towards + * the configured size after this many consecutive reads that used + * less than a quarter of it. + */ +#define ESSIO_RECV_ADAPT_BUFFER_MAX (1 << 20) +#define ESSIO_RECV_ADAPT_SHRINK_COUNT 32 // #define sock_close_event(e) /* do nothing */ #define sock_connect(s, addr, len) connect((s), (addr), (len)) #define sock_connectx(s, addrs, acnt, aidp) \ @@ -2841,6 +2848,9 @@ BOOLEAN_T essio_accept_accepted(ErlNifEnv* env, MLOCK(descP->writeMtx); accDescP->rBufSz = descP->rBufSz; // Inherit buffer size + accDescP->rBufAdapt = descP->rBufAdapt; + accDescP->rBufSzCfg = descP->rBufSzCfg; + accDescP->rBufShrinkCnt = 0; accDescP->rNum = descP->rNum; // Inherit buffer uses accDescP->rNumCnt = 0; accDescP->rCtrlSz = descP->rCtrlSz; // Inherit buffer size @@ -2944,6 +2954,9 @@ ERL_NIF_TERM essio_peeloff(ErlNifEnv* env, __FUNCTION__, descP->sock, sock) ); poDescP->rBufSz = descP->rBufSz; // Inherit buffer size + poDescP->rBufAdapt = descP->rBufAdapt; + poDescP->rBufSzCfg = descP->rBufSzCfg; + poDescP->rBufShrinkCnt = 0; poDescP->rNum = descP->rNum; // Inherit buffer uses poDescP->rNumCnt = 0; poDescP->rCtrlSz = descP->rCtrlSz; // Inherit buffer size @@ -3847,6 +3860,24 @@ ERL_NIF_TERM essio_recv(ErlNifEnv* env, } /* readResult >= 0 */ + if ((len == 0) && descP->rBufAdapt) { + if ((size_t) readResult == bufP->size) { + if (descP->rBufSz < ESSIO_RECV_ADAPT_BUFFER_MAX) + descP->rBufSz <<= 1; + descP->rBufShrinkCnt = 0; + } else if ((descP->rBufSz > descP->rBufSzCfg) && + ((size_t) readResult < (descP->rBufSz >> 2))) { + if (++descP->rBufShrinkCnt >= ESSIO_RECV_ADAPT_SHRINK_COUNT) { + descP->rBufSz >>= 1; + if (descP->rBufSz < descP->rBufSzCfg) + descP->rBufSz = descP->rBufSzCfg; + descP->rBufShrinkCnt = 0; + } + } else { + descP->rBufShrinkCnt = 0; + } + } + ESOCK_ASSERT( recv_create_bin(bufP, readResult, &bin) ); if (bin.size < bufP->size) { From 3e784a054be51c0fdd7d1a7f8dc40b8520f3685d Mon Sep 17 00:00:00 2001 From: Louis-Philippe Gauthier Date: Fri, 21 Aug 2026 11:44:43 -0400 Subject: [PATCH 2/5] erts: Keep the configured esock rcvbuf in rBufSz rBufSz gets back its single meaning, the configured size, so getopt, setopt and the recvfrom/recvmsg/recvmmsg paths are untouched by the adaptation. The working size moves to rBufSzAdapt, written and read only by essio_recv, which stops recvmmsg from multiplying an adapted 1 MB by up to 1024 buffers into an allocation large enough to abort the emulator. Adaptation is also gated to stream sockets. A dgram read into a shrunken buffer would truncate datagrams unpredictably, whereas the configured size truncates them predictably. The fields live in the existing non-Windows block, size_t first to avoid padding, and the Windows backend no longer sees them at all. --- erts/emulator/nifs/common/prim_socket_int.h | 22 +++++++++--------- erts/emulator/nifs/common/prim_socket_nif.c | 17 +++++++------- erts/emulator/nifs/unix/unix_socket_syncio.c | 24 +++++++++++--------- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/erts/emulator/nifs/common/prim_socket_int.h b/erts/emulator/nifs/common/prim_socket_int.h index dbde1f988cb6..cc7f539af281 100644 --- a/erts/emulator/nifs/common/prim_socket_int.h +++ b/erts/emulator/nifs/common/prim_socket_int.h @@ -504,19 +504,11 @@ typedef struct { ESockCounter accFails; /* +++ Config stuff +++ */ size_t rBufSz; // Read buffer size (when data length = 0) - /* While rcvbuf has not been set explicitly, rBufSz adapts to the - * traffic (grows when reads fill the buffer, shrinks back towards - * the default when they stop). An explicit rcvbuf pins the size, - * since it bounds the chunks a length 0 recv may return. - */ - BOOLEAN_T rBufAdapt; - size_t rBufSzCfg; - unsigned int rBufShrinkCnt; - /* rNum and rNumCnt are used (together with rBufSz) when calling the recv + /* rNum and rNumCnt are used (together with rBufSz) when calling the recv * function with the Length argument set to 0 (zero). * If rNum is 0 (zero), then rNumCnt is not used and only *one* read will - * be done. Also, when get'ing the value of the option (rcvbuf) with - * getopt, the value will be reported as an integer. If the rNum has a + * be done. Also, when get'ing the value of the option (rcvbuf) with + * getopt, the value will be reported as an integer. If the rNum has a * value greater then 0 (zero), then it will instead be reported as * {N, BufSz}. * On Windows, rNum and rNumCnt is *not* used! @@ -524,6 +516,14 @@ typedef struct { #ifndef __WIN32__ unsigned int rNum; // recv: Number of reads using rBufSz unsigned int rNumCnt; // recv: Current number of reads (so far) + /* While rcvbuf has not been set explicitly, a length 0 recv on a + * stream socket reads into a buffer that adapts to the traffic + * (rBufSzAdapt, only used by essio_recv). An explicit rcvbuf pins + * the size, since it bounds the chunks such a recv may return. + */ + size_t rBufSzAdapt; // Current adaptive read buffer size + unsigned int rBufShrinkCnt; + BOOLEAN_T rBufAdapt; #endif size_t rCtrlSz; // Read control buffer size diff --git a/erts/emulator/nifs/common/prim_socket_nif.c b/erts/emulator/nifs/common/prim_socket_nif.c index f2550574fb47..e7c3fbc2badb 100644 --- a/erts/emulator/nifs/common/prim_socket_nif.c +++ b/erts/emulator/nifs/common/prim_socket_nif.c @@ -8224,9 +8224,11 @@ ERL_NIF_TERM esock_setopt_otp_rcvbuf(ErlNifEnv* env, descP->rBufSz = ESOCK_RECV_BUFFER_SIZE_MIN; else descP->rBufSz = bufSz; +#ifndef __WIN32__ descP->rBufAdapt = FALSE; - descP->rBufSzCfg = descP->rBufSz; + descP->rBufSzAdapt = descP->rBufSz; descP->rBufShrinkCnt = 0; +#endif SSDBG( descP, ("SOCKET", "esock_setopt_otp_rcvbuf {%d} -> ok" @@ -11836,15 +11838,14 @@ ERL_NIF_TERM esock_getopt_otp_rcvbuf(ErlNifEnv* env, } #ifdef __WIN32__ - eVal = MKUL(env, (unsigned long) descP->rBufSzCfg); + eVal = MKUL(env, (unsigned long) descP->rBufSz); #else - /* The current (adapted) size is internal; report the configured one */ if (descP->rNum == 0) { - eVal = MKUL(env, (unsigned long) descP->rBufSzCfg); + eVal = MKUL(env, (unsigned long) descP->rBufSz); } else { eVal = MKT2(env, MKI(env, descP->rNum), - MKUL(env, (unsigned long) descP->rBufSzCfg)); + MKUL(env, (unsigned long) descP->rBufSz)); } #endif @@ -17063,12 +17064,12 @@ ESockDescriptor* esock_alloc_descriptor(SOCKET sock) /* *** Config section *** */ // sprintf(buf, "esock.cfg[" SOCKET_FORMAT_STR "]", sock); descP->rBufSz = ESOCK_RECV_BUFFER_SIZE_DEFAULT; - descP->rBufAdapt = TRUE; - descP->rBufSzCfg = ESOCK_RECV_BUFFER_SIZE_DEFAULT; - descP->rBufShrinkCnt = 0; #ifndef __WIN32__ descP->rNum = ESOCK_RECV_BUFFER_COUNT_DEFAULT; descP->rNumCnt = 0; + descP->rBufAdapt = TRUE; + descP->rBufSzAdapt = ESOCK_RECV_BUFFER_SIZE_DEFAULT; + descP->rBufShrinkCnt = 0; #endif descP->rCtrlSz = ESOCK_RECV_CTRL_BUFFER_SIZE_DEFAULT; descP->wCtrlSz = ESOCK_SEND_CTRL_BUFFER_SIZE_DEFAULT; diff --git a/erts/emulator/nifs/unix/unix_socket_syncio.c b/erts/emulator/nifs/unix/unix_socket_syncio.c index 16ad49d991e2..127fe1f46bbe 100644 --- a/erts/emulator/nifs/unix/unix_socket_syncio.c +++ b/erts/emulator/nifs/unix/unix_socket_syncio.c @@ -2849,7 +2849,7 @@ BOOLEAN_T essio_accept_accepted(ErlNifEnv* env, accDescP->rBufSz = descP->rBufSz; // Inherit buffer size accDescP->rBufAdapt = descP->rBufAdapt; - accDescP->rBufSzCfg = descP->rBufSzCfg; + accDescP->rBufSzAdapt = descP->rBufSz; accDescP->rBufShrinkCnt = 0; accDescP->rNum = descP->rNum; // Inherit buffer uses accDescP->rNumCnt = 0; @@ -2955,7 +2955,7 @@ ERL_NIF_TERM essio_peeloff(ErlNifEnv* env, poDescP->rBufSz = descP->rBufSz; // Inherit buffer size poDescP->rBufAdapt = descP->rBufAdapt; - poDescP->rBufSzCfg = descP->rBufSzCfg; + poDescP->rBufSzAdapt = descP->rBufSz; poDescP->rBufShrinkCnt = 0; poDescP->rNum = descP->rNum; // Inherit buffer uses poDescP->rNumCnt = 0; @@ -3816,7 +3816,9 @@ ERL_NIF_TERM essio_recv(ErlNifEnv* env, int saveErrno; ErlNifBinary bin, *bufP; ssize_t readResult; - size_t bufSz = (len != 0 ? len : descP->rBufSz); // 0 means default + size_t bufSz = (len != 0 ? (size_t) len : // 0 means default + (descP->type == SOCK_STREAM ? + descP->rBufSzAdapt : descP->rBufSz)); ERL_NIF_TERM ret; SSDBG( descP, ("UNIX-ESSIO", "essio_recv {%d} -> entry with" @@ -3860,17 +3862,17 @@ ERL_NIF_TERM essio_recv(ErlNifEnv* env, } /* readResult >= 0 */ - if ((len == 0) && descP->rBufAdapt) { + if ((len == 0) && descP->rBufAdapt && (descP->type == SOCK_STREAM)) { if ((size_t) readResult == bufP->size) { - if (descP->rBufSz < ESSIO_RECV_ADAPT_BUFFER_MAX) - descP->rBufSz <<= 1; + if (descP->rBufSzAdapt < ESSIO_RECV_ADAPT_BUFFER_MAX) + descP->rBufSzAdapt <<= 1; descP->rBufShrinkCnt = 0; - } else if ((descP->rBufSz > descP->rBufSzCfg) && - ((size_t) readResult < (descP->rBufSz >> 2))) { + } else if ((descP->rBufSzAdapt > descP->rBufSz) && + ((size_t) readResult < (descP->rBufSzAdapt >> 2))) { if (++descP->rBufShrinkCnt >= ESSIO_RECV_ADAPT_SHRINK_COUNT) { - descP->rBufSz >>= 1; - if (descP->rBufSz < descP->rBufSzCfg) - descP->rBufSz = descP->rBufSzCfg; + descP->rBufSzAdapt >>= 1; + if (descP->rBufSzAdapt < descP->rBufSz) + descP->rBufSzAdapt = descP->rBufSz; descP->rBufShrinkCnt = 0; } } else { From 50da2f5baa0fce02ffdb12f559988105a1001cf3 Mon Sep 17 00:00:00 2001 From: Louis-Philippe Gauthier Date: Fri, 21 Aug 2026 11:45:49 -0400 Subject: [PATCH 3/5] erts: Shrink the adaptive esock rcvbuf via an EWMA The consecutive-reads counter made shrinking statistically unreachable on mixed traffic: a single read over a quarter of the buffer reset it, so a buffer at the cap could stay there through tens of thousands of smaller reads. Track an EWMA of the read sizes instead (alpha 1/8, shift arithmetic) and halve the buffer whenever the average falls below a quarter of it. Bulk bursts still ramp up unchanged since every filled read doubles the buffer, and once traffic turns small the average converges within a couple dozen reads and the buffer halves per read back to the configured size. An isolated filled read among small ones now also shrinks back instead of ratcheting. --- erts/emulator/nifs/common/prim_socket_int.h | 2 +- erts/emulator/nifs/common/prim_socket_nif.c | 8 +++--- erts/emulator/nifs/unix/unix_socket_syncio.c | 26 +++++++++----------- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/erts/emulator/nifs/common/prim_socket_int.h b/erts/emulator/nifs/common/prim_socket_int.h index cc7f539af281..b7de21f749af 100644 --- a/erts/emulator/nifs/common/prim_socket_int.h +++ b/erts/emulator/nifs/common/prim_socket_int.h @@ -522,7 +522,7 @@ typedef struct { * the size, since it bounds the chunks such a recv may return. */ size_t rBufSzAdapt; // Current adaptive read buffer size - unsigned int rBufShrinkCnt; + size_t rBufSzAvg; // EWMA of the read sizes BOOLEAN_T rBufAdapt; #endif size_t rCtrlSz; // Read control buffer size diff --git a/erts/emulator/nifs/common/prim_socket_nif.c b/erts/emulator/nifs/common/prim_socket_nif.c index e7c3fbc2badb..7316c63170f2 100644 --- a/erts/emulator/nifs/common/prim_socket_nif.c +++ b/erts/emulator/nifs/common/prim_socket_nif.c @@ -8225,9 +8225,9 @@ ERL_NIF_TERM esock_setopt_otp_rcvbuf(ErlNifEnv* env, else descP->rBufSz = bufSz; #ifndef __WIN32__ - descP->rBufAdapt = FALSE; - descP->rBufSzAdapt = descP->rBufSz; - descP->rBufShrinkCnt = 0; + descP->rBufAdapt = FALSE; + descP->rBufSzAdapt = descP->rBufSz; + descP->rBufSzAvg = descP->rBufSz; #endif SSDBG( descP, @@ -17069,7 +17069,7 @@ ESockDescriptor* esock_alloc_descriptor(SOCKET sock) descP->rNumCnt = 0; descP->rBufAdapt = TRUE; descP->rBufSzAdapt = ESOCK_RECV_BUFFER_SIZE_DEFAULT; - descP->rBufShrinkCnt = 0; + descP->rBufSzAvg = ESOCK_RECV_BUFFER_SIZE_DEFAULT; #endif descP->rCtrlSz = ESOCK_RECV_CTRL_BUFFER_SIZE_DEFAULT; descP->wCtrlSz = ESOCK_SEND_CTRL_BUFFER_SIZE_DEFAULT; diff --git a/erts/emulator/nifs/unix/unix_socket_syncio.c b/erts/emulator/nifs/unix/unix_socket_syncio.c index 127fe1f46bbe..7df5150a02bb 100644 --- a/erts/emulator/nifs/unix/unix_socket_syncio.c +++ b/erts/emulator/nifs/unix/unix_socket_syncio.c @@ -211,11 +211,11 @@ #define sock_close(s) close((s)) /* Adaptive read buffer: double on a filled buffer, halve back towards - * the configured size after this many consecutive reads that used - * less than a quarter of it. + * the configured size once an EWMA of the read sizes falls below a + * quarter of it. */ #define ESSIO_RECV_ADAPT_BUFFER_MAX (1 << 20) -#define ESSIO_RECV_ADAPT_SHRINK_COUNT 32 +#define ESSIO_RECV_ADAPT_EWMA_SHIFT 3 // #define sock_close_event(e) /* do nothing */ #define sock_connect(s, addr, len) connect((s), (addr), (len)) #define sock_connectx(s, addrs, acnt, aidp) \ @@ -2850,7 +2850,7 @@ BOOLEAN_T essio_accept_accepted(ErlNifEnv* env, accDescP->rBufSz = descP->rBufSz; // Inherit buffer size accDescP->rBufAdapt = descP->rBufAdapt; accDescP->rBufSzAdapt = descP->rBufSz; - accDescP->rBufShrinkCnt = 0; + accDescP->rBufSzAvg = descP->rBufSz; accDescP->rNum = descP->rNum; // Inherit buffer uses accDescP->rNumCnt = 0; accDescP->rCtrlSz = descP->rCtrlSz; // Inherit buffer size @@ -2956,7 +2956,7 @@ ERL_NIF_TERM essio_peeloff(ErlNifEnv* env, poDescP->rBufSz = descP->rBufSz; // Inherit buffer size poDescP->rBufAdapt = descP->rBufAdapt; poDescP->rBufSzAdapt = descP->rBufSz; - poDescP->rBufShrinkCnt = 0; + poDescP->rBufSzAvg = descP->rBufSz; poDescP->rNum = descP->rNum; // Inherit buffer uses poDescP->rNumCnt = 0; poDescP->rCtrlSz = descP->rCtrlSz; // Inherit buffer size @@ -3863,20 +3863,16 @@ ERL_NIF_TERM essio_recv(ErlNifEnv* env, /* readResult >= 0 */ if ((len == 0) && descP->rBufAdapt && (descP->type == SOCK_STREAM)) { + descP->rBufSzAvg -= descP->rBufSzAvg >> ESSIO_RECV_ADAPT_EWMA_SHIFT; + descP->rBufSzAvg += ((size_t) readResult) >> ESSIO_RECV_ADAPT_EWMA_SHIFT; if ((size_t) readResult == bufP->size) { if (descP->rBufSzAdapt < ESSIO_RECV_ADAPT_BUFFER_MAX) descP->rBufSzAdapt <<= 1; - descP->rBufShrinkCnt = 0; } else if ((descP->rBufSzAdapt > descP->rBufSz) && - ((size_t) readResult < (descP->rBufSzAdapt >> 2))) { - if (++descP->rBufShrinkCnt >= ESSIO_RECV_ADAPT_SHRINK_COUNT) { - descP->rBufSzAdapt >>= 1; - if (descP->rBufSzAdapt < descP->rBufSz) - descP->rBufSzAdapt = descP->rBufSz; - descP->rBufShrinkCnt = 0; - } - } else { - descP->rBufShrinkCnt = 0; + (descP->rBufSzAvg < (descP->rBufSzAdapt >> 2))) { + descP->rBufSzAdapt >>= 1; + if (descP->rBufSzAdapt < descP->rBufSz) + descP->rBufSzAdapt = descP->rBufSz; } } From fbf5e584a5f372ae9cd61bb0ca2fd3c1852a2c66 Mon Sep 17 00:00:00 2001 From: Louis-Philippe Gauthier Date: Fri, 21 Aug 2026 11:48:10 -0400 Subject: [PATCH 4/5] erts: Cap the adaptive esock rcvbuf at 256 KB Draining a local bulk stream on an M2 Pro is flat or slightly better with a 128 KB or 256 KB cap (7.4-7.6 GB/s) than with 1 MB (6.0 GB/s), so past the point where the buffer amortises the syscall the larger memcpy only costs. 256 KB keeps 2x headroom over the measured knee for links with more latency than loopback. --- erts/emulator/nifs/unix/unix_socket_syncio.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erts/emulator/nifs/unix/unix_socket_syncio.c b/erts/emulator/nifs/unix/unix_socket_syncio.c index 7df5150a02bb..858c6c489905 100644 --- a/erts/emulator/nifs/unix/unix_socket_syncio.c +++ b/erts/emulator/nifs/unix/unix_socket_syncio.c @@ -214,7 +214,7 @@ * the configured size once an EWMA of the read sizes falls below a * quarter of it. */ -#define ESSIO_RECV_ADAPT_BUFFER_MAX (1 << 20) +#define ESSIO_RECV_ADAPT_BUFFER_MAX (1 << 18) #define ESSIO_RECV_ADAPT_EWMA_SHIFT 3 // #define sock_close_event(e) /* do nothing */ #define sock_connect(s, addr, len) connect((s), (addr), (len)) From a582f343ae113dd13c0a9631cfed2b95cdba3c64 Mon Sep 17 00:00:00 2001 From: Louis-Philippe Gauthier Date: Fri, 21 Aug 2026 11:59:21 -0400 Subject: [PATCH 5/5] kernel: Test the adaptive esock rcvbuf Covers that a length 0 recv on a bulk stream returns chunks larger than the default buffer, that an explicitly set (otp) rcvbuf still bounds the chunks while getopt reports the configured size, and that dgram sockets keep truncating at the configured size. --- lib/kernel/test/socket_api_SUITE.erl | 133 +++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/lib/kernel/test/socket_api_SUITE.erl b/lib/kernel/test/socket_api_SUITE.erl index fe7caf1932f9..79aaff0b0f12 100644 --- a/lib/kernel/test/socket_api_SUITE.erl +++ b/lib/kernel/test/socket_api_SUITE.erl @@ -200,6 +200,7 @@ api_opt_simple_otp_options/1, api_opt_simple_otp_meta_option/1, api_opt_simple_otp_rcvbuf_option/1, + api_opt_adaptive_otp_rcvbuf_option/1, api_opt_simple_otp_controlling_process/1, api_opt_sock_acceptconn_udp/1, api_opt_sock_acceptconn_tcp/1, @@ -510,6 +511,7 @@ api_options_otp_cases() -> api_opt_simple_otp_options, api_opt_simple_otp_meta_option, api_opt_simple_otp_rcvbuf_option, + api_opt_adaptive_otp_rcvbuf_option, api_opt_simple_otp_controlling_process ]. @@ -12024,6 +12026,137 @@ api_opt_simple_otp_rcvbuf_option() -> +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% The buffer used by a recv with Length = 0 on a stream socket adapts +%% to the traffic, unless the (otp) rcvbuf option has been set, in +%% which case that size is used (and bounds the returned chunks) as +%% before. Dgram sockets read into the configured size as before. +%% Adaptation is not implemented on Windows. +api_opt_adaptive_otp_rcvbuf_option(_Config) when is_list(_Config) -> + ?TT(?SECS(30)), + tc_try(?FUNCTION_NAME, + fun() -> + has_support_ipv4(), + is_not_windows() + end, + fun() -> + api_opt_adaptive_otp_rcvbuf_option() + end). + +api_opt_adaptive_otp_rcvbuf_option() -> + LSA = which_local_socket_addr(inet), + + {ok, L} = socket:open(inet, stream, tcp), + ok = socket:bind(L, LSA#{port => 0}), + ok = socket:listen(L), + {ok, SSA} = socket:sockname(L), + {ok, Default} = socket:getopt(L, otp, rcvbuf), + + i("verify the buffer adapts to bulk traffic (default rcvbuf ~w)", + [Default]), + Bulk = 64 * 1024 * 1024, + Client1 = aor_stream_client(SSA, Bulk), + {ok, S1} = socket:accept(L), + MaxChunk1 = aor_drain(S1, Bulk, 0), + i("max chunk: ~w", [MaxChunk1]), + if + MaxChunk1 > Default -> + ok; + true -> + exit({no_adaptation, MaxChunk1, Default}) + end, + %% The adapted size is internal; getopt reports the configured size + {ok, Default} = socket:getopt(S1, otp, rcvbuf), + aor_stop_client(Client1), + _ = socket:close(S1), + + i("verify an explicitly set rcvbuf bounds the chunks"), + Pinned = 2048, + Bulk2 = 8 * 1024 * 1024, + Client2 = aor_stream_client(SSA, Bulk2), + {ok, S2} = socket:accept(L), + ok = socket:setopt(S2, otp, rcvbuf, Pinned), + MaxChunk2 = aor_drain(S2, Bulk2, 0), + i("max chunk: ~w", [MaxChunk2]), + if + MaxChunk2 =< Pinned -> + ok; + true -> + exit({not_pinned, MaxChunk2, Pinned}) + end, + aor_stop_client(Client2), + _ = socket:close(S2), + _ = socket:close(L), + + i("verify a dgram socket does not adapt"), + {ok, U} = socket:open(inet, dgram, udp), + ok = socket:bind(U, LSA#{port => 0}), + {ok, USA} = socket:sockname(U), + {ok, C} = socket:open(inet, dgram, udp), + ok = socket:setopt(C, socket, sndbuf, 64 * 1024), + %% Datagrams that exactly fill the buffer would grow it if + %% adaptation was (wrongly) applied to dgram sockets + Fill = binary:copy(<<$x>>, Default), + Send = fun(Data) -> + case socket:sendto(C, Data, USA) of + ok -> ok; + {error, emsgsize} -> skip("dgram size not supported") + end + end, + [begin + ok = Send(Fill), + {ok, D} = socket:recv(U, 0, ?SECS(5)), + Default = byte_size(D) + end || _ <- lists:seq(1, 8)], + %% An oversized datagram is still truncated at the configured size + ok = Send(binary:copy(<<$y>>, Default + 4096)), + {ok, T} = socket:recv(U, 0, ?SECS(5)), + i("oversized dgram read back as ~w bytes", [byte_size(T)]), + Default = byte_size(T), + _ = socket:close(C), + _ = socket:close(U), + ok. + +aor_stream_client(SSA, Bytes) -> + Self = self(), + spawn_monitor( + fun() -> + {ok, S} = socket:open(inet, stream, tcp), + ok = socket:connect(S, SSA), + Chunk = binary:copy(<<$x>>, 1024 * 1024), + aor_send(S, Chunk, Bytes), + receive + {Self, stop} -> + _ = socket:close(S), + exit(normal) + end + end). + +aor_send(_S, _Chunk, Bytes) when Bytes =< 0 -> + ok; +aor_send(S, Chunk, Bytes) -> + ok = socket:send(S, Chunk), + aor_send(S, Chunk, Bytes - byte_size(Chunk)). + +aor_drain(_S, Bytes, MaxChunk) when Bytes =< 0 -> + MaxChunk; +aor_drain(S, Bytes, MaxChunk) -> + {ok, Data} = socket:recv(S, 0, ?SECS(10)), + Sz = byte_size(Data), + aor_drain(S, Bytes - Sz, max(Sz, MaxChunk)). + +aor_stop_client({Pid, MRef}) -> + Pid ! {self(), stop}, + receive + {'DOWN', MRef, process, Pid, normal} -> + ok; + {'DOWN', MRef, process, Pid, Reason} -> + exit({client, Reason}) + end. + + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Perform some simple getopt and setopt with the level = otp options