Turn the display portrait and split it into live and since-reset panels
This commit is contained in:
@@ -58,8 +58,14 @@ type direction struct {
|
||||
drops uint64
|
||||
errBase sample
|
||||
dropBase uint64
|
||||
nicRaw uint64
|
||||
nicNow uint64
|
||||
nicBase uint64
|
||||
|
||||
recent errs
|
||||
recentBase sample
|
||||
recentDrops uint64
|
||||
recentNic uint64
|
||||
}
|
||||
|
||||
// Smoothing has to be steady against high-frequency noise yet still chase a
|
||||
@@ -84,7 +90,13 @@ type rateEstimator struct {
|
||||
n int
|
||||
}
|
||||
|
||||
func (e *rateEstimator) update(x float64) {
|
||||
// While the rate window is still growing it already averages everything there
|
||||
// is, so smoothing it again would only average in the startup ramp twice.
|
||||
func (e *rateEstimator) update(x float64, windowFull bool) {
|
||||
if !windowFull {
|
||||
e.est, e.shown, e.mad, e.n = x, x, 0, 0
|
||||
return
|
||||
}
|
||||
if e.n == 0 {
|
||||
e.est, e.shown, e.n = x, x, 1
|
||||
return
|
||||
@@ -147,6 +159,27 @@ type rateSample struct {
|
||||
txFrames, txBytes, rxFrames, rxBytes uint64
|
||||
}
|
||||
|
||||
// Late is counted but left out of the total, since a reordered frame arrived.
|
||||
type errs struct {
|
||||
lost, late uint64
|
||||
crc, badMagic uint64
|
||||
badLen uint64
|
||||
kdrop, link uint64
|
||||
}
|
||||
|
||||
func (e errs) total() uint64 {
|
||||
return e.lost + e.crc + e.badMagic + e.badLen + e.kdrop + e.link
|
||||
}
|
||||
|
||||
func (e errs) add(o errs) errs {
|
||||
return errs{
|
||||
lost: e.lost + o.lost, late: e.late + o.late,
|
||||
crc: e.crc + o.crc, badMagic: e.badMagic + o.badMagic,
|
||||
badLen: e.badLen + o.badLen,
|
||||
kdrop: e.kdrop + o.kdrop, link: e.link + o.link,
|
||||
}
|
||||
}
|
||||
|
||||
// A sliding window: the counters are sampled every frame and the rate is taken
|
||||
// across the whole window, so the figure moves every frame while still being
|
||||
// measured over a long enough span to be steady.
|
||||
@@ -311,40 +344,51 @@ var intervalCols = []colSpec{
|
||||
// One interval's numbers, shared by the console table and the framebuffer so
|
||||
// both always show the same figures.
|
||||
type view struct {
|
||||
txPPS, rxPPS float64
|
||||
txGbps, rxGbps float64
|
||||
txFrames, txSent uint64
|
||||
rxFrames, rxGot uint64
|
||||
lost, late uint64
|
||||
crc, badMagic uint64
|
||||
kdrop, link uint64
|
||||
errors uint64
|
||||
cable cableView
|
||||
txPPS, rxPPS float64
|
||||
txGbps, rxGbps float64
|
||||
rxFrames, rxGot uint64
|
||||
since errs
|
||||
window errs
|
||||
cable cableView
|
||||
}
|
||||
|
||||
// Cumulative fields, which need no rate window and are identical for both the
|
||||
// console and the display.
|
||||
func (d *direction) counters(now sample) view {
|
||||
b := d.errBase
|
||||
v := view{
|
||||
txFrames: now.txFrames - b.txFrames,
|
||||
txSent: now.txBytes - b.txBytes,
|
||||
return view{
|
||||
rxFrames: now.rxFrames - b.rxFrames,
|
||||
rxGot: now.rxBytes - b.rxBytes,
|
||||
lost: now.lost - b.lost,
|
||||
late: now.late - b.late,
|
||||
crc: now.crcErr - b.crcErr,
|
||||
badMagic: now.badMagic - b.badMagic,
|
||||
kdrop: d.drops - d.dropBase,
|
||||
cable: d.cable.view(),
|
||||
since: errs{
|
||||
lost: now.lost - b.lost,
|
||||
late: now.late - b.late,
|
||||
crc: now.crcErr - b.crcErr,
|
||||
badMagic: now.badMagic - b.badMagic,
|
||||
badLen: now.badLen - b.badLen,
|
||||
kdrop: d.drops - d.dropBase,
|
||||
// A frame the stack refused and a frame the driver dropped are the same
|
||||
// failure seen from either side of the ring, and never the same frame
|
||||
// twice: a send that fails never reaches the driver to be dropped.
|
||||
link: d.nicNow - d.nicBase +
|
||||
(now.txErrs - b.txErrs) + (now.rxErrs - b.rxErrs),
|
||||
},
|
||||
}
|
||||
// A frame the stack refused and a frame the driver dropped are the same
|
||||
// failure seen from either side of the ring, and never the same frame twice:
|
||||
// a send that fails never reaches the driver to be dropped.
|
||||
v.link = d.nicNow - d.nicBase +
|
||||
(now.txErrs - b.txErrs) + (now.rxErrs - b.rxErrs)
|
||||
v.errors = v.lost + v.crc + v.badMagic + (now.badLen - b.badLen) + v.kdrop + v.link
|
||||
return v
|
||||
}
|
||||
|
||||
func totalView(views []view) view {
|
||||
var t view
|
||||
for _, v := range views {
|
||||
t.txPPS += v.txPPS
|
||||
t.rxPPS += v.rxPPS
|
||||
t.txGbps += v.txGbps
|
||||
t.rxGbps += v.rxGbps
|
||||
t.rxFrames += v.rxFrames
|
||||
t.rxGot += v.rxGot
|
||||
t.since = t.since.add(v.since)
|
||||
t.window = t.window.add(v.window)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (d *direction) view(prev *sample, secs float64) view {
|
||||
@@ -369,8 +413,9 @@ func (d *direction) displayView(t time.Time) view {
|
||||
d.win.push(rateSample{t, now.txFrames, now.txBytes, now.rxFrames, now.rxBytes})
|
||||
|
||||
v := d.counters(now)
|
||||
v.txFrames = d.heldFrames.get(t, v.txFrames)
|
||||
v.txSent = d.heldSent.get(t, v.txSent)
|
||||
v.window = d.recent
|
||||
v.rxFrames = d.heldFrames.get(t, v.rxFrames)
|
||||
v.rxGot = d.heldSent.get(t, v.rxGot)
|
||||
|
||||
o, n, ok := d.win.span()
|
||||
if !ok {
|
||||
@@ -382,10 +427,11 @@ func (d *direction) displayView(t time.Time) view {
|
||||
}
|
||||
txF := n.txFrames - o.txFrames
|
||||
rxF := n.rxFrames - o.rxFrames
|
||||
d.est.txPPS.update(float64(txF) / secs)
|
||||
d.est.rxPPS.update(float64(rxF) / secs)
|
||||
d.est.txGbps.update(gbps(n.txBytes-o.txBytes, txF, secs))
|
||||
d.est.rxGbps.update(gbps(n.rxBytes-o.rxBytes, rxF, secs))
|
||||
full := d.win.filled
|
||||
d.est.txPPS.update(float64(txF)/secs, full)
|
||||
d.est.rxPPS.update(float64(rxF)/secs, full)
|
||||
d.est.txGbps.update(gbps(n.txBytes-o.txBytes, txF, secs), full)
|
||||
d.est.rxGbps.update(gbps(n.rxBytes-o.rxBytes, rxF, secs), full)
|
||||
|
||||
v.txPPS = d.est.txPPS.value()
|
||||
v.rxPPS = d.est.rxPPS.value()
|
||||
@@ -402,24 +448,64 @@ func (d *direction) row(elapsed time.Duration, v view, target float64, length st
|
||||
rateCell(v.txGbps, target),
|
||||
commas(uint64(v.rxPPS)),
|
||||
rateCell(v.rxGbps, target),
|
||||
statusCell(v.lost),
|
||||
statusCell(v.late),
|
||||
statusCell(v.crc),
|
||||
statusCell(v.badMagic),
|
||||
statusCell(v.kdrop),
|
||||
statusCell(v.link),
|
||||
statusCell(v.errors),
|
||||
statusCell(v.since.lost),
|
||||
statusCell(v.since.late),
|
||||
statusCell(v.since.crc),
|
||||
statusCell(v.since.badMagic),
|
||||
statusCell(v.since.kdrop),
|
||||
statusCell(v.since.link),
|
||||
statusCell(v.since.total()),
|
||||
paint(v.cable.minText(), cCyan),
|
||||
paint(length, cCyan),
|
||||
}
|
||||
}
|
||||
|
||||
// Sampled once a second, since these are sysfs reads; the display uses whatever
|
||||
// the last sample left behind.
|
||||
// Sampled once a second, since these are sysfs reads. The recent errors roll
|
||||
// here rather than over the rate window because the nic counters only move at
|
||||
// this rate, and a shorter span would alias them into a flicker.
|
||||
func (d *direction) sampleNIC() {
|
||||
d.accumulateNIC()
|
||||
|
||||
now := d.snapshot()
|
||||
b := d.recentBase
|
||||
d.recent = errs{
|
||||
lost: now.lost - b.lost,
|
||||
late: now.late - b.late,
|
||||
crc: now.crcErr - b.crcErr,
|
||||
badMagic: now.badMagic - b.badMagic,
|
||||
badLen: now.badLen - b.badLen,
|
||||
kdrop: d.drops - d.recentDrops,
|
||||
link: d.nicNow - d.recentNic +
|
||||
(now.txErrs - b.txErrs) + (now.rxErrs - b.rxErrs),
|
||||
}
|
||||
d.recentBase, d.recentDrops, d.recentNic = now, d.drops, d.nicNow
|
||||
}
|
||||
|
||||
func (d *direction) readNICTotal() uint64 {
|
||||
tx := readNIC(d.tx.name)
|
||||
rx := readNIC(d.rx.name)
|
||||
d.nicNow = tx.tx + rx.rx + tx.carrierDown
|
||||
return tx.tx + rx.rx + tx.carrierDown
|
||||
}
|
||||
|
||||
// Sysfs nic counters run from boot and restart from zero whenever the driver
|
||||
// resets its statistics, so only their forward motion is accumulated. Taking
|
||||
// raw differences instead charges a boot's worth of errors to the first sample
|
||||
// and turns a reset into a near-2^64 underflow.
|
||||
func (d *direction) accumulateNIC() {
|
||||
raw := d.readNICTotal()
|
||||
if raw > d.nicRaw {
|
||||
d.nicNow += raw - d.nicRaw
|
||||
}
|
||||
d.nicRaw = raw
|
||||
}
|
||||
|
||||
// Whatever the interfaces counted before now is not ours, and no interval has
|
||||
// elapsed yet, so every baseline starts here and nothing is reported until the
|
||||
// first one completes.
|
||||
func (d *direction) primeCounters() {
|
||||
d.nicRaw = d.readNICTotal()
|
||||
d.reset()
|
||||
d.recentBase, d.recentDrops, d.recentNic = d.snapshot(), d.drops, d.nicNow
|
||||
}
|
||||
|
||||
func buildDirection(label string, tx, rx endpoint, sizes []int, cfg config) (*direction, error) {
|
||||
@@ -471,9 +557,6 @@ func buildDirection(label string, tx, rx endpoint, sizes []int, cfg config) (*di
|
||||
}
|
||||
d.probeRxFD = fd
|
||||
|
||||
// Whatever the interfaces have counted before now is not ours.
|
||||
d.sampleNIC()
|
||||
d.nicBase = d.nicNow
|
||||
return d, nil
|
||||
}
|
||||
|
||||
@@ -550,7 +633,7 @@ func main() {
|
||||
streams = flag.Int("streams", 7, "independent streams per direction, capped by rx rings; each gets its own ethertype, steered by a flow rule to its own rx queue")
|
||||
batch = flag.Int("batch", 64, "frames per sendmmsg/recvmmsg call")
|
||||
|
||||
nsPerM = flag.Float64("ns-per-m", 5.3, "mean of both directions, per metre of cable")
|
||||
nsPerM = flag.Float64("ns-per-m", 5.2, "mean of both directions, per metre of cable")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
@@ -703,11 +786,15 @@ func run(aName, bName, sizesArg string,
|
||||
}
|
||||
defer disp.close()
|
||||
|
||||
touch, err := watchTouch(disp.fb.w, disp.fb.h)
|
||||
touch, err := watchTouch(disp.fb.pw, disp.fb.ph)
|
||||
if err != nil {
|
||||
return fmt.Errorf("touchscreen: %w", err)
|
||||
}
|
||||
|
||||
for _, d := range dirs {
|
||||
d.primeCounters()
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
close(startTx)
|
||||
tick := time.NewTicker(reportInterval)
|
||||
@@ -733,13 +820,20 @@ func run(aName, bName, sizesArg string,
|
||||
case <-space:
|
||||
start = resetAll(dirs, stats)
|
||||
case now := <-frame.C:
|
||||
if x, y, down := touch.get(); disp.holdReset(x, y, down, now) {
|
||||
px, py, down := touch.get()
|
||||
x, y := disp.fb.fromPanel(px, py)
|
||||
if disp.holdReset(x, y, down, now) {
|
||||
start = resetAll(dirs, stats)
|
||||
}
|
||||
for i, d := range dirs {
|
||||
views[i] = d.displayView(now)
|
||||
}
|
||||
disp.render(dirs, views, now.Sub(start), target, cfg.cableText(views))
|
||||
cable := "-"
|
||||
if m, ok := cfg.cableMetres(views); ok {
|
||||
cable = fmt.Sprintf("%.1f m", m)
|
||||
}
|
||||
disp.render(totalView(views), now.Sub(start),
|
||||
target*float64(len(dirs)), cable)
|
||||
case now := <-tick.C:
|
||||
secs := now.Sub(last).Seconds()
|
||||
last = now
|
||||
|
||||
Reference in New Issue
Block a user