178 lines
4.4 KiB
Go
178 lines
4.4 KiB
Go
package main
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// The panel's recent-history ring: 18 slots of 5 seconds. Fault and corrected
|
|
// deltas arrive straight from the layers below the moment they clear the mask,
|
|
// and the per-frame judgments — measure state, rate, SNR — from the draw loop,
|
|
// so a slot has data within one tick of being born. Slots are wall-time and
|
|
// survive a reset — recently is recently, whatever the aggregates were
|
|
// re-based to — while the fault ages and since-reset extremes 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
|
|
}
|
|
|
|
type history struct {
|
|
mu sync.Mutex
|
|
start time.Time
|
|
slots [histSlots]histSlot
|
|
|
|
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
|
|
}
|
|
|
|
// Bucketing sits above the mask: only fault deltas that survived it arrive
|
|
// here, already flattened, so a slot is a plain sum.
|
|
func (h *history) addFaults(now time.Time, e errs) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
s := h.slot(int64(now.Sub(h.start) / histSlotSpan))
|
|
for i, c := range faultClasses {
|
|
if d := c.get(e); d > 0 {
|
|
s.faults[i] += d
|
|
h.lastFault[i] = now
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *history) addCorrected(now time.Time, d uint64) {
|
|
h.mu.Lock()
|
|
h.slot(int64(now.Sub(h.start)/histSlotSpan)).corrected += d
|
|
h.mu.Unlock()
|
|
}
|
|
|
|
// The per-frame judgments, from the same figures the panel paints.
|
|
func (h *history) sample(now time.Time, v view, phy phyDisplay,
|
|
measuring bool, target float64) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
s := h.slot(int64(now.Sub(h.start) / histSlotSpan))
|
|
|
|
if measuring {
|
|
s.measuring = true
|
|
return
|
|
}
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
// The wall-time slots stay: recently is recently. Everything judged against
|
|
// the reset origin clears.
|
|
func (h *history) reset() {
|
|
h.mu.Lock()
|
|
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
|
|
}
|
|
|
|
// The window's newest column is the slot containing now, taken from the draw
|
|
// clock rather than the last sample, so the grid rolls on the slot boundary
|
|
// itself.
|
|
func (h *history) view(now time.Time) histView {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
newest := int64(now.Sub(h.start) / histSlotSpan)
|
|
var out histView
|
|
for k := 0; k < histSlots; k++ {
|
|
seq := newest - 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
|
|
}
|