Loading src/quic.cpp +31 −10 Original line number Diff line number Diff line Loading @@ -4502,7 +4502,7 @@ size_t quic::recvData(buffer& data, int flags) { // For use by HTTP/3 clients that read specific streams via recvStreamData(). // ============================================================================ void quic::pumpNetwork(int flags) { size_t quic::pumpNetwork(int flags) { (void)flags; std::vector<PendingDispatch> dispatches; Loading @@ -4512,6 +4512,7 @@ void quic::pumpNetwork(int flags) { static constexpr int MAX_PUMP = 64; std::vector<DatagramView> datagrams; recvBatchViews(datagrams, MAX_PUMP); const size_t datagram_count = datagrams.size(); if (!datagrams.empty()) { std::lock_guard<std::recursive_mutex> lock(quic_mtx()); Loading Loading @@ -4598,19 +4599,39 @@ void quic::pumpNetwork(int flags) { std::lock_guard<std::recursive_mutex> lock(quic_mtx()); pmtudTick(); } return datagram_count; } void quic::pumpNetworkWait(int timeout_ms) { // Bounded, best-effort wait for the socket to become readable — never // holds quic_mtx() while waiting (socketwait operates on the raw fd), // so this can't stall anything else touching this connection // concurrently. A false/timeout return is not an error: it just means // pumpNetwork() below will find nothing new, same as if this wait were // skipped entirely. if (timeout_ms > 0) { socketwait sw; sw.waitRead(*this, timeout_ms); // Fast path: try the cheap non-blocking pump first. If it already found // something, we're done — this costs exactly what pumpNetwork( // MSG_DONTWAIT) alone would have cost (one recvmmsg()), no extra // syscalls, regardless of timeout_ms. This matters: an earlier version // of this function waited unconditionally *before* pumping, which on a // tight interleaved send/receive loop (e.g. a request/response client) // paid for epoll_ctl(ADD)+epoll_wait()+epoll_ctl(DEL) on every single // call even when data was already sitting in the socket buffer — // measured ~35% slower end-to-end than plain busy-polling on a loopback // echo benchmark, i.e. worse, not better. if (pumpNetwork(MSG_DONTWAIT) > 0 || timeout_ms <= 0) { return; } // Nothing immediately available — block (bounded) for it instead of // letting the caller spin this function itself. Reuses one socketwait // (and its epoll fd) across every call instead of constructing a fresh // one each time: socketwait's constructor/destructor each cost a // syscall (epoll_create1()/close()) on top of waitRead()'s own // epoll_ctl(ADD)+epoll_wait()+epoll_ctl(DEL) — see _cc_socketwait's // comment for the same fix applied to the congestion-window wait path. // Never holds quic_mtx() while waiting (socketwait operates on the raw // fd), so this can't stall anything else touching this connection // concurrently. A false/timeout return is not an error: the follow-up // pumpNetwork() below just finds nothing new, same as if the wait were // skipped entirely. if (!_pump_socketwait) _pump_socketwait = std::make_unique<socketwait>(); _pump_socketwait->waitRead(*this, timeout_ms); pumpNetwork(MSG_DONTWAIT); } Loading src/socket.h +16 −2 Original line number Diff line number Diff line Loading @@ -884,8 +884,12 @@ namespace netplus { // Read raw UDP datagrams and process QUIC packets without consuming // stream data. Use this in HTTP/3 client loops to pump the network // while reading specific streams via recvStreamData(). void pumpNetwork(int flags = 0); // while reading specific streams via recvStreamData(). Returns the // number of datagrams processed this call (0 if none were // available) — existing callers that ignore the return value are // unaffected; pumpNetworkWait() below uses it to decide whether a // wait is even necessary. size_t pumpNetwork(int flags = 0); // Like pumpNetwork(), but if nothing is immediately available, // blocks up to timeout_ms waiting for the socket to become readable Loading Loading @@ -2185,6 +2189,16 @@ namespace netplus { // fd per connection that never needs one. std::unique_ptr<socketwait> _cc_socketwait; // Same reuse rationale as _cc_socketwait, but dedicated to // pumpNetworkWait() rather than shared with it: the congestion-wait // loop above unlocks quic_mtx() while _cc_socketwait->waitRead() is // in flight, so another thread calling pumpNetworkWait() on this // same connection at that moment could otherwise be touching the // same socketwait object (and its single epoll fd) concurrently — // socketwait isn't designed for that. A second, separate lazily- // constructed instance avoids the question entirely. std::unique_ptr<socketwait> _pump_socketwait; // ---- Path MTU Discovery (DPLPMTUD, RFC 8899 / RFC 9000 §14.3-14.4) ---- // A probe is a PATH_CHALLENGE frame padded with PADDING to an exact // target wire size, sent as an ordinary 1-RTT packet. Success is Loading Loading
src/quic.cpp +31 −10 Original line number Diff line number Diff line Loading @@ -4502,7 +4502,7 @@ size_t quic::recvData(buffer& data, int flags) { // For use by HTTP/3 clients that read specific streams via recvStreamData(). // ============================================================================ void quic::pumpNetwork(int flags) { size_t quic::pumpNetwork(int flags) { (void)flags; std::vector<PendingDispatch> dispatches; Loading @@ -4512,6 +4512,7 @@ void quic::pumpNetwork(int flags) { static constexpr int MAX_PUMP = 64; std::vector<DatagramView> datagrams; recvBatchViews(datagrams, MAX_PUMP); const size_t datagram_count = datagrams.size(); if (!datagrams.empty()) { std::lock_guard<std::recursive_mutex> lock(quic_mtx()); Loading Loading @@ -4598,19 +4599,39 @@ void quic::pumpNetwork(int flags) { std::lock_guard<std::recursive_mutex> lock(quic_mtx()); pmtudTick(); } return datagram_count; } void quic::pumpNetworkWait(int timeout_ms) { // Bounded, best-effort wait for the socket to become readable — never // holds quic_mtx() while waiting (socketwait operates on the raw fd), // so this can't stall anything else touching this connection // concurrently. A false/timeout return is not an error: it just means // pumpNetwork() below will find nothing new, same as if this wait were // skipped entirely. if (timeout_ms > 0) { socketwait sw; sw.waitRead(*this, timeout_ms); // Fast path: try the cheap non-blocking pump first. If it already found // something, we're done — this costs exactly what pumpNetwork( // MSG_DONTWAIT) alone would have cost (one recvmmsg()), no extra // syscalls, regardless of timeout_ms. This matters: an earlier version // of this function waited unconditionally *before* pumping, which on a // tight interleaved send/receive loop (e.g. a request/response client) // paid for epoll_ctl(ADD)+epoll_wait()+epoll_ctl(DEL) on every single // call even when data was already sitting in the socket buffer — // measured ~35% slower end-to-end than plain busy-polling on a loopback // echo benchmark, i.e. worse, not better. if (pumpNetwork(MSG_DONTWAIT) > 0 || timeout_ms <= 0) { return; } // Nothing immediately available — block (bounded) for it instead of // letting the caller spin this function itself. Reuses one socketwait // (and its epoll fd) across every call instead of constructing a fresh // one each time: socketwait's constructor/destructor each cost a // syscall (epoll_create1()/close()) on top of waitRead()'s own // epoll_ctl(ADD)+epoll_wait()+epoll_ctl(DEL) — see _cc_socketwait's // comment for the same fix applied to the congestion-window wait path. // Never holds quic_mtx() while waiting (socketwait operates on the raw // fd), so this can't stall anything else touching this connection // concurrently. A false/timeout return is not an error: the follow-up // pumpNetwork() below just finds nothing new, same as if the wait were // skipped entirely. if (!_pump_socketwait) _pump_socketwait = std::make_unique<socketwait>(); _pump_socketwait->waitRead(*this, timeout_ms); pumpNetwork(MSG_DONTWAIT); } Loading
src/socket.h +16 −2 Original line number Diff line number Diff line Loading @@ -884,8 +884,12 @@ namespace netplus { // Read raw UDP datagrams and process QUIC packets without consuming // stream data. Use this in HTTP/3 client loops to pump the network // while reading specific streams via recvStreamData(). void pumpNetwork(int flags = 0); // while reading specific streams via recvStreamData(). Returns the // number of datagrams processed this call (0 if none were // available) — existing callers that ignore the return value are // unaffected; pumpNetworkWait() below uses it to decide whether a // wait is even necessary. size_t pumpNetwork(int flags = 0); // Like pumpNetwork(), but if nothing is immediately available, // blocks up to timeout_ms waiting for the socket to become readable Loading Loading @@ -2185,6 +2189,16 @@ namespace netplus { // fd per connection that never needs one. std::unique_ptr<socketwait> _cc_socketwait; // Same reuse rationale as _cc_socketwait, but dedicated to // pumpNetworkWait() rather than shared with it: the congestion-wait // loop above unlocks quic_mtx() while _cc_socketwait->waitRead() is // in flight, so another thread calling pumpNetworkWait() on this // same connection at that moment could otherwise be touching the // same socketwait object (and its single epoll fd) concurrently — // socketwait isn't designed for that. A second, separate lazily- // constructed instance avoids the question entirely. std::unique_ptr<socketwait> _pump_socketwait; // ---- Path MTU Discovery (DPLPMTUD, RFC 8899 / RFC 9000 §14.3-14.4) ---- // A probe is a PATH_CHALLENGE frame padded with PADDING to an exact // target wire size, sent as an ordinary 1-RTT packet. Success is Loading