use SO_TIMESTAMP, works on Linux, FreeBSD and macOS

This commit is contained in:
Andreas Wrede
2026-04-07 10:46:54 -04:00
parent 02bc42fbf0
commit 51f9bdc2b5
+18 -17
View File
@@ -14,22 +14,23 @@ from . import notify as notify_mod
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
eventlog = notify_mod.eventlog eventlog = notify_mod.eventlog
# SO_TIMESTAMPNS: kernel attaches a struct timespec to each received datagram. # SO_TIMESTAMP: kernel attaches a struct timeval to each received datagram.
# The constant is not exposed by Python's socket module on all platforms, # Supported on Linux, FreeBSD, and macOS. The constant is not exposed by
# so fall back to the Linux value (35) when absent. # Python's socket module on all platforms, so fall back to the Linux value (29)
_SO_TIMESTAMPNS = getattr(socket, 'SO_TIMESTAMPNS', 35) # when absent.
# struct timespec uses two native C longs: tv_sec and tv_nsec _SO_TIMESTAMP = getattr(socket, 'SO_TIMESTAMP', 29)
_TIMESPEC = struct.Struct('@ll') # struct timeval uses two native C longs: tv_sec and tv_usec
_TIMEVAL = struct.Struct('@ll')
def enable_kernel_timestamps(sock) -> bool: def enable_kernel_timestamps(sock) -> bool:
"""Try to enable SO_TIMESTAMPNS on *sock*. """Try to enable SO_TIMESTAMP on *sock*.
Returns True if the kernel will supply receive timestamps, False otherwise Returns True if the kernel will supply receive timestamps, False otherwise
(non-Linux, older kernel, or insufficient permissions). (unsupported platform, older kernel, or insufficient permissions).
""" """
try: try:
sock.setsockopt(socket.SOL_SOCKET, _SO_TIMESTAMPNS, 1) sock.setsockopt(socket.SOL_SOCKET, _SO_TIMESTAMP, 1)
return True return True
except OSError: except OSError:
return False return False
@@ -38,18 +39,18 @@ def enable_kernel_timestamps(sock) -> bool:
def _extract_kernel_ts(ancdata) -> float | None: def _extract_kernel_ts(ancdata) -> float | None:
"""Parse recvmsg ancillary data and return the kernel receive time. """Parse recvmsg ancillary data and return the kernel receive time.
Returns seconds as a float, or None if no SO_TIMESTAMPNS cmsg is present. Returns seconds as a float, or None if no SO_TIMESTAMP cmsg is present.
""" """
for cmsg_level, cmsg_type, cmsg_data in ancdata: for cmsg_level, cmsg_type, cmsg_data in ancdata:
if cmsg_level == socket.SOL_SOCKET and cmsg_type == _SO_TIMESTAMPNS: if cmsg_level == socket.SOL_SOCKET and cmsg_type == _SO_TIMESTAMP:
if len(cmsg_data) >= _TIMESPEC.size: if len(cmsg_data) >= _TIMEVAL.size:
sec, nsec = _TIMESPEC.unpack_from(cmsg_data) sec, usec = _TIMEVAL.unpack_from(cmsg_data)
return sec + nsec * 1e-9 return sec + usec * 1e-6
return None return None
class RecvmsgTransport: class RecvmsgTransport:
"""Thin wrapper used when SO_TIMESTAMPNS is active (add_reader path). """Thin wrapper used when SO_TIMESTAMP is active (add_reader path).
Exposes the same sendto() / close() interface as asyncio's DatagramTransport Exposes the same sendto() / close() interface as asyncio's DatagramTransport
so the rest of the code does not need to know which path is in use. so the rest of the code does not need to know which path is in use.
@@ -79,8 +80,8 @@ def make_recvmsg_reader(sock, handler, transport):
"""Return a callback suitable for loop.add_reader(). """Return a callback suitable for loop.add_reader().
Reads one datagram per call using recvmsg() so that kernel timestamps in Reads one datagram per call using recvmsg() so that kernel timestamps in
the ancillary data are accessible. Falls back to time.time() gracefully the ancillary data are accessible. Falls back to time.time() if the
if the cmsg is missing. cmsg is missing.
handler(msg, addr, transport, kernel_ts) same signature as udp_handler handler(msg, addr, transport, kernel_ts) same signature as udp_handler
in main.py with the optional kernel_ts argument. in main.py with the optional kernel_ts argument.