Files
cabletest/probe.go
T

224 lines
5.0 KiB
Go

package main
import (
"sync"
"sync/atomic"
"time"
"golang.org/x/sys/unix"
)
const (
// Above any real stream number, so a probe is never taken for payload.
probeStream = 0xffff
probeSize = 64
probePattern = 0
// The mac has only a handful of transmit stamp slots. Asking faster than it
// can drain them gets slots recycled while a stamp is still outstanding, and
// the one that comes back then belongs to a different frame.
probeInterval = 200 * time.Millisecond
probeTimeout = 20 * time.Millisecond
// A phy costs microseconds and a hundred metres of copper costs five hundred
// nanoseconds, so anything past this is a broken stamp, not a slow frame.
probeMaxDelay = 50 * time.Microsecond
probePendCap = 256
)
// Both ports hang off one PTP clock, so a transmit stamp from one and a receive
// stamp from the other subtract directly. Both are taken at the mac, so all host
// time and all queueing falls outside the stamped interval, which is why load
// does not move it.
type cableStats struct {
mu sync.Mutex
min int64
samples uint64
floor int64
txPend map[uint64]int64
rxPend map[uint64]int64
}
type cableView struct {
min int64
floor int64
ok bool
}
// Averaging the two directions cancels the phy asymmetry between them, which is
// about 790ns and swamps any cable, so one direction alone cannot give a length.
func cableMetres(views []view, nsPerM float64) (float64, bool) {
if len(views) == 0 {
return 0, false
}
var excess float64
for _, v := range views {
if !v.cable.ok {
return 0, false
}
excess += float64(v.cable.min - v.cable.floor)
}
return excess / float64(len(views)) / nsPerM, true
}
func newCableStats() *cableStats {
return &cableStats{
txPend: make(map[uint64]int64, probePendCap),
rxPend: make(map[uint64]int64, probePendCap),
}
}
func (c *cableStats) put(seq uint64, ts int64, tx bool) {
c.mu.Lock()
defer c.mu.Unlock()
mine, theirs := c.txPend, c.rxPend
if !tx {
mine, theirs = c.rxPend, c.txPend
}
other, ok := theirs[seq]
if !ok {
if len(mine) >= probePendCap {
clear(mine)
}
mine[seq] = ts
return
}
delete(theirs, seq)
delta := ts - other
if tx {
delta = -delta
}
// The driver rebuilds a full timestamp from a truncated hardware value plus a
// cached clock read, and a stale cache lands hundreds of milliseconds out. A
// minimum would latch onto the first of those and never recover.
if delta <= 0 || delta > int64(probeMaxDelay) {
return
}
if c.samples == 0 || delta < c.min {
c.min = delta
}
if c.floor == 0 || delta < c.floor {
c.floor = delta
}
c.samples++
}
func (c *cableStats) view() cableView {
c.mu.Lock()
defer c.mu.Unlock()
return cableView{c.min, c.floor, c.samples > 0}
}
func (c *cableStats) reset() {
c.mu.Lock()
c.min, c.samples = 0, 0
clear(c.txPend)
clear(c.rxPend)
c.mu.Unlock()
}
type probeSender struct {
fd int
spec *frameSpec
stats *cableStats
}
func (p *probeSender) run(done *atomic.Bool, startTx <-chan struct{}) {
buf := make([]byte, probeSize)
p.spec.prefill(buf, probePattern)
oob := make([]byte, 512)
scratch := make([]byte, 1)
<-startTx
tick := time.NewTicker(probeInterval)
defer tick.Stop()
var seq uint64
for !done.Load() {
<-tick.C
// Stamps are matched to sends by position in the queue, so one that
// arrived after its probe gave up would be handed to this probe.
for {
if _, _, _, _, err := unix.Recvmsg(p.fd, scratch, oob,
unix.MSG_ERRQUEUE|unix.MSG_DONTWAIT); err != nil {
break
}
}
putHeader(buf, probePattern, probeStream, seq, probeSize-minFrame)
err := unix.Send(p.fd, buf, 0)
// The sequence advances even when a probe fails, so a stale receive half
// can never be paired with a later probe that reused its number.
cur := seq
seq++
if err != nil {
continue
}
ts, ok := p.awaitTx(scratch, oob)
if !ok {
continue
}
p.stats.put(cur, ts, true)
}
}
func (p *probeSender) awaitTx(scratch, oob []byte) (int64, bool) {
fds := []unix.PollFd{{Fd: int32(p.fd), Events: unix.POLLERR}}
deadline := time.Now().Add(probeTimeout)
for {
ms := int(time.Until(deadline).Milliseconds())
if ms <= 0 {
return 0, false
}
n, err := unix.Poll(fds, ms)
if err == unix.EINTR {
continue
}
if err != nil || n == 0 {
return 0, false
}
_, oobn, _, _, err := unix.Recvmsg(p.fd, scratch, oob,
unix.MSG_ERRQUEUE|unix.MSG_DONTWAIT)
if err == unix.EAGAIN || err == unix.EINTR {
continue
}
if err != nil {
return 0, false
}
return hwTimestamp(oob[:oobn])
}
}
type probeReceiver struct {
fd int
stats *cableStats
ready *sync.WaitGroup
}
func (r *probeReceiver) run(done *atomic.Bool) {
buf := make([]byte, maxFrame)
oob := make([]byte, 512)
r.ready.Done()
for !done.Load() {
n, oobn, _, _, err := unix.Recvmsg(r.fd, buf, oob, 0)
if err != nil {
continue
}
h, st := parseHeader(buf[:n])
if st != hdrOK || h.stream != probeStream {
continue
}
ts, ok := hwTimestamp(oob[:oobn])
if !ok {
continue
}
r.stats.put(h.seq, ts, false)
}
}