Skip to content

Udp batching - #8

Open
xl32 wants to merge 2 commits into
machine-moon:trunkfrom
xl32:udp-batching
Open

Udp batching#8
xl32 wants to merge 2 commits into
machine-moon:trunkfrom
xl32:udp-batching

Conversation

@xl32

@xl32 xl32 commented Aug 7, 2026

Copy link
Copy Markdown
  • Read queued datagrams in one batch with recvmmsg() where the platform has
    it, and read the socket's local address once per event-loop pass instead
    of once per datagram, halving the syscalls the ngtcp2 engine spends on a
    busy socket. The OpenSSL engine already batches inside its own datagram
    BIO. Also dropped a per-pass walk of the connection list whose result was
    never used.

  • Added H3SocketBufferSize, which asks for the QUIC socket send and receive
    buffer size. A receive buffer left at the OS default overflows once a
    single connection runs at speed, and each dropped datagram costs a
    retransmit; the OS still caps what it grants, and a capped grant is
    logged rather than fatal.

xl32 added 2 commits August 7, 2026 01:06
The ngtcp2 engine spent two syscalls on every datagram: one recvfrom()
and one getsockname(), the latter inside the read loop even though the
comment above it already noted the bound address cannot change while the
engine lives. Read the local address once per pass, and add an optional
recv_batch hook to quic_io that quic_io_udp implements with recvmmsg(),
so a busy socket costs one syscall per batch of 16 rather than one per
datagram. The hook is optional: left NULL on platforms without
recvmmsg(), callers keep using recv(). The OpenSSL engine needs none of
this, as it drives its own datagram BIO through BIO_recvmmsg() already.

Sending is deliberately not batched. Both accumulating packets for
sendmmsg() and coalescing them for UDP_SEGMENT/GSO need ngtcp2's write
loop restructured to emit equal-sized packets per destination, which is
not a change this diff can make safely, and the default engine could not
use it.

Also add H3SocketBufferSize. A UDP receive buffer at the OS default
(commonly 208KB) overflows as soon as one connection runs at speed, and
each dropped datagram costs a retransmit and a congestion-window cut.
The OS caps what it grants, so the module asks, reports a capped or
refused grant at info level, and never fails the socket over it.

Removes a per-pass walk of the whole connection list whose two counters
were never read.
The bound address cannot change while the engine lives, which is what
the comment on the old recv helper already said, so asking the kernel
for it on every event-loop pass was avoidable. Read it when the engine
is created -- the socket is bound before that point and stays bound --
and keep it in the engine, which takes getsockname() off the pump path
entirely rather than merely out of the per-datagram path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant