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
+35 -20
View File
@@ -31,6 +31,9 @@ const (
viScreenInfoLen = 40 viScreenInfoLen = 40
) )
// w and h are the logical canvas, which is portrait; pw and ph are the panel,
// which is landscape. Every draw is turned a quarter turn on its way to memory,
// so logical top lands on the panel's right edge.
type framebuffer struct { type framebuffer struct {
file *os.File file *os.File
tty *os.File tty *os.File
@@ -38,12 +41,22 @@ type framebuffer struct {
back []byte back []byte
w int w int
h int h int
pw int
ph int
stride int stride int
rShift uint rShift uint
gShift uint gShift uint
bShift uint bShift uint
} }
func (fb *framebuffer) offset(x, y int) int {
return x*fb.stride + (fb.pw-1-y)*4
}
func (fb *framebuffer) fromPanel(x, y int) (int, int) {
return y, fb.pw - 1 - x
}
// The kernel console draws into the same framebuffer, including a blinking // The kernel console draws into the same framebuffer, including a blinking
// cursor and any keyboard echo, which fights every frame we write. Putting the // cursor and any keyboard echo, which fights every frame we write. Putting the
// active console into graphics mode stops it touching the framebuffer at all. // active console into graphics mode stops it touching the framebuffer at all.
@@ -88,21 +101,23 @@ func openFramebuffer(path string) (*framebuffer, error) {
fb := &framebuffer{ fb := &framebuffer{
file: f, file: f,
w: int(vi[viXres]), pw: int(vi[viXres]),
h: int(vi[viYres]), ph: int(vi[viYres]),
w: int(vi[viYres]),
h: int(vi[viXres]),
stride: int(stride), stride: int(stride),
rShift: uint(vi[viRedOffset]), rShift: uint(vi[viRedOffset]),
gShift: uint(vi[viGreenOffset]), gShift: uint(vi[viGreenOffset]),
bShift: uint(vi[viBlueOffset]), bShift: uint(vi[viBlueOffset]),
} }
fb.mem, err = unix.Mmap(int(f.Fd()), 0, fb.stride*fb.h, fb.mem, err = unix.Mmap(int(f.Fd()), 0, fb.stride*fb.ph,
unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED) unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
if err != nil { if err != nil {
f.Close() f.Close()
return nil, fmt.Errorf("mmap: %w", err) return nil, fmt.Errorf("mmap: %w", err)
} }
fb.back = make([]byte, fb.stride*fb.h) fb.back = make([]byte, fb.stride*fb.ph)
fb.tty, err = takeConsole() fb.tty, err = takeConsole()
if err != nil { if err != nil {
@@ -139,28 +154,28 @@ func (fb *framebuffer) fill(c rgb) {
row[x+2] = byte(v >> 16) row[x+2] = byte(v >> 16)
row[x+3] = byte(v >> 24) row[x+3] = byte(v >> 24)
} }
for y := 0; y < fb.h; y++ { for y := 0; y < fb.ph; y++ {
copy(fb.back[y*fb.stride:], row) copy(fb.back[y*fb.stride:], row)
} }
} }
// One logical column is contiguous after the turn, so it fills a span at a time.
func (fb *framebuffer) rect(x0, y0, w, h int, c rgb) { func (fb *framebuffer) rect(x0, y0, w, h int, c rgb) {
x1, y1 := min(x0+w, fb.w), min(y0+h, fb.h)
x0, y0 = max(x0, 0), max(y0, 0)
if x0 >= x1 || y0 >= y1 {
return
}
v := fb.pixel(c) v := fb.pixel(c)
for y := y0; y < y0+h; y++ { span := make([]byte, (y1-y0)*4)
if y < 0 || y >= fb.h { for i := 0; i+4 <= len(span); i += 4 {
continue span[i+0] = byte(v)
} span[i+1] = byte(v >> 8)
base := y * fb.stride span[i+2] = byte(v >> 16)
for x := x0; x < x0+w; x++ { span[i+3] = byte(v >> 24)
if x < 0 || x >= fb.w {
continue
}
o := base + x*4
fb.back[o+0] = byte(v)
fb.back[o+1] = byte(v >> 8)
fb.back[o+2] = byte(v >> 16)
fb.back[o+3] = byte(v >> 24)
} }
for x := x0; x < x1; x++ {
copy(fb.back[fb.offset(x, y1-1):], span)
} }
} }
@@ -169,7 +184,7 @@ func (fb *framebuffer) blend(x, y int, c rgb, cov uint8) {
if x < 0 || y < 0 || x >= fb.w || y >= fb.h || cov == 0 { if x < 0 || y < 0 || x >= fb.w || y >= fb.h || cov == 0 {
return return
} }
o := y*fb.stride + x*4 o := fb.offset(x, y)
if cov == 255 { if cov == 255 {
v := fb.pixel(c) v := fb.pixel(c)
fb.back[o+0] = byte(v) fb.back[o+0] = byte(v)
+134 -40
View File
@@ -58,8 +58,14 @@ type direction struct {
drops uint64 drops uint64
errBase sample errBase sample
dropBase uint64 dropBase uint64
nicRaw uint64
nicNow uint64 nicNow uint64
nicBase uint64 nicBase uint64
recent errs
recentBase sample
recentDrops uint64
recentNic uint64
} }
// Smoothing has to be steady against high-frequency noise yet still chase a // Smoothing has to be steady against high-frequency noise yet still chase a
@@ -84,7 +90,13 @@ type rateEstimator struct {
n int 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 { if e.n == 0 {
e.est, e.shown, e.n = x, x, 1 e.est, e.shown, e.n = x, x, 1
return return
@@ -147,6 +159,27 @@ type rateSample struct {
txFrames, txBytes, rxFrames, rxBytes uint64 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 // 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 // across the whole window, so the figure moves every frame while still being
// measured over a long enough span to be steady. // measured over a long enough span to be steady.
@@ -313,12 +346,9 @@ var intervalCols = []colSpec{
type view struct { type view struct {
txPPS, rxPPS float64 txPPS, rxPPS float64
txGbps, rxGbps float64 txGbps, rxGbps float64
txFrames, txSent uint64
rxFrames, rxGot uint64 rxFrames, rxGot uint64
lost, late uint64 since errs
crc, badMagic uint64 window errs
kdrop, link uint64
errors uint64
cable cableView cable cableView
} }
@@ -326,25 +356,39 @@ type view struct {
// console and the display. // console and the display.
func (d *direction) counters(now sample) view { func (d *direction) counters(now sample) view {
b := d.errBase b := d.errBase
v := view{ return view{
txFrames: now.txFrames - b.txFrames,
txSent: now.txBytes - b.txBytes,
rxFrames: now.rxFrames - b.rxFrames, rxFrames: now.rxFrames - b.rxFrames,
rxGot: now.rxBytes - b.rxBytes, rxGot: now.rxBytes - b.rxBytes,
cable: d.cable.view(),
since: errs{
lost: now.lost - b.lost, lost: now.lost - b.lost,
late: now.late - b.late, late: now.late - b.late,
crc: now.crcErr - b.crcErr, crc: now.crcErr - b.crcErr,
badMagic: now.badMagic - b.badMagic, badMagic: now.badMagic - b.badMagic,
badLen: now.badLen - b.badLen,
kdrop: d.drops - d.dropBase, kdrop: d.drops - d.dropBase,
cable: d.cable.view(),
}
// A frame the stack refused and a frame the driver dropped are the same // 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: // failure seen from either side of the ring, and never the same frame
// a send that fails never reaches the driver to be dropped. // twice: a send that fails never reaches the driver to be dropped.
v.link = d.nicNow - d.nicBase + link: d.nicNow - d.nicBase +
(now.txErrs - b.txErrs) + (now.rxErrs - b.rxErrs) (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 { 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}) d.win.push(rateSample{t, now.txFrames, now.txBytes, now.rxFrames, now.rxBytes})
v := d.counters(now) v := d.counters(now)
v.txFrames = d.heldFrames.get(t, v.txFrames) v.window = d.recent
v.txSent = d.heldSent.get(t, v.txSent) v.rxFrames = d.heldFrames.get(t, v.rxFrames)
v.rxGot = d.heldSent.get(t, v.rxGot)
o, n, ok := d.win.span() o, n, ok := d.win.span()
if !ok { if !ok {
@@ -382,10 +427,11 @@ func (d *direction) displayView(t time.Time) view {
} }
txF := n.txFrames - o.txFrames txF := n.txFrames - o.txFrames
rxF := n.rxFrames - o.rxFrames rxF := n.rxFrames - o.rxFrames
d.est.txPPS.update(float64(txF) / secs) full := d.win.filled
d.est.rxPPS.update(float64(rxF) / secs) d.est.txPPS.update(float64(txF)/secs, full)
d.est.txGbps.update(gbps(n.txBytes-o.txBytes, txF, secs)) d.est.rxPPS.update(float64(rxF)/secs, full)
d.est.rxGbps.update(gbps(n.rxBytes-o.rxBytes, rxF, secs)) 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.txPPS = d.est.txPPS.value()
v.rxPPS = d.est.rxPPS.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), rateCell(v.txGbps, target),
commas(uint64(v.rxPPS)), commas(uint64(v.rxPPS)),
rateCell(v.rxGbps, target), rateCell(v.rxGbps, target),
statusCell(v.lost), statusCell(v.since.lost),
statusCell(v.late), statusCell(v.since.late),
statusCell(v.crc), statusCell(v.since.crc),
statusCell(v.badMagic), statusCell(v.since.badMagic),
statusCell(v.kdrop), statusCell(v.since.kdrop),
statusCell(v.link), statusCell(v.since.link),
statusCell(v.errors), statusCell(v.since.total()),
paint(v.cable.minText(), cCyan), paint(v.cable.minText(), cCyan),
paint(length, cCyan), paint(length, cCyan),
} }
} }
// Sampled once a second, since these are sysfs reads; the display uses whatever // Sampled once a second, since these are sysfs reads. The recent errors roll
// the last sample left behind. // 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() { 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) tx := readNIC(d.tx.name)
rx := readNIC(d.rx.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) { 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 d.probeRxFD = fd
// Whatever the interfaces have counted before now is not ours.
d.sampleNIC()
d.nicBase = d.nicNow
return d, nil 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") 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") 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() flag.Parse()
@@ -703,11 +786,15 @@ func run(aName, bName, sizesArg string,
} }
defer disp.close() defer disp.close()
touch, err := watchTouch(disp.fb.w, disp.fb.h) touch, err := watchTouch(disp.fb.pw, disp.fb.ph)
if err != nil { if err != nil {
return fmt.Errorf("touchscreen: %w", err) return fmt.Errorf("touchscreen: %w", err)
} }
for _, d := range dirs {
d.primeCounters()
}
start := time.Now() start := time.Now()
close(startTx) close(startTx)
tick := time.NewTicker(reportInterval) tick := time.NewTicker(reportInterval)
@@ -733,13 +820,20 @@ func run(aName, bName, sizesArg string,
case <-space: case <-space:
start = resetAll(dirs, stats) start = resetAll(dirs, stats)
case now := <-frame.C: 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) start = resetAll(dirs, stats)
} }
for i, d := range dirs { for i, d := range dirs {
views[i] = d.displayView(now) 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: case now := <-tick.C:
secs := now.Sub(last).Seconds() secs := now.Sub(last).Seconds()
last = now last = now
-9
View File
@@ -1,7 +1,6 @@
package main package main
import ( import (
"fmt"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -69,14 +68,6 @@ func (c config) cableMetres(views []view) (float64, bool) {
return excess / float64(len(views)) / c.nsPerM, true return excess / float64(len(views)) / c.nsPerM, true
} }
func (c config) cableText(views []view) string {
m, ok := c.cableMetres(views)
if !ok {
return ""
}
return fmt.Sprintf("cable %.1f m", m)
}
func newCableStats() *cableStats { func newCableStats() *cableStats {
return &cableStats{ return &cableStats{
txPend: make(map[uint64]int64, probePendCap), txPend: make(map[uint64]int64, probePendCap),
+94 -139
View File
@@ -8,39 +8,27 @@ import (
var ( var (
uiBg = rgb{0x12, 0x14, 0x18} uiBg = rgb{0x12, 0x14, 0x18}
uiPanel = rgb{0x1c, 0x20, 0x26} uiOKFill = rgb{0x18, 0x42, 0x26}
uiRule = rgb{0x2e, 0x34, 0x3c} uiOKEdge = rgb{0x3c, 0xe0, 0x70}
uiErrFil = rgb{0x54, 0x18, 0x1c}
uiErrEdg = rgb{0xff, 0x46, 0x46}
uiButton = rgb{0x25, 0x2b, 0x33} uiButton = rgb{0x25, 0x2b, 0x33}
uiFg = rgb{0xe6, 0xe8, 0xea} uiFg = rgb{0xe6, 0xe8, 0xea}
uiDim = rgb{0x7a, 0x82, 0x8c} uiDim = rgb{0x9a, 0xa2, 0xac}
uiCyan = rgb{0x5c, 0xc8, 0xe0} uiCyan = rgb{0x5c, 0xc8, 0xe0}
uiGreen = rgb{0x4c, 0xc2, 0x6a} uiGreen = rgb{0x6c, 0xdc, 0x86}
uiRed = rgb{0xe0, 0x4b, 0x4b} uiRed = rgb{0xf0, 0x6b, 0x6b}
uiYellow = rgb{0xe0, 0xb0, 0x40} uiYellow = rgb{0xe0, 0xb0, 0x40}
) )
// Everything tabular sits on one monospace cell grid and every number is
// right-aligned to a column end, so columns line up by construction.
const ( const (
uiMargin = 16 uiMargin = 16
uiPad = 14
uiBorder = 12
rowGap = 5
blockGap = 16
cellDirA = 0 btnW = 240
cellArrow = 2
cellDirB = 4
colTxGbEnd = 14
colTxPPSEnd = 27
colRxGbEnd = 37
colRxPPSEnd = 50
colFramesEnd = 18
colDataEnd = 27
colLostEnd = 38
colLateEnd = 46
colCRCEnd = 53
colKdropEnd = 62
btnW = 200
btnH = 64 btnH = 64
holdDuration = time.Second holdDuration = time.Second
) )
@@ -58,8 +46,11 @@ type display struct {
huge *textFace huge *textFace
grid *textFace grid *textFace
gridB *textFace gridB *textFace
small *textFace
nowPanel rect
sincePanel rect
nowH int
sinceH int
resetBtn rect resetBtn rect
holdStart time.Time holdStart time.Time
holdFrac float64 holdFrac float64
@@ -77,10 +68,9 @@ func newDisplay() (*display, error) {
bold bool bold bool
size float64 size float64
}{ }{
{&d.huge, true, 72}, {&d.huge, true, 60},
{&d.grid, false, 24}, {&d.grid, false, 22},
{&d.gridB, true, 24}, {&d.gridB, true, 22},
{&d.small, false, 18},
} { } {
face, err := loadFace(spec.bold, spec.size) face, err := loadFace(spec.bold, spec.size)
if err != nil { if err != nil {
@@ -94,9 +84,22 @@ func newDisplay() (*display, error) {
return nil, fmt.Errorf("grid faces disagree on cell width: %d vs %d", return nil, fmt.Errorf("grid faces disagree on cell width: %d vs %d",
d.grid.cellW, d.gridB.cellW) d.grid.cellW, d.gridB.cellW)
} }
// Guessed heights collide on a screen this small, so the split follows what
// the loaded faces actually measure.
gridLine := d.grid.cellH + rowGap
errH := len(errRows) * gridLine
d.nowH = d.huge.cellH + rowGap + gridLine + blockGap + errH
d.sinceH = 4*gridLine + blockGap + errH + blockGap + btnH
inner := fb.w - 2*uiMargin
chrome := 2 * (uiBorder + uiPad)
avail := fb.h - 2*uiMargin - blockGap
h1 := (avail-2*chrome)*d.nowH/(d.nowH+d.sinceH) + chrome
d.nowPanel = rect{uiMargin, uiMargin, inner, h1}
d.sincePanel = rect{uiMargin, uiMargin + h1 + blockGap, inner, avail - h1}
d.resetBtn = rect{ d.resetBtn = rect{
x: fb.w - uiMargin - btnW, x: d.sincePanel.x + (inner-btnW)/2,
y: fb.h - uiMargin - btnH, y: d.sincePanel.y + d.sincePanel.h - uiBorder - uiPad - btnH,
w: btnW, w: btnW,
h: btnH, h: btnH,
} }
@@ -163,28 +166,59 @@ func (d *display) close() {
d.fb.close() d.fb.close()
} }
func (d *display) cellX(c int) int {
return uiMargin + c*d.grid.cellW
}
func (d *display) at(c, y int, s string, col rgb) {
d.grid.draw(d.fb, d.cellX(c), y, s, col)
}
func (d *display) atBold(c, y int, s string, col rgb) {
d.gridB.draw(d.fb, d.cellX(c), y, s, col)
}
func (d *display) rightAt(cEnd, y int, s string, col rgb) {
d.grid.draw(d.fb, d.cellX(cEnd)-len([]rune(s))*d.grid.cellW, y, s, col)
}
func (d *display) right(f *textFace, xEnd, y int, s string, col rgb) { func (d *display) right(f *textFace, xEnd, y int, s string, col rgb) {
f.draw(d.fb, xEnd-len([]rune(s))*f.cellW, y, s, col) f.draw(d.fb, xEnd-len([]rune(s))*f.cellW, y, s, col)
} }
func (d *display) center(f *textFace, y int, s string, col rgb) { func (d *display) row(f *textFace, x, w, y int, label, value string, col rgb) int {
f.draw(d.fb, (d.fb.w-len([]rune(s))*f.cellW)/2, y, s, col) f.draw(d.fb, x, y, label, uiDim)
d.right(f, x+w, y, value, col)
return y + f.cellH + rowGap
}
func (d *display) rateRow(x, w, y int, label string, gb, target float64) int {
d.gridB.draw(d.fb, x, y+(d.huge.cellH-d.gridB.cellH)/2, label, uiDim)
d.right(d.huge, x+w, y, fmt.Sprintf("%.2f Gb/s", gb), rateColor(gb, target))
return y + d.huge.cellH + rowGap
}
var errRows = []struct {
label string
get func(errs) uint64
}{
{"lost", func(e errs) uint64 { return e.lost }},
{"late", func(e errs) uint64 { return e.late }},
{"crc", func(e errs) uint64 { return e.crc }},
{"bad magic", func(e errs) uint64 { return e.badMagic }},
{"bad length", func(e errs) uint64 { return e.badLen }},
{"kernel drops", func(e errs) uint64 { return e.kdrop }},
{"link", func(e errs) uint64 { return e.link }},
}
func (d *display) errBlock(x, w, y int, e errs) int {
for _, r := range errRows {
n := r.get(e)
y = d.row(d.grid, x, w, y, r.label, commas(n), errColor(n))
}
return y
}
func (d *display) panel(p rect, e errs, contentH int) (int, int, int) {
fill, edge := uiOKFill, uiOKEdge
if e.total() > 0 {
fill, edge = uiErrFil, uiErrEdg
}
d.fb.rect(p.x, p.y, p.w, p.h, edge)
d.fb.rect(p.x+uiBorder, p.y+uiBorder,
p.w-2*uiBorder, p.h-2*uiBorder, fill)
inset := uiBorder + uiPad
x, w := p.x+inset, p.w-2*inset
y := p.y + inset
if slack := (p.y + p.h - inset) - y - contentH; slack > 0 {
y += slack / 2
}
return x, w, y
} }
// Rendered rates are quantised so the text only changes when the value moves // Rendered rates are quantised so the text only changes when the value moves
@@ -216,101 +250,22 @@ func rateColor(gb, target float64) rgb {
} }
} }
func (d *display) render(dirs []*direction, views []view, elapsed time.Duration, target float64, cable string) { func (d *display) render(v view, elapsed time.Duration, target float64, cable string) {
fb := d.fb fb := d.fb
fb.fill(uiBg) fb.fill(uiBg)
d.small.draw(fb, uiMargin, 10, "cabletest", uiCyan) x, w, y := d.panel(d.nowPanel, v.window, d.nowH)
d.small.draw(fb, uiMargin+11*d.small.cellW, 10, y = d.rateRow(x, w, y, "RX", v.rxGbps, target)
fmt.Sprintf("%s %s %s %s", dirs[0].tx.tag, dirs[0].tx.name, y = d.row(d.grid, x, w, y, "packets/s", commas(roundPPS(v.rxPPS)), uiFg)
dirs[0].rx.tag, dirs[0].rx.name), uiDim) d.errBlock(x, w, y+blockGap, v.window)
d.right(d.small, fb.w-uiMargin, 10, uptime(elapsed), uiDim)
var total uint64 x, w, y = d.panel(d.sincePanel, v.since, d.sinceH)
for _, v := range views { y = d.row(d.grid, x, w, y, "uptime", uptime(elapsed), uiFg)
total += v.errors y = d.row(d.grid, x, w, y, "frames", commas(v.rxFrames), uiFg)
} y = d.row(d.grid, x, w, y, "data", humanBytes(v.rxGot), uiFg)
y = d.row(d.grid, x, w, y, "cable", cable, uiCyan)
bandY, bandH := 40, 150 d.errBlock(x, w, y+blockGap, v.since)
fb.rect(0, bandY, fb.w, bandH, uiPanel)
fb.rect(0, bandY, 8, bandH, errColor(total))
word, wc := "NO ERRORS", uiGreen
if total > 0 {
word, wc = fmt.Sprintf("%s ERRORS", commas(total)), uiRed
}
d.center(d.huge, bandY+(bandH-d.huge.cellH)/2, word, wc)
// Two tight blocks rather than rows spread over the whole panel, centred in
// what is left below the band.
lineH := d.grid.cellH + 4
sectionH := d.small.cellH + 10 + (1+len(dirs))*lineH
gap := 30
avail := fb.h - (bandY + bandH) - btnH - 2*uiMargin
y := bandY + bandH + 8 + (avail-2*sectionH-gap)/2
y = d.section(y, "RATE", "")
d.rightAt(colTxGbEnd, y, "TX Gb/s", uiDim)
d.rightAt(colTxPPSEnd, y, "TX pps", uiDim)
d.rightAt(colRxGbEnd, y, "RX Gb/s", uiDim)
d.rightAt(colRxPPSEnd, y, "RX pps", uiDim)
y += lineH
for i, dir := range dirs {
d.dirTag(dir, y)
v := views[i]
d.rightAt(colTxGbEnd, y, fmt.Sprintf("%.2f", v.txGbps), rateColor(v.txGbps, target))
d.rightAt(colTxPPSEnd, y, commas(roundPPS(v.txPPS)), uiFg)
d.rightAt(colRxGbEnd, y, fmt.Sprintf("%.2f", v.rxGbps), rateColor(v.rxGbps, target))
d.rightAt(colRxPPSEnd, y, commas(roundPPS(v.rxPPS)), uiFg)
y += lineH
}
y += gap
y = d.section(y, "OVERALL", cable)
d.rightAt(colFramesEnd, y, "frames", uiDim)
d.rightAt(colDataEnd, y, "data", uiDim)
d.rightAt(colLostEnd, y, "lost", uiDim)
d.rightAt(colLateEnd, y, "late", uiDim)
d.rightAt(colCRCEnd, y, "crc", uiDim)
d.rightAt(colKdropEnd, y, "kdrop", uiDim)
y += lineH
for i, dir := range dirs {
d.dirTag(dir, y)
v := views[i]
d.rightAt(colFramesEnd, y, commas(v.txFrames), uiFg)
d.rightAt(colDataEnd, y, humanBytes(v.txSent), uiFg)
d.rightAt(colLostEnd, y, commas(v.lost), errColor(v.lost))
d.rightAt(colLateEnd, y, commas(v.late), errColor(v.late))
d.rightAt(colCRCEnd, y, commas(v.crc), errColor(v.crc))
d.rightAt(colKdropEnd, y, commas(v.kdrop), errColor(v.kdrop))
y += lineH
}
d.drawResetButton() d.drawResetButton()
fb.flush() fb.flush()
} }
func (d *display) section(y int, title, right string) int {
d.small.draw(d.fb, uiMargin, y, title, uiFg)
if right != "" {
d.right(d.small, d.cellX(colKdropEnd), y, right, uiCyan)
}
ruleY := y + d.small.cellH + 3
d.fb.rect(uiMargin, ruleY, d.fb.w-2*uiMargin, 1, uiRule)
return ruleY + 7
}
func (d *display) dirTag(dir *direction, y int) {
d.atBold(cellDirA, y, dir.tx.tag, uiCyan)
d.arrow(d.cellX(cellArrow), y+d.grid.cellH/2, uiDim)
d.atBold(cellDirB, y, dir.rx.tag, uiCyan)
}
// Drawn because the font has no arrow glyph. Occupies exactly one cell so the
// grid is unaffected.
func (d *display) arrow(x, y int, c rgb) {
w := d.grid.cellW
d.fb.rect(x, y-1, w-5, 2, c)
for i := 0; i < 6; i++ {
d.fb.rect(x+w-6+i, y-5+i, 1, 11-2*i, c)
}
}