[RFC] server: report a stream socket writable while its send buffer has room
Hi all, Before I open a merge request, I’d like feedback on the approach, as it has a portability angle I’m unsure about. Question up front: the fix relies on TIOCOUTQ to query the amount of unsent data in a socket’s send buffer. That is Linux-specific. Is gating it on #ifdef TIOCOUTQ (leaving current behaviour unchanged elsewhere) acceptable, or would you prefer a different mechanism, or a different layer than poll_socket()? I’d rather hear that now than in review. On Windows, select() reports a connected stream socket writable whenever a send() would still accept data into the send buffer. Wine derives writability from the host poll(); on Linux POLLOUT is only set once the send queue has drained below roughly 2/3 of SO_SNDBUF. So while the buffer is between ~2/3 full and full, Windows says “writable” but Wine says “not writable” even though send() still succeeds. The common application pattern is a non-blocking select() writability check before waiting on an FD_WRITE event. libcurl’s multi loop does exactly this. Under Wine, an app-limited sender (sends keep succeeding, never hitting WSAEWOULDBLOCK) sees “not writable” while it still has room, and since FD_WRITE is edge-triggered and not re-recorded until a send fails, it waits out its full poll timeout (typically 1s) between bursts. Single-stream uploads end up throttled to ~140 KB/s. I found this with a libcurl-based backup client whose uploads could not finish within their timeout under Wine. Proposed fix: In poll_socket(), when the immediate poll() does not report POLLOUT for a connected stream socket that still has send-buffer room (TIOCOUTQ < SO_SNDBUF), report it writable. One small block, #ifdef TIOCOUTQ. Conformance: I have a ws2_32:sock test (the invariant: select() reports writable exactly while send() accepts data). On the same test binary: * real Windows pass (0 violations) * Wine, unpatched fail (~230 of ~660 sends report not-writable yet send ok) * Wine, patched pass (0 violations) I’ll submit the test as a todo_wine commit ahead of the fix. One thing I tried and dropped: I first also changed FD_WRITE to re-arm on every send. A conformance test on real Windows showed Windows does NOT re-arm FD_WRITE after a successful send (only after WSAEWOULDBLOCK, matching MSDN), so that change would have diverged from Windows. The writability fix alone is sufficient and is the Windows-conforming part. For background, curl’s issue #6146 (https://github.com/curl/curl/issues/6146) documents this same select/FD_WRITE behaviour on Windows; curl’s workaround is the select()-before-wait pre-check in commit a69d1a4 (PR #5634, “multi: implement wait using winsock events”). That pre-check is exactly the path this fix unblocks under Wine. Bug: https://bugs.winehq.org/show_bug.cgi?id=59893 Happy to adjust the mechanism based on what you’d prefer here. Thanks, Martyn Forryan
On Friday, 19 June 2026 22:56:09 CDT wine-devel--- via Wine-Devel wrote:
Hi all,
Before I open a merge request, I’d like feedback on the approach, as it has a portability angle I’m unsure about.
Question up front: the fix relies on TIOCOUTQ to query the amount of unsent data in a socket’s send buffer. That is Linux-specific. Is gating it on #ifdef TIOCOUTQ (leaving current behaviour unchanged elsewhere) acceptable, or would you prefer a different mechanism, or a different layer than poll_socket()? I’d rather hear that now than in review.
On Windows, select() reports a connected stream socket writable whenever a send() would still accept data into the send buffer. Wine derives writability from the host poll(); on Linux POLLOUT is only set once the send queue has drained below roughly 2/3 of SO_SNDBUF. So while the buffer is between ~2/3 full and full, Windows says “writable” but Wine says “not writable” even though send() still succeeds.
The common application pattern is a non-blocking select() writability check before waiting on an FD_WRITE event. libcurl’s multi loop does exactly this. Under Wine, an app-limited sender (sends keep succeeding, never hitting WSAEWOULDBLOCK) sees “not writable” while it still has room, and since FD_WRITE is edge-triggered and not re-recorded until a send fails, it waits out its full poll timeout (typically 1s) between bursts. Single-stream uploads end up throttled to ~140 KB/s. I found this with a libcurl-based backup client whose uploads could not finish within their timeout under Wine.
Proposed fix: In poll_socket(), when the immediate poll() does not report POLLOUT for a connected stream socket that still has send-buffer room (TIOCOUTQ < SO_SNDBUF), report it writable. One small block, #ifdef TIOCOUTQ.
This seems reasonable to me, probably, though I have to ask, what does curl do on linux? Does it adjust SNDBUF to be bigger? --Zeb
Good question. Short answer: on Linux, curl doesn't grow SO_SNDBUF itself; it leaves the send buffer to the kernel's autotuning. It's on Windows that libcurl actively grows it: because Windows doesn't autotune the send buffer the same way, libcurl periodically queries the ideal send backlog (SIO_IDEAL_SEND_BACKLOG_QUERY) and sets SO_SNDBUF from that. (That came out of curl's "Upload speed in Windows may be diminished", issue #2224.) But I don't think buffer size is the actual variable here; the stall is about writability reporting, and it reproduces independently of SNDBUF tuning. That's what the conformance test isolates: pure ws2_32 sockets, no curl, default buffers, just the invariant "select() reports writable iff send() would still accept data." Windows holds that invariant; unmodified Wine breaks it once the send queue passes ~2/3 of SO_SNDBUF (the Linux POLLOUT threshold), reporting not-writable while sends keep succeeding. Growing SNDBUF only moves that boundary to a larger absolute size; it doesn't close the gap. As for why native Linux curl never hits it: its multi loop waits with poll(), which is level-triggered, so it's woken the moment the socket is writable again, the Linux threshold just means it wakes slightly later, not that it stalls. The path that stalls is the Windows backend, which waits on WSAEventSelect/FD_WRITE (edge-triggered) behind a select() writability pre-check (curl #6146). Under Wine, that pre-check returns the wrong answer, so curl falls through to a full ~1s WSAWaitForMultipleEvents per burst. Making Wine's select() match Windows is what lets curl's existing pre-check work as designed. (Happy to also check whether Wine even implements SIO_IDEAL_SEND_BACKLOG_QUERY. If it doesn't, curl can't grow the buffer under Wine at all, but that'd be an orthogonal gap; the reporting fix stands either way, per the test.) Martyn
participantes (2)
-
Elizabeth Figura -
wine-devel@forryan.co.uk