From ce44fe9866a8934500f669bda1435d4cb2be6090 Mon Sep 17 00:00:00 2001 From: flamingcow Date: Tue, 4 Aug 2026 20:48:17 -0700 Subject: [PATCH] Walk the control buffer by hand instead of allocating a message list per frame --- ts.go | 34 ++++++++++++------- ts_test.go | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 ts_test.go diff --git a/ts.go b/ts.go index 1991a6d..bb2903e 100644 --- a/ts.go +++ b/ts.go @@ -80,21 +80,31 @@ func enableRxTimestamps(fd int) error { // means the mac did not produce a stamp. const scmTimestampingLen = 3 * int(unsafe.Sizeof(unix.Timespec{})) +// A control message's payload starts at its header rounded up to a pointer, and +// the next starts at its length rounded up the same way. +const ( + cmsgAlignMask = unix.SizeofPtr - 1 + cmsgDataOff = (unix.SizeofCmsghdr + cmsgAlignMask) &^ cmsgAlignMask +) + +// Walked by hand rather than through ParseSocketControlMessage, which allocates +// per call on a path that runs for every frame received. func hwTimestamp(oob []byte) (int64, bool) { - msgs, err := unix.ParseSocketControlMessage(oob) - if err != nil { - return 0, false - } - for _, m := range msgs { - if m.Header.Level != unix.SOL_SOCKET || m.Header.Type != unix.SCM_TIMESTAMPING { - continue - } - if len(m.Data) < scmTimestampingLen { + for off := 0; off+unix.SizeofCmsghdr <= len(oob); { + h := (*unix.Cmsghdr)(unsafe.Pointer(&oob[off])) + n := int(h.Len) + if n < unix.SizeofCmsghdr || off+n > len(oob) { return 0, false } - ts := (*[3]unix.Timespec)(unsafe.Pointer(&m.Data[0])) - ns := ts[2].Nano() - return ns, ns != 0 + if h.Level == unix.SOL_SOCKET && h.Type == unix.SCM_TIMESTAMPING { + if cmsgDataOff+scmTimestampingLen > n { + return 0, false + } + ts := (*[3]unix.Timespec)(unsafe.Pointer(&oob[off+cmsgDataOff])) + ns := ts[2].Nano() + return ns, ns != 0 + } + off += (n + cmsgAlignMask) &^ cmsgAlignMask } return 0, false } diff --git a/ts_test.go b/ts_test.go new file mode 100644 index 0000000..b363a3e --- /dev/null +++ b/ts_test.go @@ -0,0 +1,99 @@ +package main + +import ( + "testing" + "unsafe" + + "golang.org/x/sys/unix" +) + +func putCmsg(buf []byte, off int, level, typ int32, dataLen int) int { + h := (*unix.Cmsghdr)(unsafe.Pointer(&buf[off])) + h.SetLen(unix.CmsgLen(dataLen)) + h.Level = level + h.Type = typ + return off + unix.CmsgSpace(dataLen) +} + +func putStamp(buf []byte, off int, raw unix.Timespec) int { + next := putCmsg(buf, off, unix.SOL_SOCKET, unix.SCM_TIMESTAMPING, scmTimestampingLen) + ts := (*[3]unix.Timespec)(unsafe.Pointer(&buf[off+cmsgDataOff])) + ts[2] = raw + return next +} + +func TestHwTimestampReadsRawHardwareClock(t *testing.T) { + buf := make([]byte, cmsgLen) + n := putStamp(buf, 0, unix.Timespec{Sec: 5, Nsec: 7}) + + got, ok := hwTimestamp(buf[:n]) + if !ok || got != 5e9+7 { + t.Errorf("hwTimestamp = %d, %v; want 5000000007, true", got, ok) + } +} + +func TestHwTimestampSkipsOtherMessages(t *testing.T) { + buf := make([]byte, cmsgLen) + off := putCmsg(buf, 0, unix.SOL_SOCKET, unix.SCM_RIGHTS, 4) + n := putStamp(buf, off, unix.Timespec{Sec: 1, Nsec: 2}) + + got, ok := hwTimestamp(buf[:n]) + if !ok || got != 1e9+2 { + t.Errorf("hwTimestamp = %d, %v; want 1000000002, true", got, ok) + } +} + +func TestHwTimestampRejectsZeroStamp(t *testing.T) { + buf := make([]byte, cmsgLen) + n := putStamp(buf, 0, unix.Timespec{}) + + if got, ok := hwTimestamp(buf[:n]); ok { + t.Errorf("hwTimestamp = %d, %v; want a zero stamp refused", got, ok) + } +} + +func TestHwTimestampWithoutAStamp(t *testing.T) { + buf := make([]byte, cmsgLen) + n := putCmsg(buf, 0, unix.SOL_SOCKET, unix.SCM_RIGHTS, 4) + + if _, ok := hwTimestamp(buf[:n]); ok { + t.Error("a buffer carrying no timestamp yielded one") + } + if _, ok := hwTimestamp(nil); ok { + t.Error("an empty buffer yielded a timestamp") + } +} + +func TestHwTimestampRefusesBadLengths(t *testing.T) { + buf := make([]byte, cmsgLen) + n := putStamp(buf, 0, unix.Timespec{Sec: 1}) + if _, ok := hwTimestamp(buf[:n-1]); ok { + t.Error("a message running past the buffer yielded a timestamp") + } + + short := make([]byte, cmsgLen) + putCmsg(short, 0, unix.SOL_SOCKET, unix.SCM_TIMESTAMPING, scmTimestampingLen) + (*unix.Cmsghdr)(unsafe.Pointer(&short[0])).SetLen(unix.SizeofCmsghdr - 1) + if _, ok := hwTimestamp(short); ok { + t.Error("a message shorter than its own header yielded a timestamp") + } + + trunc := make([]byte, cmsgLen) + tn := putCmsg(trunc, 0, unix.SOL_SOCKET, unix.SCM_TIMESTAMPING, scmTimestampingLen/3) + if _, ok := hwTimestamp(trunc[:tn]); ok { + t.Error("a truncated stamp yielded a timestamp") + } +} + +func BenchmarkHwTimestamp(b *testing.B) { + buf := make([]byte, cmsgLen) + n := putStamp(buf, 0, unix.Timespec{Sec: 5, Nsec: 7}) + oob := buf[:n] + + b.ReportAllocs() + for b.Loop() { + if _, ok := hwTimestamp(oob); !ok { + b.Fatal("no timestamp") + } + } +}