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:
flamingcow
2026-08-17 09:35:58 -07:00
parent 06de095d1c
commit 3d7105073e
15 changed files with 313 additions and 404 deletions
+48 -21
View File
@@ -47,14 +47,24 @@ type direction struct {
drops uint64
base counterSet
// The completed receive bucket the display draws its rate from, refreshed by
// the sampler because the buckets are keyed by the mac's clock and staleness
// has to be judged against the wall.
// The smeared receive bucket the display draws its rate from, refreshed by
// the sampler because the buckets are keyed by the shared read clock and
// staleness has to be judged against the wall.
rateFrames uint64
rateBytes uint64
epoch int64
epochAt time.Time
// The settled window: each completed bucket enters once, donates its
// excess backward once, and leaves for display once no later bucket can
// still refill it. Recomputing the smear from the raw ring every sample
// instead would show each excess twice — once as the donation and again,
// unspent, when its bucket reaches the display slot of a later window.
smFrames [smearWindow]uint64
smBytes [smearWindow]uint64
smLen int
smNext int64
nic atomic.Uint64
poller *nicPoller
}
@@ -136,11 +146,13 @@ func (w *rateWindow) at(i int) counterSet {
// frontier means a stream has stopped delivering rather than an unchanged rate.
const rateStale = 100 * time.Millisecond
// A stamp is only knowable once a worker drains the frame carrying it, so each
// stream's newest epoch is a frontier: everything the wire delivered to that
// queue before it has been counted. Reading one bucket across the board takes
// the one behind the lowest frontier, which every stream has delivered past.
// The leader's frontier would claim buckets the stragglers are still filling.
// An epoch is only reachable once a worker drains frames into it, so each
// stream's newest epoch is a frontier: everything that queue has been read
// through. Epochs behind the lowest frontier — the leader's would claim
// buckets the stragglers are still filling — are closed to further commits,
// so each is settled into the window exactly once. The display takes the
// window's oldest bucket, the one no later bucket can still refill, so the
// headline runs one window behind real time.
func (d *direction) readRateBucket(now time.Time) {
newest := int64(math.MaxInt64)
for _, r := range d.rxStats {
@@ -151,15 +163,35 @@ func (d *direction) readRateBucket(now time.Time) {
if newest > d.epoch {
d.epoch, d.epochAt = newest, now
}
if d.smNext == 0 && d.epoch > 1 {
d.smNext = d.epoch - 1
}
for e := d.smNext; e > 0 && e < d.epoch; e++ {
d.settle(e)
d.smNext = e + 1
}
d.rateFrames, d.rateBytes = 0, 0
if d.epoch == 0 || now.Sub(d.epochAt) > rateStale {
if d.smLen == 0 || now.Sub(d.epochAt) > rateStale {
return
}
for _, r := range d.rxStats {
f, b := r.bucket(d.epoch - 1)
d.rateFrames += f
d.rateBytes += b
d.rateFrames, d.rateBytes = d.smFrames[0], d.smBytes[0]
}
func (d *direction) settle(e int64) {
if d.smLen == smearWindow {
copy(d.smFrames[:], d.smFrames[1:])
copy(d.smBytes[:], d.smBytes[1:])
d.smLen--
}
var f, b uint64
for _, r := range d.rxStats {
rf, rb := r.bucket(e)
f += rf
b += rb
}
d.smFrames[d.smLen], d.smBytes[d.smLen] = f, b
d.smLen++
fillBack(d.smFrames[:d.smLen], d.smBytes[:d.smLen])
}
type sample struct {
@@ -424,11 +456,6 @@ func buildDirection(label string, tx, rx endpoint) (*direction, error) {
if err != nil {
return nil, fmt.Errorf("%s rx socket for 0x%04x: %w", label, et, err)
}
// The mac already stamps every frame; this only asks for the stamp to be
// delivered.
if err := enableRxTimestamps(fd); err != nil {
return nil, fmt.Errorf("%s rx timestamps for 0x%04x: %w", label, et, err)
}
d.rxFDs = append(d.rxFDs, fd)
d.rxStats = append(d.rxStats, &rxStats{})
}
@@ -493,7 +520,7 @@ const (
numStreams = 7
batchSize = 64
testDriver = "ice"
testDriver = "ixgbe"
// A constant rather than the negotiated speed, since this has to come up
// with no cable in the port and nothing to negotiate.
@@ -508,7 +535,7 @@ func main() {
// Left empty, the test pair is found by driver name instead: as PID 1 there
// is no udev to pin names and no command line to pass, and which port gets
// which ethN shifts with every driver built into the kernel.
aName := flag.String("a", "", "first interface (default: the ice pair)")
aName := flag.String("a", "", "first interface (default: the ixgbe pair)")
bName := flag.String("b", "", "second interface")
flag.Parse()
@@ -531,7 +558,7 @@ const (
// the sampling clock would stretch the window it reports.
sampleInterval = 16 * time.Millisecond
// How far back the shown errors reach. The rate is not taken from this ring
// but from the receive buckets, which are keyed by the mac's clock.
// but from the smeared receive buckets, which are keyed by the read clock.
rateWindowSpan = time.Second
)