Turn the display portrait and split it into live and since-reset panels

This commit is contained in:
flamingcow
2026-07-26 10:27:41 -07:00
parent 4d102f377e
commit 102d53fb9b
4 changed files with 276 additions and 221 deletions
+142 -48
View File
@@ -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