Files

171 lines
3.9 KiB
Go

package main
import (
"hash/crc32"
"sync"
"sync/atomic"
"golang.org/x/sys/unix"
)
type rxStats struct {
frames atomic.Uint64
bytes atomic.Uint64
badMagic atomic.Uint64
badHdr atomic.Uint64
badLen atomic.Uint64
crcErr atomic.Uint64
rxErrs atomic.Uint64
// Frames counted into the interval their mac receive stamp falls in, rather
// than the interval a worker got round to draining them in.
newest atomic.Int64
buckets [rateBuckets]rxBucket
}
// One sample interval of arrivals, keyed by the mac's clock, with enough of them
// kept that a bucket is read long before its slot comes round again.
const (
rateBucketNs = int64(sampleInterval)
rateBuckets = 64
rateBucketSecs = float64(rateBucketNs) / 1e9
)
type rxBucket struct {
epoch atomic.Int64
frames atomic.Uint64
bytes atomic.Uint64
}
// Only the owning worker writes its own buckets, so a slot coming round again is
// simply zeroed before it is claimed for the new epoch.
func (s *rxStats) commit(e int64, frames, bytes uint64) {
b := &s.buckets[e&(rateBuckets-1)]
if b.epoch.Load() != e {
b.frames.Store(0)
b.bytes.Store(0)
b.epoch.Store(e)
}
b.frames.Add(frames)
b.bytes.Add(bytes)
if e > s.newest.Load() {
s.newest.Store(e)
}
}
// Frames drained together that fell in the same epoch, so the buckets take one
// pair of adds per epoch a batch spans rather than one per frame.
type rateRun struct {
stats *rxStats
epoch int64
frames uint64
bytes uint64
}
func (r *rateRun) add(stamp int64, n uint64) {
if e := stamp / rateBucketNs; e != r.epoch {
r.flush()
r.epoch = e
}
r.frames++
r.bytes += n
}
func (r *rateRun) flush() {
if r.frames == 0 {
return
}
r.stats.commit(r.epoch, r.frames, r.bytes)
r.frames, r.bytes = 0, 0
}
// What this worker counted into one epoch, or nothing if that epoch has already
// fallen out of the ring.
func (s *rxStats) bucket(e int64) (frames, bytes uint64) {
b := &s.buckets[e&(rateBuckets-1)]
if b.epoch.Load() != e {
return 0, 0
}
return b.frames.Load(), b.bytes.Load()
}
type rxWorker struct {
fd int
batch int
stream uint16
spec *frameSpec
stats *rxStats
loss *lossWindow
ready *sync.WaitGroup
}
func (w *rxWorker) run(done *atomic.Bool) {
bufs := make([][]byte, w.batch)
for i := range bufs {
bufs[i] = make([]byte, maxFrame)
for j := 0; j < maxFrame; j += 4096 {
bufs[i][j] = 0
}
}
hdrs, oob := newRxMmsghdrs(bufs)
run := rateRun{stats: w.stats}
w.ready.Done()
for !done.Load() {
// The kernel overwrites each Controllen with what it wrote, so they are
// reset before every call.
for i := range hdrs {
hdrs[i].hdr.Controllen = cmsgLen
}
n, err := recvmmsg(w.fd, hdrs, unix.MSG_WAITFORONE)
if n <= 0 {
if err != nil && err != unix.EAGAIN && err != unix.EINTR {
w.stats.rxErrs.Add(1)
}
continue
}
var frames, bytes uint64
for i := 0; i < n; i++ {
buf := bufs[i][:int(hdrs[i].len)]
p, st := parseHeader(buf)
if st != hdrOK {
if st == hdrForeign {
w.stats.badMagic.Add(1)
} else {
w.stats.badHdr.Add(1)
}
continue
}
frames++
bytes += uint64(len(buf))
if ts, ok := hwTimestamp(oob[i][:hdrs[i].hdr.Controllen]); ok {
run.add(ts, uint64(len(buf)))
}
// The ethertype this socket is bound to already says which stream the
// frame belongs to, so a header naming another one is damaged, as is a
// sequence number the sender never reached.
if p.stream != w.stream || !w.loss.observe(p.seq) {
w.stats.badHdr.Add(1)
continue
}
want, ok := w.spec.expectedCRC(p.patIdx, p.payLen)
if !ok {
w.stats.badLen.Add(1)
continue
}
pay := buf[minFrame : minFrame+p.payLen]
if crc32.Checksum(pay, crcTable) != want {
w.stats.crcErr.Add(1)
}
}
// Nothing reads these between frames, and they share a cache line, so a
// batch commits once rather than locking the line for every frame.
w.stats.frames.Add(frames)
w.stats.bytes.Add(bytes)
run.flush()
}
}