flare_mlake · API · udp

flare.udp.batch

Batched UDP I/O: recvmmsg / sendmmsg + GSO segmentation.

A QUIC server's hot loop is dominated by per-datagram syscalls: one recvfrom per inbound packet, one sendto per outbound packet. Under a burst that is N syscalls for N datagrams. Linux's recvmmsg(2) / sendmmsg(2) move a whole vector of datagrams across the user/kernel boundary in a single call, and UDP generic segmentation offload (GSO, UDP_SEGMENT) lets one sendmsg hand the kernel a big buffer that it slices into many equal-sized wire datagrams. Together these cut the syscall count on a saturated path by ~Nx.

These syscalls are Linux-only and version-gated (recvmmsg 2.6.33, sendmmsg 3.0, UDP_SEGMENT 4.18). This module is the capability probe + safe wrapper:

  • :func:udp_batch_supported -- compile-time Linux gate. On any other target the helpers are never reached; callers use the per-datagram path.
  • :class:UdpBatchUnsupported -- raised when a running kernel lacks the syscall (ENOSYS). Callers catch it once and latch back to the per-datagram path for the rest of the process lifetime.
  • :class:BatchReceiver -- owns the struct mmsghdr[] + iovec[]
    • name + data arrays and drains a burst with one recvmmsg.
  • :func:send_batch -- one sendmmsg for a vector of (payload, destination) pairs.
  • :func:send_segmented -- one GSO sendmsg that slices data into seg_size wire datagrams to a single destination.

Struct layout (x86_64 / aarch64 Linux, the only targets these calls run on):

struct iovec   { void *base; size_t len; }              // 16 B
struct msghdr  { void *name; socklen_t namelen; (pad)   //  0,  8
                 struct iovec *iov; size_t iovlen;       // 16, 24
                 void *control; size_t controllen;       // 32, 40
                 int flags; (pad) }                      // 48  -> 56 B
struct mmsghdr { struct msghdr hdr; unsigned len; (pad)} // 56  -> 64 B

The arrays are modeled as flat Pointer[UInt8] regions with the fields poked at fixed offsets, mirroring flare.runtime.iovec.IoVecBuf.

The name slots are sized for IPv4 + IPv6 (28 B), but the wire structs are pinned to the 64-bit Linux ABI -- the only ABI where these syscalls exist in this codebase. A 32-bit Linux port would need different offsets, branching the comptime offsets on pointer width.

Structs

UdpBatchUnsupported

struct UdpBatchUnsupported

Raised when the running kernel lacks recvmmsg / sendmmsg (ENOSYS). The caller falls back to per-datagram I/O.

Implements Copyable, Movable, Writable

Fields
op: String

__init__

def __init__(out self, op: String)

write_to

def write_to[W: Writer](self, mut writer: W)

BatchReceiver

struct BatchReceiver

Owns the mmsghdr vector + backing buffers for a single recvmmsg drain of up to capacity datagrams.

Reuse one receiver across reactor ticks: :meth:recv re-arms the per-message lengths in place, so steady-state has zero allocation.

Example:

var rx = BatchReceiver(capacity=64, max_payload=1500)
var n = rx.recv(sock.fd())
for i in range(n):
    var dg = rx.message(i)        # Span[UInt8] of the payload
    var who = rx.sender(i)        # SocketAddr of the source

Implements Movable

__init__

def __init__(out self, capacity: Int, max_payload: Int)

Allocate the vector for capacity datagrams of at most max_payload bytes each.

Args
capacity: Max datagrams drained per :meth:recv. Must be > 0.
max_payload: Per-datagram buffer size (e.g. the connection's max UDP payload). Must be > 0.

Raises Error: If either argument is not positive, or if capacity * max_payload (the data region) or capacity * 64 (the largest per-slot array, the mmsghdr vector) does not fit in an Int. Such a product would wrap to a small allocation while every iovec still announced max_payload bytes to recvmmsg.

__deinit__

def __deinit__(deinit self)

recv

def recv(mut self, fd: Int, wait: Bool = False) -> Int

Drain up to capacity datagrams with one non-blocking recvmmsg; with wait, block (up to the socket's SO_RCVTIMEO) for the first, then take what else is queued.

Returns the number of datagrams received (0 when the socket queue is empty -- EAGAIN / EWOULDBLOCK).

Raises UdpBatchUnsupported: kernel lacks recvmmsg (ENOSYS). NetworkError: any other libc errno.

count

def count(self) -> Int

Number of datagrams from the last :meth:recv.

message

def message(self, i: Int) -> Span[UInt8, MutUntrackedOrigin]

Borrow the i-th received datagram's payload.

The span aliases the receiver's internal buffer and is only valid until the next :meth:recv. Bounds-checked under -D ASSERT=safe.

ecn

def ecn(self, i: Int) -> Int

The ECN codepoint (low two bits of the IPv4 TOS / IPv6 traffic class) the i-th datagram arrived with; 0 unless the socket asked for it (:meth:flare.udp.UdpSocket.set_ecn).

sender

def sender(self, i: Int) -> SocketAddr

Decode the i-th datagram's source address.

Functions

udp_batch_supported

def udp_batch_supported() -> Bool

True when batched UDP syscalls can be attempted.

Compile-time Linux gate. A True result still does not promise the running kernel implements the syscall -- the first call may raise :class:UdpBatchUnsupported (ENOSYS), which the caller latches to disable the path. On macOS / other targets this is False and the wrappers are never invoked.

send_batch

def send_batch(fd: Int, payloads: List[List[UInt8]], addrs: List[SocketAddr]) -> Int

Send a vector of (payload, destination) pairs with one sendmmsg.

Args
fd: UDP socket fd.
payloads: One byte buffer per datagram.
addrs: Matching destination addresses (len == len(payloads)).

Raises UdpBatchUnsupported: kernel lacks sendmmsg (ENOSYS). NetworkError: any other libc errno.

send_segmented

def send_segmented(fd: Int, addr: SocketAddr, data: Span[UInt8], seg_size: Int, ecn: Int = Int(0)) -> Int

Send data to addr as GSO segments of seg_size bytes.

One sendmsg hands the kernel the whole data buffer plus a UDP_SEGMENT control message; the kernel (or NIC) slices it into wire datagrams of seg_size bytes (the final one may be shorter). seg_size 0 sends one datagram. A non-zero ecn codepoint (RFC 3168) marks every datagram via an IP_TOS / IPV6_TCLASS control message, whatever the socket's own setting.

Returns bytes accepted by the kernel.

Raises UdpBatchUnsupported: kernel lacks UDP_SEGMENT (EINVAL on the cmsg is mapped here as unsupported). NetworkError: any other libc errno.

Aliases

comptime SOL_UDP = IPPROTO_UDP
comptime UDP_SEGMENT = Int32(103)