Compare commits

..
3 Commits
7 changed files with 307 additions and 253 deletions
+36 -21
View File
@@ -31,6 +31,9 @@ const (
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 {
file *os.File
tty *os.File
@@ -38,12 +41,22 @@ type framebuffer struct {
back []byte
w int
h int
pw int
ph int
stride int
rShift uint
gShift 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
// 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.
@@ -88,21 +101,23 @@ func openFramebuffer(path string) (*framebuffer, error) {
fb := &framebuffer{
file: f,
w: int(vi[viXres]),
h: int(vi[viYres]),
pw: int(vi[viXres]),
ph: int(vi[viYres]),
w: int(vi[viYres]),
h: int(vi[viXres]),
stride: int(stride),
rShift: uint(vi[viRedOffset]),
gShift: uint(vi[viGreenOffset]),
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)
if err != nil {
f.Close()
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()
if err != nil {
@@ -139,28 +154,28 @@ func (fb *framebuffer) fill(c rgb) {
row[x+2] = byte(v >> 16)
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)
}
}
// 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) {
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)
for y := y0; y < y0+h; y++ {
if y < 0 || y >= fb.h {
continue
}
base := y * fb.stride
for x := x0; x < x0+w; x++ {
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)
}
span := make([]byte, (y1-y0)*4)
for i := 0; i+4 <= len(span); i += 4 {
span[i+0] = byte(v)
span[i+1] = byte(v >> 8)
span[i+2] = byte(v >> 16)
span[i+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 {
return
}
o := y*fb.stride + x*4
o := fb.offset(x, y)
if cov == 255 {
v := fb.pixel(c)
fb.back[o+0] = byte(v)
-8
View File
@@ -60,14 +60,6 @@ var patterns = []pattern{
}},
}
func patternNames() []string {
names := make([]string, len(patterns))
for i, p := range patterns {
names[i] = p.name
}
return names
}
// Every pattern is laid out at full length up front with its checksum at each
// size, so a sender picks a pattern by choosing a buffer, never by writing one.
type frameSpec struct {
+4 -3
View File
@@ -3,7 +3,8 @@ module g.fc.run/theater/cabletest
go 1.26.5
require (
golang.org/x/image v0.44.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.40.0 // indirect
golang.org/x/image v0.44.0
golang.org/x/sys v0.47.0
)
require golang.org/x/text v0.40.0 // indirect
+146 -54
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
}
@@ -539,7 +622,6 @@ type config struct {
streams int
batch int
probeEther uint16
zeroNS float64
nsPerM float64
}
@@ -550,13 +632,13 @@ func main() {
sizesArg = flag.String("sizes", "64,128,256,512,1024,1280,1514", "frame sizes in bytes, excluding FCS, cycled per packet")
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")
zeroNS = flag.Float64("zero-ns", 2163.77, "mean of both directions at zero cable length; belongs to the media adapters, recalibrate when they change")
nsPerM = flag.Float64("ns-per-m", 5.4545, "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()
if err := run(*aName, *bName, *sizesArg,
*streams, *batch, *zeroNS, *nsPerM); err != nil {
*streams, *batch, *nsPerM); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
@@ -574,7 +656,7 @@ const (
)
func run(aName, bName, sizesArg string,
nStreams, batch int, zeroNS, nsPerM float64) error {
nStreams, batch int, nsPerM float64) error {
if aName == "" || bName == "" {
return fmt.Errorf("both -a and -b are required")
@@ -629,7 +711,6 @@ func run(aName, bName, sizesArg string,
streams: nStreams,
batch: batch,
probeEther: uint16(etherBase + nStreams),
zeroNS: zeroNS,
nsPerM: nsPerM,
}
@@ -674,7 +755,7 @@ func run(aName, bName, sizesArg string,
nStreams, ethertypes[0], ethertypes[len(ethertypes)-1])},
{"probe", fmt.Sprintf("ethertype 0x%04x every %s", cfg.probeEther, probeInterval)},
{"batch", fmt.Sprintf("%d frames per syscall", batch)},
{"calibration", fmt.Sprintf("%g ns at zero length, %g ns/m", zeroNS, nsPerM)},
{"calibration", fmt.Sprintf("%g ns/m, zero taken from the shortest delay seen so far", nsPerM)},
{"buffers", fmt.Sprintf("sndbuf %s, rcvbuf %s",
humanBytes(uint64(sockBufSize(dirs[0].txFDs[0], unix.SO_SNDBUF))),
humanBytes(uint64(sockBufSize(dirs[0].rxFDs[0], unix.SO_RCVBUF))))},
@@ -705,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)
@@ -735,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
+13 -18
View File
@@ -1,7 +1,6 @@
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
@@ -34,18 +33,20 @@ type cableStats struct {
mu sync.Mutex
min int64
samples uint64
floor int64
txPend map[uint64]int64
rxPend map[uint64]int64
}
type cableView struct {
min int64
samples uint64
min int64
floor int64
ok bool
}
func (v cableView) minText() string {
if v.samples == 0 {
if !v.ok {
return "-"
}
return commasInt(v.min)
@@ -57,23 +58,14 @@ func (c config) cableMetres(views []view) (float64, bool) {
if len(views) == 0 {
return 0, false
}
var sum float64
var excess float64
for _, v := range views {
if v.cable.samples == 0 {
if !v.cable.ok {
return 0, false
}
sum += float64(v.cable.min)
excess += float64(v.cable.min - v.cable.floor)
}
mean := sum / float64(len(views))
return (mean - c.zeroNS) / 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)
return excess / float64(len(views)) / c.nsPerM, true
}
func newCableStats() *cableStats {
@@ -114,13 +106,16 @@ func (c *cableStats) put(seq uint64, ts int64, tx bool) {
if c.samples == 0 || delta < c.min {
c.min = delta
}
if c.floor == 0 || delta < c.floor {
c.floor = delta
}
c.samples++
}
func (c *cableStats) view() cableView {
c.mu.Lock()
defer c.mu.Unlock()
return cableView{c.min, c.samples}
return cableView{c.min, c.floor, c.samples > 0}
}
func (c *cableStats) reset() {
+10 -6
View File
@@ -199,14 +199,18 @@ func renderBox(title string, headers []string, rights []bool, rows [][]string) s
}
var b strings.Builder
if title != "" {
b.WriteString(paint(title, cBold+cCyan) + "\n")
writeln := func(s string) {
b.WriteString(s)
b.WriteByte('\n')
}
b.WriteString(line("┌", "┬", "┐") + "\n")
b.WriteString(rowText(headers, true) + "\n")
b.WriteString(line("├", "┼", "┤") + "\n")
if title != "" {
writeln(paint(title, cBold+cCyan))
}
writeln(line("┌", "┬", "┐"))
writeln(rowText(headers, true))
writeln(line("├", "┼", "┤"))
for _, r := range rows {
b.WriteString(rowText(r, false) + "\n")
writeln(rowText(r, false))
}
b.WriteString(line("└", "┴", "┘"))
return b.String()
+98 -143
View File
@@ -8,39 +8,27 @@ import (
var (
uiBg = rgb{0x12, 0x14, 0x18}
uiPanel = rgb{0x1c, 0x20, 0x26}
uiRule = rgb{0x2e, 0x34, 0x3c}
uiOKFill = rgb{0x18, 0x42, 0x26}
uiOKEdge = rgb{0x3c, 0xe0, 0x70}
uiErrFil = rgb{0x54, 0x18, 0x1c}
uiErrEdg = rgb{0xff, 0x46, 0x46}
uiButton = rgb{0x25, 0x2b, 0x33}
uiFg = rgb{0xe6, 0xe8, 0xea}
uiDim = rgb{0x7a, 0x82, 0x8c}
uiDim = rgb{0x9a, 0xa2, 0xac}
uiCyan = rgb{0x5c, 0xc8, 0xe0}
uiGreen = rgb{0x4c, 0xc2, 0x6a}
uiRed = rgb{0xe0, 0x4b, 0x4b}
uiGreen = rgb{0x6c, 0xdc, 0x86}
uiRed = rgb{0xf0, 0x6b, 0x6b}
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 (
uiMargin = 16
uiPad = 14
uiBorder = 12
rowGap = 5
blockGap = 16
cellDirA = 0
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
btnW = 240
btnH = 64
holdDuration = time.Second
)
@@ -58,12 +46,15 @@ type display struct {
huge *textFace
grid *textFace
gridB *textFace
small *textFace
resetBtn rect
holdStart time.Time
holdFrac float64
fired bool
nowPanel rect
sincePanel rect
nowH int
sinceH int
resetBtn rect
holdStart time.Time
holdFrac float64
fired bool
}
func newDisplay() (*display, error) {
@@ -77,10 +68,9 @@ func newDisplay() (*display, error) {
bold bool
size float64
}{
{&d.huge, true, 72},
{&d.grid, false, 24},
{&d.gridB, true, 24},
{&d.small, false, 18},
{&d.huge, true, 60},
{&d.grid, false, 22},
{&d.gridB, true, 22},
} {
face, err := loadFace(spec.bold, spec.size)
if err != nil {
@@ -94,9 +84,22 @@ func newDisplay() (*display, error) {
return nil, fmt.Errorf("grid faces disagree on cell width: %d vs %d",
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{
x: fb.w - uiMargin - btnW,
y: fb.h - uiMargin - btnH,
x: d.sincePanel.x + (inner-btnW)/2,
y: d.sincePanel.y + d.sincePanel.h - uiBorder - uiPad - btnH,
w: btnW,
h: btnH,
}
@@ -163,28 +166,59 @@ func (d *display) 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) {
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) {
f.draw(d.fb, (d.fb.w-len([]rune(s))*f.cellW)/2, y, s, col)
func (d *display) row(f *textFace, x, w, y int, label, value string, col rgb) int {
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
@@ -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.fill(uiBg)
d.small.draw(fb, uiMargin, 10, "cabletest", uiCyan)
d.small.draw(fb, uiMargin+11*d.small.cellW, 10,
fmt.Sprintf("%s %s %s %s", dirs[0].tx.tag, dirs[0].tx.name,
dirs[0].rx.tag, dirs[0].rx.name), uiDim)
d.right(d.small, fb.w-uiMargin, 10, uptime(elapsed), uiDim)
x, w, y := d.panel(d.nowPanel, v.window, d.nowH)
y = d.rateRow(x, w, y, "RX", v.rxGbps, target)
y = d.row(d.grid, x, w, y, "packets/s", commas(roundPPS(v.rxPPS)), uiFg)
d.errBlock(x, w, y+blockGap, v.window)
var total uint64
for _, v := range views {
total += v.errors
}
bandY, bandH := 40, 150
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
}
x, w, y = d.panel(d.sincePanel, v.since, d.sinceH)
y = d.row(d.grid, x, w, y, "uptime", uptime(elapsed), uiFg)
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)
d.errBlock(x, w, y+blockGap, v.since)
d.drawResetButton()
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)
}
}