Files
cabletest/history.go
T

187 lines
4.2 KiB
Go
Raw Normal View History

package main
import (
"sync"
"time"
)
// The panel's recent-history ring: 18 slots of 5 seconds, sampled once a
// second from the same figures the console row prints. Slots are wall-time
// and survive a reset — recently is recently, whatever the counters were
// re-based to — while the since-reset aggregates and fault ages clear.
const (
histSlots = 18
histSlotSpan = 5 * time.Second
histSpan = histSlots * histSlotSpan
// Line rate is a verdict here, not a number: the headline is smeared and
// briefly gated at startup, so anything close to the target counts.
rateOKFrac = 0.98
)
var faultClasses = []struct {
label string
noun string
get func(errs) uint64
}{
{"lost", "loss", func(e errs) uint64 { return e.lost }},
{"corrupt", "corrupt", func(e errs) uint64 { return e.corrupt }},
{"link", "link fault", func(e errs) uint64 { return e.link }},
{"internal", "internal", func(e errs) uint64 { return e.internal }},
}
type histSlot struct {
seq int64
sampled bool
measuring bool
faults [4]uint64
corrected uint64
rateLow bool
haveSNR bool
snrMin float64
noiseOn int
noiseN int
}
type history struct {
mu sync.Mutex
start time.Time
slots [histSlots]histSlot
seq int64
prevValid bool
prevSince errs
prevCorrected uint64
lastFault [4]time.Time
sinceRateLow bool
sinceHaveSNR bool
sinceSNRMin float64
}
func newHistory(now time.Time) *history {
return &history{start: now}
}
func (h *history) slot(seq int64) *histSlot {
s := &h.slots[seq%histSlots]
if s.seq != seq || !s.sampled {
*s = histSlot{seq: seq, sampled: true}
}
return s
}
// Counters only move forward between samples except across a reset, where
// they re-base to zero; a backward step is that re-base, not negative faults.
func delta(now, prev uint64) uint64 {
if now < prev {
return 0
}
return now - prev
}
func (h *history) sample(now time.Time, v view, phy phyDisplay, nv noiseView,
measuring bool, target float64) {
h.mu.Lock()
defer h.mu.Unlock()
h.seq = int64(now.Sub(h.start) / histSlotSpan)
s := h.slot(h.seq)
if h.prevValid {
for i, c := range faultClasses {
d := delta(c.get(v.since), c.get(h.prevSince))
s.faults[i] += d
if d > 0 {
h.lastFault[i] = now
}
}
s.corrected += delta(phy.corrected, h.prevCorrected)
}
h.prevValid = true
h.prevSince = v.since
h.prevCorrected = phy.corrected
if measuring {
s.measuring = true
} else {
if v.rxGbps < rateOKFrac*target {
s.rateLow = true
h.sinceRateLow = true
}
if phy.haveSNR {
if !s.haveSNR || phy.worstMargin < s.snrMin {
s.haveSNR, s.snrMin = true, phy.worstMargin
}
if !h.sinceHaveSNR || phy.worstMargin < h.sinceSNRMin {
h.sinceHaveSNR, h.sinceSNRMin = true, phy.worstMargin
}
}
}
s.noiseN++
if nv.on {
s.noiseOn++
}
}
// The wall-time slots stay: recently is recently. Everything judged against
// the reset origin clears.
func (h *history) reset() {
h.mu.Lock()
h.prevValid = false
h.lastFault = [4]time.Time{}
h.sinceRateLow = false
h.sinceHaveSNR = false
h.sinceSNRMin = 0
h.mu.Unlock()
}
type histView struct {
// Oldest first; a slot that predates the ring or was never sampled has
// sampled false and paints as unknown.
slots [histSlots]histSlot
lastFault [4]time.Time
sinceRateLow bool
sinceHaveSNR bool
sinceSNRMin float64
}
func (h *history) view() histView {
h.mu.Lock()
defer h.mu.Unlock()
var out histView
for k := 0; k < histSlots; k++ {
seq := h.seq - int64(histSlots-1-k)
if seq < 0 {
continue
}
s := h.slots[seq%histSlots]
if s.seq == seq && s.sampled {
out.slots[k] = s
}
}
out.lastFault = h.lastFault
out.sinceRateLow = h.sinceRateLow
out.sinceHaveSNR = h.sinceHaveSNR
out.sinceSNRMin = h.sinceSNRMin
return out
}
// The newest fault on record names the verdict; the cable's own verdict
// outranks traffic faults because it is the thing under test.
func (hv histView) newestFault(now time.Time) (i int, age time.Duration, ok bool) {
best := -1
for c, t := range hv.lastFault {
if t.IsZero() {
continue
}
if best < 0 || t.After(hv.lastFault[best]) {
best = c
}
}
if best < 0 {
return 0, 0, false
}
return best, now.Sub(hv.lastFault[best]), true
}