[PATCH 0/2] MR11272: server: Report a stream socket writable while its send buffer has room.
Previously floated as an RFC on wine-devel (https://list.winehq.org/hyperkitty/list/wine-devel@list.winehq.org/thread/HV...); positive response, no objections, opening as an MR now. Problem. Windows select() reports a connected stream socket writable whenever send() would still accept data. Wine follows the host poll(), and on Linux POLLOUT is only raised once the send queue drains below ~2/3 of SO_SNDBUF. An app-limited sender that does a select() writability check before arming FD_WRITE (libcurl's multi loop, and anything built on it) sees "not writable" while its sends keep succeeding, and waits out its full poll timeout (~1s) between bursts, throttling single-stream uploads to ~140 KB/s. Found with a Backblaze B2 client. Fix. In poll_socket(), report a connected stream socket writable when TIOCOUTQ < SO_SNDBUF. Guarded by #ifdef TIOCOUTQ so behaviour is unchanged where it is unavailable. Why this is correct for Wine. It reproduces what the application observes on real Windows rather than fixing a Windows-side defect: native Windows reports the socket writable and runs the same client at full speed, while the Linux POLLOUT threshold is a host artefact invisible to the Windows application. Validated against real Windows and against Wine on a Linux guest. Test. dlls/ws2_32/tests adds a conformance test for the writability invariant marked todo_wine; the fix commit removes the todo. It counts invariant violations and asserts zero rather than asserting timing or throughput, to stay robust across CI configurations. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=59893 -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11272
From: Martyn Forryan <winehq-gitlab@forryan.co.uk> Fill a non-blocking, never-drained loopback socket and check the Windows invariant that select() reports the socket writable exactly while send() still accepts data. The assertion is marked todo_wine; it is cleared by the following fix. Wine-Bug: https://bugs.winehq.org/show_bug.cgi\?id\=59893 --- dlls/ws2_32/tests/sock.c | 62 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/dlls/ws2_32/tests/sock.c b/dlls/ws2_32/tests/sock.c index e67e50907b0..a4424d6d51d 100644 --- a/dlls/ws2_32/tests/sock.c +++ b/dlls/ws2_32/tests/sock.c @@ -5,6 +5,7 @@ * Copyright 2005 Thomas Kho * Copyright 2008 Jeff Zaroyko * Copyright 2017 Dmitry Timoshkov + * Copyright 2026 Martyn Forryan * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -6358,6 +6359,65 @@ static DWORD WINAPI drain_socket_thread(LPVOID arg) return 0; } + +/* Windows reports a stream socket writable whenever send() can still accept data. + * Wine used to follow Linux poll() behaviour, which withholds POLLOUT once the + * send queue is above roughly 2/3 of SO_SNDBUF; check that invariant here. */ +static void test_send_writability(void) +{ + static const char buffer[4096]; + SOCKET client = INVALID_SOCKET, server = INVALID_SOCKET; + unsigned int total = 0, violations = 0; + BOOL filled = FALSE; + int ret, err; + + tcp_socketpair(&client, &server); + + ret = set_blocking(client, FALSE); + ok(!ret, "failed to make socket nonblocking, error %u\n", WSAGetLastError()); + + while (total < 128 * 1024 * 1024) + { + struct timeval timeout = {0}; + fd_set writefds; + BOOL writable; + + FD_ZERO(&writefds); + FD_SET(client, &writefds); + + ret = select(client + 1, NULL, &writefds, NULL, &timeout); + ok(ret != SOCKET_ERROR, "select failed, error %u\n", WSAGetLastError()); + if (ret == SOCKET_ERROR) + break; + + writable = FD_ISSET(client, &writefds); + + ret = send(client, buffer, sizeof(buffer), 0); + if (ret == SOCKET_ERROR) + { + err = WSAGetLastError(); + if (err == WSAEWOULDBLOCK) + { + filled = TRUE; + break; + } + + ok(0, "send failed, error %u\n", err); + break; + } + + if (!writable) + violations++; + total += ret; + } + + ok(filled, "send buffer was not filled after %u bytes\n", total); + todo_wine ok(!violations, "select/send writability invariant was violated %u times\n", violations); + + closesocket(client); + closesocket(server); +} + static void test_send(void) { SOCKET src = INVALID_SOCKET; @@ -14960,6 +15020,8 @@ START_TEST( sock ) Init(); + test_send_writability(); + test_set_getsockopt(); test_reuseaddr(); test_ip_pktinfo(); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/11272
From: Martyn Forryan <winehq-gitlab@forryan.co.uk> Windows select() reports a connected stream socket writable whenever send() would still accept data. Wine follows the host poll(); on Linux POLLOUT is only set once the send queue drains below ~2/3 of SO_SNDBUF. An application that performs a select() writability check before waiting on FD_WRITE - for example libcurl's multi event loop, and therefore anything built on it - then sees the socket as not writable while its sends keep succeeding, and waits out its full poll timeout (typically 1s) between bursts, throttling single-stream uploads to ~140 KB/s. Report a connected stream socket writable in poll_socket() when its send buffer still has room (TIOCOUTQ < SO_SNDBUF), matching Windows. Guard with #ifdef TIOCOUTQ so behaviour is unchanged where it is unavailable. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=59893 --- dlls/ws2_32/tests/sock.c | 2 +- server/sock.c | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/dlls/ws2_32/tests/sock.c b/dlls/ws2_32/tests/sock.c index a4424d6d51d..d31d6d1e5eb 100644 --- a/dlls/ws2_32/tests/sock.c +++ b/dlls/ws2_32/tests/sock.c @@ -6412,7 +6412,7 @@ static void test_send_writability(void) } ok(filled, "send buffer was not filled after %u bytes\n", total); - todo_wine ok(!violations, "select/send writability invariant was violated %u times\n", violations); + ok(!violations, "select/send writability invariant was violated %u times\n", violations); closesocket(client); closesocket(server); diff --git a/server/sock.c b/server/sock.c index fdc26bc2276..c4c3ed63edf 100644 --- a/server/sock.c +++ b/server/sock.c @@ -2,6 +2,7 @@ * Server-side socket management * * Copyright (C) 1999 Marcus Meissner, Ove Kåven + * Copyright (C) 2026 Martyn Forryan * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -3652,7 +3653,23 @@ static void poll_socket( struct sock *poll_sock, struct async *async, int exclus pollfd.fd = get_unix_fd( sock->fd ); pollfd.events = poll_flags_from_afd( sock, mask ); if (pollfd.events >= 0 && poll( &pollfd, 1, 0 ) >= 0) + { +#ifdef TIOCOUTQ + int outq = 0, sndbuf = 0; + socklen_t len = sizeof(sndbuf); + + /* Linux withholds POLLOUT until the send queue has drained well below + * SO_SNDBUF, while Windows reports a stream socket writable whenever + * send() can still accept data. If there is any send-buffer space + * left, report writability here to match Windows semantics. */ + if ((mask & AFD_POLL_WRITE) && !(pollfd.revents & (POLLOUT | POLLERR | POLLHUP)) && + sock->type == WS_SOCK_STREAM && sock->state == SOCK_CONNECTED && !sock->wr_shutdown && + !ioctl( pollfd.fd, TIOCOUTQ, &outq ) && + !getsockopt( pollfd.fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, &len ) && outq < sndbuf) + pollfd.revents |= POLLOUT; +#endif sock_poll_event( sock->fd, pollfd.revents ); + } /* FIXME: do other error conditions deserve a similar treatment? */ if (sock->state != SOCK_CONNECTING && sock->errors[AFD_POLL_BIT_CONNECT_ERR] && (mask & AFD_POLL_CONNECT_ERR)) -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/11272
@vibhavp @gofman - you've both worked in `server/sock.c` and the ws2_32 socket tests recently, so flagging this in case either of you has a moment to take a look. Short version: it reports a connected stream socket writable in `poll_socket()` while its send buffer still has room (`TIOCOUTQ < SO_SNDBUF`), to match Windows `select()`. Without it, an app-limited libcurl-style sender that never trips `WSAEWOULDBLOCK` sees the socket as not writable while its sends keep succeeding and waits out the poll timeout, throttling single-stream uploads to \~140 KB/s under Wine. It was floated as an RFC on wine-devel first; the root cause, the Windows-semantics-not-a-Windows-fix reasoning, and the test (a `todo_wine` flip) are all in the description. No urgency, and thanks either way. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11272#note_144361
Not sure I have a final suggestion at once, that could possibly use a bit of exploration (or a bit of more info on possibilities if some exploration has already been done). Does libcurl sender use synchronous or asynchronous socket writes? I think at very least the added condition is missing async_queue_has_waiting_asyncs( &sock->write_q ) check. But my initial thoughts that maybe we can consider a different way. Mind the following facts: 1. Windows never does short writes (regardless of the attempt size attempts, even if it is GBs, and regardless of SO_SNDBUG). We have partial support for that, see ntdll.dll/unix/sock.c:sock_send() (after "If we had a short write..." comment. IIRC we stomped upon the fact that sync sends also behave the same on Windows and that should probably be tested and implemented (that should likely be as easy as altering the condition after the said comment). 2. The suggested implementation will only satisfy select() when it is called when there is some free space in buffer but if it was already waiting and some space has been freed the select() won't be woken. Probably not a blocker per se, but maybe we can do better. In the view of 1, I am thinking that maybe instead of trying to tweak around Linux native buffer sizes and logic we can extend a bit the logic of our big buffer writeback (only for stream sockets), like: - always allow sending if there is no pending async writes (now we will allow only if partial write happened); - give up on Unix socket write polling entirely and satisfy WINAPI select / poll based on the absence of queued writes (of course we should also signal already waiting select once async write queue is empty). @zfigura any thoughts on that? -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11272#note_144439
I think at very least the added condition is missing async_queue_has_waiting_asyncs( &sock-\>write_q ) check.
That's handled by sock_dispatch_asyncs(), which clears POLLOUT etc when satisfying a waiting async.
Windows never does short writes (regardless of the attempt size attempts, even if it is GBs, and regardless of SO_SNDBUG). We have partial support for that, see ntdll.dll/unix/sock.c:sock_send() (after "If we had a short write..." comment. IIRC we stomped upon the fact that sync sends also behave the same on Windows and that should probably be tested and implemented (that should likely be as easy as altering the condition after the said comment).
I think this is an orthogonal problem?
The suggested implementation will only satisfy select() when it is called when there is some free space in buffer but if it was already waiting and some space has been freed the select() won't be woken. Probably not a blocker per se, but maybe we can do better.
That occurred to me, but I'm not sure there's a way to, without kernel modification. Maybe that's warranted here? Depending on the loop there still might be throttling. In the view of 1, I am thinking that maybe instead of trying to tweak around Linux native buffer sizes and logic we can extend a bit the logic of our big buffer writeback (only for stream sockets), like:
* always allow sending if there is no pending async writes (now we will allow only if partial write happened); * give up on Unix socket write polling entirely and satisfy WINAPI select / poll based on the absence of queued writes (of course we should also signal already waiting select once async write queue is empty).
I don't think that's right or a good idea. Windows does have a buffer limit, and letting writes queue forever is probably a bad idea. ``` + /* Linux withholds POLLOUT until the send queue has drained well below + * SO_SNDBUF, while Windows reports a stream socket writable whenever + * send() can still accept data. If there is any send-buffer space + * left, report writability here to match Windows semantics. */ ``` Do we know whether this behaviour is specific to Linux? Should we specifically guard for Linux, not just TIOCOUTQ? ``` + sock->type == WS_SOCK_STREAM && sock->state == SOCK_CONNECTED && !sock->wr_shutdown && ``` SOCK_CONNECTED should imply SOCK_STREAM. But what about UDP sockets? -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11272#note_144443
* always allow sending if there is no pending async writes (now we will allow only if partial write happened); * give up on Unix socket write polling entirely and satisfy WINAPI select / poll based on the absence of queued writes (of course we should also signal already waiting select once async write queue is empty).
I don't think that's right or a good idea. Windows does have a buffer limit, and letting writes queue forever is probably a bad idea.
This should not queue forever of course. Once there is a buffer in-flight (queued async) the next select would not advertise write availability (and 0 size Unix socket write should not queue another buffer). -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11272#note_144444
This should not queue forever of course. Once there is a buffer in-flight (queued async) the next select would not advertise write availability (and 0 size Unix socket write should not queue another buffer).
Right, I can't think properly. I suppose this would indeed work. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11272#note_144445
Thanks both. Happy to go either way on the design. Taking the specific points first. On sync vs async (Paul): libcurl uses synchronous non-blocking `send()`, not overlapped writes. I checked current master to be sure: `swrite()` maps straight to `send()` (lib/curl_setup.h), the socket is put in non-blocking mode in `cf_socket_open()` (`curlx_nonblock` plus `SOCK_NONBLOCK`), and `cf_socket_send()` calls `swrite()` and returns `CURLE_AGAIN` on `EWOULDBLOCK`. The multi loop polls the socket for writability and only retries the send once it sees `POLLOUT`. So the stall is the writability check reporting "not writable" while the send buffer still has room, with curl waiting out its poll timeout before retrying a `send()` that would have succeeded. For `async_queue_has_waiting_asyncs()`, agreed with Elizabeth: the synthesized `POLLOUT` goes through `sock_dispatch_asyncs()`, which consumes it to wake a waiting write async before it reaches `select`, so a pending overlapped send is not starved. On guarding for Linux (Elizabeth): the block only runs when the OS withheld `POLLOUT` (`!(revents & POLLOUT)`) while the buffer has room, so it is self-limiting rather than Linux-specific. Where `poll()` already reports writability at the low-water mark (the BSDs and macOS, at `SO_SNDLOWAT`), `POLLOUT` is already set and the block is skipped. That is why I gated on `TIOCOUTQ` rather than `__linux__`. I can note that in the comment. For UDP and the `WS_SOCK_STREAM` check (Elizabeth): a connected UDP socket stays `SOCK_CONNECTIONLESS`, since `connect` only moves to `SOCK_CONNECTED` for non-`DGRAM` types, so `SOCK_CONNECTED` already rules out UDP. I kept the explicit `WS_SOCK_STREAM` to also rule out a connected raw socket, where the `TIOCOUTQ < SO_SNDBUF` reasoning does not hold. Glad to drop it and rely on the state if you would rather. On the larger question: you are right that a poll-time check will not re-wake a `select` already blocked when space frees later, and that driving writability off the write queue is cleaner and closes that gap. For the app-limited synchronous-send loops this targets, curl being the case in front of me, the writability test happens at poll time, so this restores the fast path without that gap biting in practice. That is why I sent it as the minimal change. I am not attached to it. If you would prefer the write_q approach (advertise writable on an empty queue, queue the send, and re-signal waiters on completion, extending the short-write path in `sock_send()`), I am glad to write it. Since that reworks the core send and flow-control path, I would welcome a steer on the shape you have in mind, or I will defer if one of you would rather own it. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11272#note_144505
On guarding for Linux (Elizabeth): the block only runs when the OS withheld `POLLOUT` (`!(revents & POLLOUT)`) while the buffer has room, so it is self-limiting rather than Linux-specific. Where `poll()` already reports writability at the low-water mark (the BSDs and macOS, at `SO_SNDLOWAT`), `POLLOUT` is already set and the block is skipped. That is why I gated on `TIOCOUTQ` rather than `__linux__`. I can note that in the comment.
Right, again I can't think >_>
For UDP and the `WS_SOCK_STREAM` check (Elizabeth): a connected UDP socket stays `SOCK_CONNECTIONLESS`, since `connect` only moves to `SOCK_CONNECTED` for non-`DGRAM` types, so `SOCK_CONNECTED` already rules out UDP.
I mean, why are we guarding for SOCK_STREAM in the first place? Why shouldn't this also apply to other socket types?
On the larger question: you are right that a poll-time check will not re-wake a `select` already blocked when space frees later, and that driving writability off the write queue is cleaner and closes that gap. For the app-limited synchronous-send loops this targets, curl being the case in front of me, the writability test happens at poll time, so this restores the fast path without that gap biting in practice. That is why I sent it as the minimal change. I am not attached to it. If you would prefer the write_q approach (advertise writable on an empty queue, queue the send, and re-signal waiters on completion, extending the short-write path in `sock_send()`), I am glad to write it. Since that reworks the core send and flow-control path, I would welcome a steer on the shape you have in mind, or I will defer if one of you would rather own it.
Ultimately it doesn't fix the discrepancy, because our ability to actually *send* from the queue is still going to be throttled by POLLOUT. So I don't think there's a reason to make that change. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11272#note_144857
I think waiting for POLLOUT and immediately sent as soon as it is available, even if Linux signals POLLOUT when some fraction of buffer is already sent, should surely work fine, it is one of the supposed ways to transfer bulk data and should not be the subject for slowdown. As I understand the problem with curl is that it will check for write once, see it is not ready (while there is some 1/3 of send buffer available) and then next time try to send way too late so the speed is low (or if it is not the case, what is the actual problem then, do we understand it?). That should not be the case with async buffer send, there is no benefit of async sending early at 1/3 buffer compared to sending later; in fact, the opposite, we are better off avoiding extra async server roundtrips by writing socket in bigger chunks. It is not like I am completely opposed to originally suggested approach, it is just that it seems to rely on some internal Linux sockets functioning (maybe also soubject to setup / some tunables??) and thus potentially the point of different behaviour across systems. While also not making things work just like Windows. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11272#note_144858
On Sat Jul 4 23:21:37 2026 +0000, Elizabeth Figura wrote:
On guarding for Linux (Elizabeth): the block only runs when the OS withheld `POLLOUT` (`!(revents & POLLOUT)`) while the buffer has room, so it is self-limiting rather than Linux-specific. Where `poll()` already reports writability at the low-water mark (the BSDs and macOS, at `SO_SNDLOWAT`), `POLLOUT` is already set and the block is skipped. That is why I gated on `TIOCOUTQ` rather than `__linux__`. I can note that in the comment. Right, again I can't think >_> For UDP and the `WS_SOCK_STREAM` check (Elizabeth): a connected UDP socket stays `SOCK_CONNECTIONLESS`, since `connect` only moves to `SOCK_CONNECTED` for non-`DGRAM` types, so `SOCK_CONNECTED` already rules out UDP. I mean, why are we guarding for SOCK_STREAM in the first place? Why shouldn't this also apply to other socket types? On the larger question: you are right that a poll-time check will not re-wake a `select` already blocked when space frees later, and that driving writability off the write queue is cleaner and closes that gap. For the app-limited synchronous-send loops this targets, curl being the case in front of me, the writability test happens at poll time, so this restores the fast path without that gap biting in practice. That is why I sent it as the minimal change. I am not attached to it. If you would prefer the write_q approach (advertise writable on an empty queue, queue the send, and re-signal waiters on completion, extending the short-write path in `sock_send()`), I am glad to write it. Since that reworks the core send and flow-control path, I would welcome a steer on the shape you have in mind, or I will defer if one of you would rather own it. Ultimately it doesn't fix the discrepancy, because our ability to actually *send* from the queue is still going to be throttled by POLLOUT. So I don't think there's a reason to make that change. There’s no fundamental reason, just scope. Stream sockets are what I was able to test and validate end-to-end.
The Windows behaviour should apply to datagram sockets as well, and Linux under-reports writability for them for the same underlying reason: `sock_writeable()` only signals `POLLOUT` once the allocated send memory drops below half of `SO_SNDBUF`. On that basis, widening the change seems reasonable. If you’d prefer, I’m happy to remove the type check, relax the state gating so connected datagram sockets are covered too, and extend the `ws2_32` test with a UDP case to lock in the behaviour. Otherwise, I’d keep this MR limited to the tested scope and do the wider change as a follow-up with its own test. Your call. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11272#note_144864
On Sat Jul 4 23:45:20 2026 +0000, Paul Gofman wrote:
I think waiting for POLLOUT and immediately sent as soon as it is available, even if Linux signals POLLOUT when some fraction of buffer is already sent, should surely work fine, it is one of the supposed ways to transfer bulk data and should not be the subject for slowdown. As I understand the problem with curl is that it will check for write once, see it is not ready (while there is some 1/3 of send buffer available) and then next time try to send way too late so the speed is low (or if it is not the case, what is the actual problem then, do we understand it?). That should not be the case with async buffer send, there is no benefit of async sending early at 1/3 buffer compared to sending later; in fact, the opposite, we are better off avoiding extra async server roundtrips by writing socket in bigger chunks. It is not like I am completely opposed to originally suggested approach, it is just that it seems to rely on some internal Linux sockets functioning (maybe also soubject to setup / some tunables??) and thus potentially the point of different behaviour across systems. While also not making things work just like Windows. I think the missing piece is that `curl` never uses asynchronous or overlapped sends, so the async buffer path is never involved. Its loop is simply: a synchronous non-blocking `send()`, preceded by a `select()` writability check, with `WSAEventSelect(FD_WRITE)` and `WSAWaitForMultipleEvents()` (1000 ms timeout) as the fallback if `select()` reports the socket as not writable.
On stock Wine, each \~64 KB burst goes like this: * `curl` checks writability with `select()`. The send buffer still has space because the sender is application-limited and never fills it, but Linux only reports `POLLOUT` once the queued data drops below its low-water threshold. Wine therefore reports the socket as not writable. * `curl` falls back to waiting for `FD_WRITE`. That event is only signalled after a `send()` fails with `WSAEWOULDBLOCK`, and that never happens here because the send buffer always has room. I verified this re-arm behaviour on Windows and it matches the MSDN documentation, so Wine is already behaving correctly on that side. * The wait therefore expires after the full 1000 ms. `curl` retries `select()`, sends another burst, and immediately falls back into the same wait. So the problem isn’t that sends happen slightly later than they should. It’s a fixed one-second delay for every \~64 KB burst, which matches the roughly 140 KB/s throughput users report. Under the current Wine behaviour there’s simply no wake-up path that can end that wait early. The discrepancy is in `select()`: on Windows, the pre-check reports the socket as writable whenever a `send()` would accept data, so `curl` never enters the wait in the first place. On the point about relying on Linux internals, I’d actually argue it’s the opposite. The `outq < sndbuf` check is a direct implementation of the Windows contract, namely that a socket is writable if a `send()` would accept at least one byte. It derives that from raw byte counts (`TIOCOUTQ` for queued bytes and `SO_SNDBUF` for the send buffer capacity), rather than relying on Linux’s `poll()` thresholds. That also makes it independent of tunables such as `TCP_NOTSENT_LOWAT`, which affect when `POLLOUT` is reported but not what `TIOCOUTQ` returns. As for whether it behaves like Windows, that’s exactly what the conformance test in this MR checks. It asserts the invariant that “`select()` reports writable if and only if `send()` would accept data”. Running the same test binary gives 0 violations on Windows, about 230 violations out of roughly 660 sends on unpatched Wine (where `select()` reports “not writable” even though `send()` succeeds), and 0 violations on patched Wine. So for the behaviour the test covers, the patch demonstrably brings Wine into line with Windows. On platforms where `TIOCOUTQ` isn’t available, the code is compiled out and the existing behaviour is unchanged. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11272#note_144865
I think the missing piece is that `curl` never uses asynchronous or overlapped sends,
No, it is not missed. Yet maybe it is just me but I don't actually understand from this description how the suggested checks helps exactly and what curl does:
So the problem isn’t that sends happen slightly later than they should. It’s a fixed one-second delay for every \~64 KB burst, which matches the roughly 140 KB/s throughput users report. Under the current Wine behaviour there’s simply no wake-up path that can end that wait early. The discrepancy is in `select()`: on Windows, the pre-check reports the socket as writable whenever a `send()` would accept data, so `curl` never enters the wait in the first place.
I don't yet understand how that works on Windows vs Wine exactly. It can't be just pushing data in a tight loop infinitely, it should wait somewhere? Or does it end up on Windows somehow not sleeping on anything at all and just trying send in a tight loop? It is not clear yet to me where is that 1 sec timeoout. If you understand all that maybe it would be easier to write some few lines of [pseudo]code from which it would be clear how it is supposed to wait for send ready without sleeping for 1 sec but yet waiting for send somehow. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11272#note_144875
On Sun Jul 5 06:08:28 2026 +0000, Paul Gofman wrote:
I think the missing piece is that `curl` never uses asynchronous or overlapped sends, No, it is not missed. Yet maybe it is just me but I don't actually understand from this description how the suggested checks helps exactly and what curl does: So the problem isn’t that sends happen slightly later than they should. It’s a fixed one-second delay for every \~64 KB burst, which matches the roughly 140 KB/s throughput users report. Under the current Wine behaviour there’s simply no wake-up path that can end that wait early. The discrepancy is in `select()`: on Windows, the pre-check reports the socket as writable whenever a `send()` would accept data, so `curl` never enters the wait in the first place. I don't yet understand how that works on Windows vs Wine exactly. It can't be just pushing data in a tight loop infinitely, it should wait somewhere? Or does it end up on Windows somehow not sleeping on anything at all and just trying send in a tight loop? It is not clear yet to me where is that 1 sec timeoout. If you understand all that maybe it would be easier to write some few lines of [pseudo]code from which it would be clear how it is supposed to wait for send ready without sleeping for 1 sec but yet waiting for send somehow. Fair point. Pseudocode makes it much easier to see what’s going on. This is essentially `curl`’s transfer loop (one multi-loop iteration per burst, simplified):
``` while (data remains) { /* readiness check for this iteration */ if (select(socket, writefds) says writable) { n = send(socket, next_chunk, ~64K); /* non-blocking */ if (n == SOCKET_ERROR && WSAEWOULDBLOCK) { /* buffer genuinely full: FD_WRITE is now armed */ WSAWaitForMultipleEvents(event, timeout=1000ms); /* wakes on FD_WRITE */ } /* else: sent, produce next chunk, loop */ } else { /* select says not writable: wait for the socket to become writable */ WSAWaitForMultipleEvents(event, timeout=1000ms); } } ``` The one-second delay comes from `curl`’s multi-loop poll timeout in `WSAWaitForMultipleEvents()`, using `WSAEventSelect(sock, event, FD_WRITE | ...)`. The key point is that `FD_WRITE` is edge-triggered. As we established earlier in the thread, and as I confirmed on Windows, it is only signalled after a `send()` fails with `WSAEWOULDBLOCK`. That means the wait is only useful if you enter it because a send has just failed, since that’s the only situation where the event is armed. On Windows, `select()` guarantees exactly that. It reports the socket as writable whenever a `send()` would accept data, so `curl` only reaches the wait through the `WSAEWOULDBLOCK` path. At that point, the send buffer is genuinely full, `FD_WRITE` is armed, and the wait is woken as soon as ACKs free enough space. There’s no busy loop or unnecessary sleeping: while the socket can accept data, `curl` simply alternates between `select()` and `send()`, paced by its own data production, and only blocks when the buffer is actually full. On current Wine, things diverge. This sender is application-limited, so its \~64 KB bursts never fill the send buffer. As a result, no `send()` ever returns `WSAEWOULDBLOCK`, which means `FD_WRITE` is never armed. However, `select()` reports the socket as not writable whenever the queued data is above Linux’s `POLLOUT` threshold, which is true for much of the transfer. `curl` therefore takes the `else` branch and waits in a state where the event can never fire. Nothing wakes the wait early, so it always sleeps for the full second, retries, sends one burst, and immediately falls back into the same dead wait. One burst per second is exactly the roughly 140 KB/s throughput users observe. The patch closes that gap. Once `select()` reports the socket as writable whenever the send buffer still has room, matching Windows semantics, `curl` stays on the send path exactly as it does on Windows. It only enters the wait after a `send()` has actually failed with `WSAEWOULDBLOCK`, which is the only time that wait is capable of waking promptly. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/11272#note_145012
participants (4)
-
Elizabeth Figura (@zfigura) -
Martyn Forryan -
Martyn Forryan (@foz) -
Paul Gofman (@gofman)