X520 live as the product NIC: rate buckets re-keyed to read time (one shared clock, one commit per drained batch) with the backward smear as a settled window — each completed bucket enters once, donates excess to the nearest earlier deficits once, locks for display when nothing later can refill it (per-sample recompute double-counted excess and read >20G on a 20G wire); timestamp machinery deleted (ts.go, SO_TIMESTAMPING, rx_filter=ALL check) after the audit confirmed the buckets were its sole consumer; testDriver ixgbe; RollBall mailbox hardened against orphaned-command completions (never sample status before a poll gap, value from the same block read, devad/reg echo required — a killed session's 7.60 read answered a PHY ID as 0x0006); full hardware pass: 20.00G flat both directions, zero errors, ECD 42m, SNR +7.4dB
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"hash/crc32"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
@@ -17,20 +18,35 @@ type rxStats struct {
|
||||
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.
|
||||
// Frames counted into the interval the worker read them in, on one shared
|
||||
// host clock; read jitter is repaired at display time by the backward smear.
|
||||
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.
|
||||
// One sample interval of reads per bucket, with enough of them kept that the
|
||||
// smear window fits and a bucket is read long before its slot comes round
|
||||
// again. smearWindow bounds how far back excess may travel and is how far
|
||||
// behind real time the displayed rate runs; it must comfortably exceed the
|
||||
// worst host read stall.
|
||||
const (
|
||||
rateBucketNs = int64(sampleInterval)
|
||||
rateBuckets = 64
|
||||
rateBucketSecs = float64(rateBucketNs) / 1e9
|
||||
|
||||
smearWindow = 4
|
||||
|
||||
// Full wire occupancy of one bucket at line rate, in the measure gbps()
|
||||
// reports: counted bytes plus wireOverhead per frame.
|
||||
bucketWireCap = uint64(linkSpeed*1e9/8) * uint64(rateBucketNs) / 1_000_000_000
|
||||
)
|
||||
|
||||
var rateEpochStart = time.Now()
|
||||
|
||||
func rateEpoch() int64 {
|
||||
return int64(time.Since(rateEpochStart)) / rateBucketNs
|
||||
}
|
||||
|
||||
type rxBucket struct {
|
||||
epoch atomic.Int64
|
||||
frames atomic.Uint64
|
||||
@@ -53,32 +69,6 @@ func (s *rxStats) commit(e int64, frames, bytes uint64) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -89,6 +79,36 @@ func (s *rxStats) bucket(e int64) (frames, bytes uint64) {
|
||||
return b.frames.Load(), b.bytes.Load()
|
||||
}
|
||||
|
||||
// Donate the newest bucket's excess above line rate backward into the nearest
|
||||
// earlier deficits. Read jitter is purely backward — a frame is read at or
|
||||
// after its arrival — so excess is frames that arrived earlier and were read
|
||||
// late, and the burst drains the backlog of the stall immediately before it;
|
||||
// a deficit with no later excess (a genuine wire dip) keeps its full size,
|
||||
// and excess never moves forward. Frames travel with the bytes they carried,
|
||||
// in the donor's proportion. Each bucket donates exactly once, on entry to
|
||||
// the settled window — the donation mutates the stored values, so a donated
|
||||
// byte can never display again in its donor.
|
||||
func fillBack(frames, bytes []uint64) {
|
||||
i := len(frames) - 1
|
||||
for j := i - 1; j >= 0; j-- {
|
||||
wire := bytes[i] + frames[i]*wireOverhead
|
||||
if wire <= bucketWireCap {
|
||||
return
|
||||
}
|
||||
have := bytes[j] + frames[j]*wireOverhead
|
||||
if have >= bucketWireCap {
|
||||
continue
|
||||
}
|
||||
take := min(wire-bucketWireCap, bucketWireCap-have)
|
||||
mf := frames[i] * take / wire
|
||||
mb := take - mf*wireOverhead
|
||||
frames[i] -= mf
|
||||
bytes[i] -= mb
|
||||
frames[j] += mf
|
||||
bytes[j] += mb
|
||||
}
|
||||
}
|
||||
|
||||
type rxWorker struct {
|
||||
fd int
|
||||
batch int
|
||||
@@ -107,17 +127,11 @@ func (w *rxWorker) run(done *atomic.Bool) {
|
||||
bufs[i][j] = 0
|
||||
}
|
||||
}
|
||||
hdrs, oob := newRxMmsghdrs(bufs)
|
||||
run := rateRun{stats: w.stats}
|
||||
hdrs, _ := newMmsghdrs(bufs)
|
||||
|
||||
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 {
|
||||
@@ -125,6 +139,7 @@ func (w *rxWorker) run(done *atomic.Bool) {
|
||||
}
|
||||
continue
|
||||
}
|
||||
epoch := rateEpoch()
|
||||
var frames, bytes uint64
|
||||
for i := 0; i < n; i++ {
|
||||
buf := bufs[i][:int(hdrs[i].len)]
|
||||
@@ -139,9 +154,6 @@ func (w *rxWorker) run(done *atomic.Bool) {
|
||||
}
|
||||
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
|
||||
@@ -165,6 +177,8 @@ func (w *rxWorker) run(done *atomic.Bool) {
|
||||
// batch commits once rather than locking the line for every frame.
|
||||
w.stats.frames.Add(frames)
|
||||
w.stats.bytes.Add(bytes)
|
||||
run.flush()
|
||||
if frames > 0 {
|
||||
w.stats.commit(epoch, frames, bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user