package main import ( "testing" "time" ) // Batches land in the bucket of the epoch they were read in, summed, and // newest follows the highest epoch committed. func TestRxCommitBucketsByEpoch(t *testing.T) { var s rxStats s.commit(0, 1, 100) s.commit(0, 1, 200) s.commit(1, 1, 300) if f, b := s.bucket(0); f != 2 || b != 300 { t.Errorf("epoch 0 = %d frames, %d bytes; want 2, 300", f, b) } if f, b := s.bucket(1); f != 1 || b != 300 { t.Errorf("epoch 1 = %d frames, %d bytes; want 1, 300", f, b) } if got := s.newest.Load(); got != 1 { t.Errorf("newest = %d, want 1", got) } } // A slot coming round again belongs to its new epoch, and the epoch it replaced // reports nothing rather than the stale counts. func TestRxBucketWraps(t *testing.T) { var s rxStats s.commit(0, 1, 100) s.commit(rateBuckets, 1, 200) if f, b := s.bucket(0); f != 0 || b != 0 { t.Errorf("evicted epoch = %d frames, %d bytes; want 0, 0", f, b) } if f, b := s.bucket(rateBuckets); f != 1 || b != 200 { t.Errorf("new epoch = %d frames, %d bytes; want 1, 200", f, b) } } // Every stream's frontier is past the epoch being read, so the bucket is // complete across the board and the rate comes from the window behind the // lowest frontier. func TestDirectionRateReadsOneBucketBack(t *testing.T) { d := &direction{rxStats: []*rxStats{{}, {}}} d.rxStats[0].commit(1, 1, 500) d.rxStats[1].commit(1, 1, 700) d.rxStats[0].commit(2, 1, 900) d.rxStats[1].commit(2, 1, 1100) d.readRateBucket(time.Now()) if d.rateFrames != 2 || d.rateBytes != 1200 { t.Errorf("rate bucket = %d frames, %d bytes; want the completed epoch, 2 and 1200", d.rateFrames, d.rateBytes) } } // Reading a bucket is a claim that every stream has delivered through it, so a // stream still short of the epoch holds the read back rather than having its // half-filled bucket summed. func TestDirectionRateWaitsForSlowestStream(t *testing.T) { d := &direction{rxStats: []*rxStats{{}, {}}} d.rxStats[0].commit(1, 1, 500) d.rxStats[0].commit(2, 1, 900) // One stream has never delivered at all, so there is no epoch every stream // has reached and nothing to read. d.readRateBucket(time.Now()) if d.rateFrames != 0 || d.rateBytes != 0 { t.Errorf("rate = %d frames, %d bytes; want nothing while a stream has no frontier", d.rateFrames, d.rateBytes) } // The straggler is still filling the epoch the leader finished, so its // bucket must not be read yet. d.rxStats[1].commit(1, 1, 700) d.readRateBucket(time.Now()) if d.rateFrames != 0 || d.rateBytes != 0 { t.Errorf("rate = %d frames, %d bytes; want nothing while a stream is still filling the epoch", d.rateFrames, d.rateBytes) } // Once it moves past, the bucket is complete for both streams and is read // whole. d.rxStats[1].commit(2, 1, 1100) d.readRateBucket(time.Now()) if d.rateFrames != 2 || d.rateBytes != 1200 { t.Errorf("rate = %d frames, %d bytes; want the whole completed epoch, 2 and 1200", d.rateFrames, d.rateBytes) } } // An epoch only advances when a frame is read, so a frontier that stops moving // is a quiet wire and must not keep reporting the last bucket. func TestDirectionRateGoesStale(t *testing.T) { d := &direction{rxStats: []*rxStats{{}}} d.rxStats[0].commit(1, 1, 500) d.rxStats[0].commit(2, 1, 900) now := time.Now() d.readRateBucket(now) if d.rateFrames == 0 { t.Fatal("expected a rate while the epoch was still moving") } d.readRateBucket(now.Add(2 * rateStale)) if d.rateFrames != 0 || d.rateBytes != 0 { t.Errorf("stale rate = %d frames, %d bytes; want 0, 0", d.rateFrames, d.rateBytes) } } // Each bucket entering the settled window donates once, as it arrives. func settleAll(frames, bytes []uint64) { for i := 1; i <= len(frames); i++ { fillBack(frames[:i], bytes[:i]) } } // A donated byte must never display twice: once a burst's excess has refilled // an earlier deficit, the burst bucket itself reaches the display slot // trimmed. The pre-settled-window implementation recomputed the smear from // the raw ring per sample and displayed the excess again — 2C here, and >20G // on the wire. func TestDirectionRateNeverRedisplaysDonatedExcess(t *testing.T) { d := &direction{rxStats: []*rxStats{{}}} c := bucketWireCap seq := []uint64{c, 0, 2 * c, c, c, c, c, c} now := time.Now() for i, b := range seq { d.rxStats[0].commit(int64(i+1), 0, b) d.readRateBucket(now) if d.rateBytes > c { t.Errorf("after epoch %d: displayed %d bytes, over line rate %d", i+1, d.rateBytes, c) } } // Jitter settled: the display holds flat line rate, not just under it. if d.rateBytes != c { t.Errorf("settled display = %d bytes, want exactly %d", d.rateBytes, c) } } // A stall's deficit is refilled exactly by the burst that drains its backlog, // so host jitter flattens to line rate. func TestSmearRepairsStallBurst(t *testing.T) { c := bucketWireCap bytes := []uint64{c, 0, 0, 3 * c} frames := make([]uint64, 4) settleAll(frames, bytes) for i, want := range []uint64{c, c, c, c} { if bytes[i] != want { t.Errorf("bucket %d = %d bytes, want %d", i, bytes[i], want) } } } // Excess refills the nearest earlier deficit, so a genuine wire dip — a // deficit with no matching excess — keeps its full size in its own bucket. func TestSmearLeavesRealLossInPlace(t *testing.T) { c := bucketWireCap bytes := []uint64{0, c, 0, 2 * c} frames := make([]uint64, 4) settleAll(frames, bytes) for i, want := range []uint64{0, c, c, c} { if bytes[i] != want { t.Errorf("bucket %d = %d bytes, want %d", i, bytes[i], want) } } } // A deficit after the excess is the host not having read those frames yet, not // jitter to repair: nothing ever moves forward. func TestSmearNeverMovesForward(t *testing.T) { c := bucketWireCap bytes := []uint64{c, 2 * c, 0} frames := make([]uint64, 3) settleAll(frames, bytes) for i, want := range []uint64{c, 2 * c, 0} { if bytes[i] != want { t.Errorf("bucket %d = %d bytes, want %d", i, bytes[i], want) } } } // Truly below line rate has nothing to move and displays as-is. func TestSmearLeavesBelowRateAlone(t *testing.T) { c := bucketWireCap bytes := []uint64{c / 2, c / 4, c / 2} frames := []uint64{100, 50, 100} want := []uint64{c / 2, c / 4, c / 2} settleAll(frames, bytes) for i := range bytes { if bytes[i] != want[i] { t.Errorf("bucket %d = %d bytes, want untouched %d", i, bytes[i], want[i]) } } } // Frames ride along with the bytes in the donor's own proportion, and nothing // is invented or lost in the move. func TestSmearConservesFramesAndBytes(t *testing.T) { c := bucketWireCap donorFrames := uint64(1000) donorBytes := 2*c - donorFrames*wireOverhead frames := []uint64{0, donorFrames} bytes := []uint64{0, donorBytes} settleAll(frames, bytes) if tf := frames[0] + frames[1]; tf != donorFrames { t.Errorf("total frames = %d, want %d conserved", tf, donorFrames) } if tb := bytes[0] + bytes[1]; tb != donorBytes { t.Errorf("total bytes = %d, want %d conserved", tb, donorBytes) } if got := bytes[0] + frames[0]*wireOverhead; got != c { t.Errorf("refilled bucket = %d wire bytes, want exactly %d", got, c) } if frames[0] != donorFrames/2 { t.Errorf("moved frames = %d, want the donor's proportion %d", frames[0], donorFrames/2) } if got := bytes[1] + frames[1]*wireOverhead; got != c { t.Errorf("donor left = %d wire bytes, want trimmed to %d", got, c) } } // Indexed oldest first, so the error window still spans the whole ring once it // has wrapped. func TestRateWindowIndexesOldestFirst(t *testing.T) { w := newRateWindow(3) for i := 1; i <= 5; i++ { w.push(counterSet{s: sample{rxFrames: uint64(i)}}) } if got := w.count(); got != 3 { t.Fatalf("count = %d, want 3", got) } if got := w.at(0).s.rxFrames; got != 3 { t.Errorf("oldest = %d, want 3", got) } if got := w.at(2).s.rxFrames; got != 5 { t.Errorf("newest = %d, want 5", got) } } // Which counter feeds which bucket is the whole taxonomy the panel reports, so // it is pinned here rather than left to whoever reads errsBetween next. func TestErrsBetweenBuckets(t *testing.T) { n := counterSet{ s: sample{ lost: 1, late: 7, crcErr: 2, badMagic: 3, badLen: 4, badHdr: 10, txErrs: 6, rxErrs: 5, }, drops: 9, nic: 8, } got := errsBetween(counterSet{}, n) want := errs{lost: 1, corrupt: 2 + 3 + 4 + 10, link: 8 + 5, internal: 9 + 7 + 6} if got != want { t.Errorf("errsBetween = %+v, want %+v", got, want) } if got.total() != 55 { t.Errorf("total = %d, want 55", got.total()) } } // A reset re-bases from a fresh capture while the ring still holds buckets from // just before it, so the newest bucket must not be left behind the new origin. func TestResetDoesNotUnderflowTotals(t *testing.T) { d := &direction{ win: newRateWindow(8), rxStats: []*rxStats{{}}, } d.rxStats[0].frames.Store(100) d.rxStats[0].bytes.Store(6400) d.sample() d.rxStats[0].frames.Store(150) d.rxStats[0].bytes.Store(9600) d.reset() v := d.displayView() if v.rxFrames != 0 { t.Errorf("rxFrames = %d, want 0", v.rxFrames) } if v.rxBytes != 0 { t.Errorf("rxBytes = %d, want 0", v.rxBytes) } // The rolling window and the rates are about now rather than since the // reset, so the buckets from before it have to survive. if got := d.win.count(); got < 2 { t.Errorf("ring holds %d buckets after reset, want the pre-reset history kept", got) } }