Turn the display portrait and split it into live and since-reset panels
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
@@ -313,12 +346,9 @@ var intervalCols = []colSpec{
|
||||
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
|
||||
since errs
|
||||
window errs
|
||||
cable cableView
|
||||
}
|
||||
|
||||
@@ -326,25 +356,39 @@ type view struct {
|
||||
// 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,
|
||||
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,
|
||||
cable: d.cable.view(),
|
||||
}
|
||||
// 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
|
||||
// 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),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -69,14 +68,6 @@ func (c config) cableMetres(views []view) (float64, bool) {
|
||||
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 {
|
||||
return &cableStats{
|
||||
txPend: make(map[uint64]int64, probePendCap),
|
||||
|
||||
@@ -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,8 +46,11 @@ type display struct {
|
||||
huge *textFace
|
||||
grid *textFace
|
||||
gridB *textFace
|
||||
small *textFace
|
||||
|
||||
nowPanel rect
|
||||
sincePanel rect
|
||||
nowH int
|
||||
sinceH int
|
||||
resetBtn rect
|
||||
holdStart time.Time
|
||||
holdFrac float64
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user