Group the display into instant and cumulative sections on a shared grid

This commit is contained in:
flamingcow
2026-07-25 19:48:15 -07:00
parent 7a9567da55
commit 892e855207
2 changed files with 140 additions and 86 deletions
-2
View File
@@ -469,8 +469,6 @@ func run(aName, bName, sizesArg, patArg string,
return fmt.Errorf("display: %w", err)
}
defer disp.close()
disp.config = fmt.Sprintf("%s sizes %s %d streams batch %d crc32c verify",
patterns[patIdx].name, strings.Join(sizeStrs, ","), nStreams, batch)
start := time.Now()
close(startTx)
+140 -84
View File
@@ -8,6 +8,7 @@ import (
var (
uiBg = rgb{0x12, 0x14, 0x18}
uiPanel = rgb{0x1c, 0x20, 0x26}
uiRule = rgb{0x2e, 0x34, 0x3c}
uiFg = rgb{0xe6, 0xe8, 0xea}
uiDim = rgb{0x7a, 0x82, 0x8c}
uiCyan = rgb{0x5c, 0xc8, 0xe0}
@@ -16,12 +17,34 @@ var (
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
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
)
type display struct {
fb *framebuffer
huge *textFace
big *textFace
small *textFace
config string
fb *framebuffer
huge *textFace
grid *textFace
gridB *textFace
small *textFace
}
func newDisplay() (*display, error) {
@@ -29,34 +52,58 @@ func newDisplay() (*display, error) {
if err != nil {
return nil, err
}
huge, err := loadFace(true, 46)
if err != nil {
fb.close()
return nil, err
d := &display{fb: fb}
for _, spec := range []struct {
dst **textFace
bold bool
size float64
}{
{&d.huge, true, 72},
{&d.grid, false, 24},
{&d.gridB, true, 24},
{&d.small, false, 18},
} {
face, err := loadFace(spec.bold, spec.size)
if err != nil {
fb.close()
return nil, err
}
*spec.dst = face
}
big, err := loadFace(true, 24)
if err != nil {
if d.grid.cellW != d.gridB.cellW {
fb.close()
return nil, err
return nil, fmt.Errorf("grid faces disagree on cell width: %d vs %d",
d.grid.cellW, d.gridB.cellW)
}
small, err := loadFace(false, 18)
if err != nil {
fb.close()
return nil, err
}
return &display{fb: fb, huge: huge, big: big, small: small}, nil
return d, nil
}
func (d *display) close() {
d.fb.close()
}
func (d *display) right(f *textFace, xEnd, y int, s string, c rgb) {
f.draw(d.fb, xEnd-len([]rune(s))*f.cellW, y, s, c)
func (d *display) cellX(c int) int {
return uiMargin + c*d.grid.cellW
}
func (d *display) center(f *textFace, x0, w, y int, s string, c rgb) {
f.draw(d.fb, x0+(w-len([]rune(s))*f.cellW)/2, y, s, c)
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 errColor(n uint64) rgb {
@@ -66,7 +113,7 @@ func errColor(n uint64) rgb {
return uiRed
}
func (d *display) rateColor(gb, target float64) rgb {
func rateColor(gb, target float64) rgb {
switch {
case gb >= target*rateGreenFrac:
return uiGreen
@@ -81,85 +128,94 @@ func (d *display) render(dirs []*direction, views []view, elapsed time.Duration,
fb := d.fb
fb.fill(uiBg)
d.small.draw(fb, 16, 12, "cabletest", uiCyan)
if len(dirs) > 0 {
d.small.draw(fb, 16+11*d.small.cellW, 12,
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-16, 12, uptime(elapsed), uiDim)
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)
var total uint64
for _, v := range views {
total += v.errors
}
// Status band, readable from across the room.
bandY, bandH := 44, 74
bandY, bandH := 40, 150
fb.rect(0, bandY, fb.w, bandH, uiPanel)
fb.rect(0, bandY, 6, bandH, errColor(total))
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, 0, fb.w, bandY+(bandH-d.huge.cellH)/2+2, word, wc)
d.center(d.huge, bandY+(bandH-d.huge.cellH)/2, word, wc)
panelY := bandY + bandH + 12
footerH := d.small.cellH + 16
panelH := (fb.h - panelY - footerH) / max(len(dirs), 1)
// 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) - 16
y := bandY + bandH + 8 + (avail-2*sectionH-gap)/2
y = d.section(y, "INSTANT", "this interval")
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.renderDirection(dir, views[i], 12, panelY+i*panelH, fb.w-24, panelH-10, target)
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(uint64(v.txPPS)), uiFg)
d.rightAt(colRxGbEnd, y, fmt.Sprintf("%.2f", v.rxGbps), rateColor(v.rxGbps, target))
d.rightAt(colRxPPSEnd, y, commas(uint64(v.rxPPS)), uiFg)
y += lineH
}
d.footer(fb.h - footerH + 4)
y += gap
y = d.section(y, "CUMULATIVE", "since reset")
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
}
fb.flush()
}
func (d *display) renderDirection(dir *direction, v view, x, y, w, h int, target float64) {
fb := d.fb
fb.rect(x, y, w, h, uiPanel)
// The font has no arrow glyph, so the direction marker is drawn.
d.big.draw(fb, x+14, y+12, dir.tx.tag, uiCyan)
d.arrow(x+14+2*d.big.cellW, y+12+d.big.cellH/2, 22, uiDim)
d.big.draw(fb, x+14+2*d.big.cellW+30, y+12, dir.rx.tag, uiCyan)
col := x + 110
d.rateBlock(col, y+10, "TX", v.txPPS, v.txGbps, target)
d.rateBlock(col+300, y+10, "RX", v.rxPPS, v.rxGbps, target)
ex := col + 620
d.errLine(ex, y+10, "lost", v.lost)
d.errLine(ex, y+10+d.small.cellH+2, "crc", v.crc)
d.errLine(ex, y+10+2*(d.small.cellH+2), "kdrop", v.kdrop)
d.errLine(ex, y+10+3*(d.small.cellH+2), "late", v.late)
totals := fmt.Sprintf("sent %s frames %s received %s frames %s",
commas(v.txFrames), humanBytes(v.txSent),
commas(v.rxFrames), humanBytes(v.rxGot))
d.small.draw(fb, x+14, y+h-d.small.cellH-8, totals, uiDim)
func (d *display) section(y int, title, sub string) int {
d.small.draw(d.fb, uiMargin, y, title, uiFg)
d.small.draw(d.fb, uiMargin+(len(title)+2)*d.small.cellW, y, sub, uiDim)
ruleY := y + d.small.cellH + 3
d.fb.rect(uiMargin, ruleY, d.fb.w-2*uiMargin, 1, uiRule)
return ruleY + 7
}
func (d *display) arrow(x, y, w int, c rgb) {
d.fb.rect(x, y-1, w-6, 3, c)
for i := 0; i < 7; i++ {
d.fb.rect(x+w-7+i, y-6+i, 1, 13-2*i, c)
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)
}
}
func (d *display) rateBlock(x, y int, label string, pps, gb, target float64) {
d.small.draw(d.fb, x, y, label, uiDim)
d.big.draw(d.fb, x+3*d.small.cellW, y-2,
fmt.Sprintf("%6.2f Gb/s", gb), d.rateColor(gb, target))
d.small.draw(d.fb, x+3*d.small.cellW, y+d.big.cellH,
fmt.Sprintf("%s pps", commas(uint64(pps))), uiFg)
}
func (d *display) footer(y int) {
d.small.draw(d.fb, 16, y, d.config, uiDim)
d.right(d.small, d.fb.w-16, y, "space resets counts", uiDim)
}
func (d *display) errLine(x, y int, label string, n uint64) {
d.small.draw(d.fb, x, y, label, uiDim)
d.small.draw(d.fb, x+7*d.small.cellW, y, commas(n), errColor(n))
}