diff --git a/main.go b/main.go index 4efedcb..1b68ba0 100644 --- a/main.go +++ b/main.go @@ -6,7 +6,6 @@ import ( "net" "os" "os/signal" - "slices" "strings" "sync" "sync/atomic" @@ -85,9 +84,10 @@ type counterSet struct { nic uint64 } -func (d *direction) capture(t time.Time) counterSet { +func (d *direction) capture() counterSet { d.sampleDrops() - return counterSet{t: t, s: d.snapshot(), drops: d.drops, nic: d.nic.Load()} + s := d.snapshot() + return counterSet{t: time.Now(), s: s, drops: d.drops, nic: d.nic.Load()} } // What someone testing a cable is asking, rather than how each failure happened @@ -114,10 +114,9 @@ func (e errs) add(o errs) errs { // from the gap between adjacent buckets and errors from the ends of the ring, // so both slide forward every frame instead of stepping once a second. type rateWindow struct { - buf []counterSet - idx int - filled bool - scratch []float64 + buf []counterSet + idx int + filled bool } func newRateWindow(n int) *rateWindow { @@ -148,30 +147,17 @@ func (w *rateWindow) at(i int) counterSet { return w.buf[i%len(w.buf)] } -// The median is steady against a bursty sender yet only ever a rate some bucket -// actually measured, so a step change is shown as a step: the old value holds -// until half the ring has turned over and then the new one takes it, passing -// through at most the one bucket the crossing lands on. Averaging the ring -// instead would spend the whole span sliding through rates that never happened. -func (w *rateWindow) median(rate func(prev, cur counterSet, secs float64) float64) float64 { +func (w *rateWindow) latest(rate func(prev, cur counterSet, secs float64) float64) float64 { n := w.count() if n < 2 { return 0 } - w.scratch = w.scratch[:0] - prev := w.at(0) - for i := 1; i < n; i++ { - cur := w.at(i) - if secs := cur.t.Sub(prev.t).Seconds(); secs > 0 { - w.scratch = append(w.scratch, rate(prev, cur, secs)) - } - prev = cur - } - if len(w.scratch) == 0 { + prev, cur := w.at(n-2), w.at(n-1) + secs := cur.t.Sub(prev.t).Seconds() + if secs <= 0 { return 0 } - slices.Sort(w.scratch) - return w.scratch[len(w.scratch)/2] + return rate(prev, cur, secs) } func txRatePPS(p, c counterSet, secs float64) float64 { @@ -248,7 +234,7 @@ func (d *direction) snapshot() sample { // ring so the newest bucket never sits behind it. func (d *direction) reset() { d.mu.Lock() - d.base = d.capture(time.Now()) + d.base = d.capture() d.win.push(d.base) d.mu.Unlock() @@ -347,11 +333,11 @@ func totalView(views []view) view { return t } -func (d *direction) view(t time.Time) view { +func (d *direction) view() view { d.mu.Lock() defer d.mu.Unlock() - now := d.capture(t) + now := d.capture() p := d.prevConsole d.prevConsole = now @@ -367,9 +353,9 @@ func (d *direction) view(t time.Time) view { return v } -func (d *direction) sample(t time.Time) { +func (d *direction) sample() { d.mu.Lock() - d.win.push(d.capture(t)) + d.win.push(d.capture()) d.mu.Unlock() } @@ -386,10 +372,10 @@ func (d *direction) displayView(t time.Time) view { if n >= 2 { v.window = errsBetween(d.win.at(0), d.win.at(n-1)) } - v.txPPS = d.win.median(txRatePPS) - v.rxPPS = d.win.median(rxRatePPS) - v.txGbps = d.win.median(txRateGbps) - v.rxGbps = d.win.median(rxRateGbps) + v.txPPS = d.win.latest(txRatePPS) + v.rxPPS = d.win.latest(rxRatePPS) + v.txGbps = d.win.latest(txRateGbps) + v.rxGbps = d.win.latest(rxRateGbps) d.mu.Unlock() v.rxFrames = d.heldFrames.get(t, v.rxFrames) @@ -586,8 +572,8 @@ const ( totalsHold = 50 * time.Millisecond ) -// One sampler for both directions, so their buckets share an instant and the -// cable length, which needs a figure from each, never mixes two moments. +// One sampler for both directions, so they are read back to back on one clock +// rather than drifting apart on two. type sampler struct { dirs []*direction } @@ -599,9 +585,9 @@ func (s *sampler) run(done *atomic.Bool, startTx <-chan struct{}) { defer tick.Stop() for !done.Load() { - now := <-tick.C + <-tick.C for _, d := range s.dirs { - d.sample(now) + d.sample() } } } @@ -764,7 +750,7 @@ func run(aName, bName string, nsPerM float64) error { // Length needs both directions, so every row is sampled before any of // them is printed. for i, d := range dirs { - rows[i] = d.view(now) + rows[i] = d.view() } length := "-" if m, ok := cableMetres(rows, nsPerM); ok { diff --git a/main_test.go b/main_test.go index c1703aa..43a28d5 100644 --- a/main_test.go +++ b/main_test.go @@ -16,7 +16,7 @@ func TestResetDoesNotUnderflowTotals(t *testing.T) { d.rxStats[0].frames.Store(100) d.rxStats[0].bytes.Store(6400) - d.sample(time.Now()) + d.sample() d.rxStats[0].frames.Store(150) d.rxStats[0].bytes.Store(9600)