Rework the panel onto SI figures, labelled chips and one spacing scale

This commit is contained in:
flamingcow
2026-08-01 16:07:44 -07:00
parent de2ff13eb6
commit 726c4c2445
4 changed files with 280 additions and 146 deletions
+20 -19
View File
@@ -303,17 +303,17 @@ func gbps(bytes, frames uint64, secs float64) float64 {
} }
var intervalCols = []colSpec{ var intervalCols = []colSpec{
{title: "UPTIME", width: 9, right: true}, {title: "ELAPSED", width: 9, right: true},
{title: "DIR", width: 5}, {title: "DIR", width: 5},
{title: "TX pps", width: 9, right: true}, {title: "TX packets/s", width: 12, right: true},
{title: "TX Gb/s", width: 7, right: true}, {title: "TX bits/s", width: 10, right: true},
{title: "RX pps", width: 9, right: true}, {title: "RX packets/s", width: 12, right: true},
{title: "RX Gb/s", width: 7, right: true}, {title: "RX bits/s", width: 10, right: true},
{title: "LOST", width: 11, right: true}, {title: "LOST", width: 9, right: true},
{title: "CORRUPT", width: 11, right: true}, {title: "CORRUPT", width: 9, right: true},
{title: "LINK", width: 11, right: true}, {title: "LINK", width: 9, right: true},
{title: "INTERNAL", width: 11, right: true}, {title: "INTERNAL", width: 9, right: true},
{title: "ERRORS", width: 13, right: true}, {title: "ERRORS", width: 9, right: true},
{title: "MIN ns", width: 9, right: true}, {title: "MIN ns", width: 9, right: true},
{title: "LEN m", width: 6, right: true}, {title: "LEN m", width: 6, right: true},
} }
@@ -430,12 +430,12 @@ func (d *direction) displayView(t time.Time) view {
func (d *direction) row(elapsed time.Duration, v view, target float64, length string) []string { func (d *direction) row(elapsed time.Duration, v view, target float64, length string) []string {
return []string{ return []string{
uptime(elapsed), scaleTime(elapsed),
paint(d.short, cCyan), paint(d.short, cCyan),
commas(uint64(v.txPPS)), scaleSI(v.txPPS),
rateCell(v.txGbps, target), rateCell(v.txGbps*1e9, target*1e9),
commas(uint64(v.rxPPS)), scaleSI(v.rxPPS),
rateCell(v.rxGbps, target), rateCell(v.rxGbps*1e9, target*1e9),
statusCell(v.since.lost), statusCell(v.since.lost),
statusCell(v.since.corrupt), statusCell(v.since.corrupt),
statusCell(v.since.link), statusCell(v.since.link),
@@ -801,12 +801,13 @@ func run(aName, bName, sizesArg string,
for i, d := range dirs { for i, d := range dirs {
views[i] = d.displayView(now) views[i] = d.displayView(now)
} }
cable := "-" // Empty until the probe has a stamp from each direction, so the
// panel shows nothing there rather than a placeholder.
cable := ""
if m, ok := cfg.cableMetres(views); ok { if m, ok := cfg.cableMetres(views); ok {
cable = fmt.Sprintf("%.1f m", m) cable = fmt.Sprintf("%.1f", m)
} }
if err := disp.render(totalView(views), now.Sub(start), if err := disp.render(totalView(views), now.Sub(start), cable); err != nil {
target*float64(len(dirs)), cable); err != nil {
return err return err
} }
case now := <-tick.C: case now := <-tick.C:
+38 -9
View File
@@ -57,11 +57,6 @@ func pad(s string, w int, right bool) string {
return s + strings.Repeat(" ", gap) return s + strings.Repeat(" ", gap)
} }
func uptime(d time.Duration) string {
total := int(d.Seconds())
return fmt.Sprintf("%d:%02d:%02d", total/3600, (total/60)%60, total%60)
}
func commas(v uint64) string { func commas(v uint64) string {
s := fmt.Sprintf("%d", v) s := fmt.Sprintf("%d", v)
if len(s) <= 3 { if len(s) <= 3 {
@@ -94,6 +89,37 @@ func humanBytes(b uint64) string {
return fmt.Sprintf("%.1f PB", v) return fmt.Sprintf("%.1f PB", v)
} }
// A figure with the letter for its magnitude, so the unit itself can stay a
// fixed word on the label and only the letter moves with the value. Below a
// thousand there is no letter and none is left dangling, since a trailing space
// would push the figure off centre.
func scaleSI(v float64) string {
for _, mag := range []string{"", "k", "M", "G", "T"} {
if v < 1000 {
if mag == "" {
return fmt.Sprintf("%.2f", v)
}
return fmt.Sprintf("%.2f %s", v, mag)
}
v /= 1000
}
return fmt.Sprintf("%.2f P", v)
}
// The same shape for time, whose magnitudes are sixties and twenty-fours rather
// than thousands. The letter changes with the value; the label does not.
func scaleTime(d time.Duration) string {
switch {
case d < time.Minute:
return fmt.Sprintf("%.2f s", d.Seconds())
case d < time.Hour:
return fmt.Sprintf("%.2f m", d.Minutes())
case d < 24*time.Hour:
return fmt.Sprintf("%.2f h", d.Hours())
}
return fmt.Sprintf("%.2f d", d.Hours()/24)
}
type colSpec struct { type colSpec struct {
title string title string
width int width int
@@ -216,6 +242,9 @@ func renderBox(title string, headers []string, rights []bool, rows [][]string) s
return b.String() return b.String()
} }
// Counted exactly rather than scaled: these are whole frames, and the figure
// that matters most is the small one. Scaled, a single lost frame and a
// thousand of them both read as 1.00, separated only by a letter.
func statusCell(v uint64) string { func statusCell(v uint64) string {
s := commas(v) s := commas(v)
if v == 0 { if v == 0 {
@@ -231,12 +260,12 @@ const (
rateYellowFrac = 0.80 rateYellowFrac = 0.80
) )
func rateCell(gb float64, target float64) string { func rateCell(bits float64, target float64) string {
s := fmt.Sprintf("%.2f", gb) s := scaleSI(bits)
switch { switch {
case gb >= target*rateGreenFrac: case bits >= target*rateGreenFrac:
return paint(s, cGreen) return paint(s, cGreen)
case gb >= target*rateYellowFrac: case bits >= target*rateYellowFrac:
return paint(s, cYellow) return paint(s, cYellow)
default: default:
return paint(s, cRed) return paint(s, cRed)
+30 -2
View File
@@ -26,6 +26,30 @@ type textFace struct {
cellH int cellH int
ascent int ascent int
cache map[rune]*glyph cache map[rune]*glyph
// Where a line of text starts and stops as far as the eye is concerned: the
// top of a digit or capital, down to the baseline. The cell is taller at
// both ends, reserving space above for accents nothing here uses and below
// for descenders, which hang past the line without being read as part of
// it. Laying out by the cell therefore puts visibly more air around text
// than around a bordered box the same distance away.
capTop int
lineH int
}
// Digits carry no ascender or descender, so the first row one marks is the top
// of the line and the baseline is the bottom.
func (t *textFace) measureLine() error {
g := t.glyph('0')
for y := 0; y < g.h; y++ {
for x := 0; x < g.w; x++ {
if g.cov[y*g.w+x] != 0 {
t.capTop, t.lineH = y, t.ascent-y
return nil
}
}
}
return fmt.Errorf("font rasterised no ink for a digit")
} }
func loadFace(bold bool, sizePx float64) (*textFace, error) { func loadFace(bold bool, sizePx float64) (*textFace, error) {
@@ -54,13 +78,17 @@ func loadFace(bold bool, sizePx float64) (*textFace, error) {
if !ok { if !ok {
return nil, fmt.Errorf("font has no digit glyphs") return nil, fmt.Errorf("font has no digit glyphs")
} }
return &textFace{ t := &textFace{
face: face, face: face,
cellW: adv.Ceil(), cellW: adv.Ceil(),
cellH: (m.Ascent + m.Descent).Ceil(), cellH: (m.Ascent + m.Descent).Ceil(),
ascent: m.Ascent.Ceil(), ascent: m.Ascent.Ceil(),
cache: make(map[rune]*glyph), cache: make(map[rune]*glyph),
}, nil }
if err := t.measureLine(); err != nil {
return nil, err
}
return t, nil
} }
// Rasterises once per rune and keeps the coverage mask, since the same few // Rasterises once per rune and keeps the coverage mask, since the same few
+192 -116
View File
@@ -17,27 +17,47 @@ var (
uiCyan = rgb{0x5c, 0xc8, 0xe0} uiCyan = rgb{0x5c, 0xc8, 0xe0}
uiGreen = rgb{0x6c, 0xdc, 0x86} uiGreen = rgb{0x6c, 0xdc, 0x86}
uiRed = rgb{0xf0, 0x6b, 0x6b} uiRed = rgb{0xf0, 0x6b, 0x6b}
uiYellow = rgb{0xe0, 0xb0, 0x40} )
// Every gap is a multiple of one step, so the spacing carries meaning: things
// a step apart belong together, things eight steps apart do not. Picking each
// number for itself is what produced a panel where a label could have gone with
// either the figure above it or the one below.
//
// These are distances actually seen, since layout measures a line of text from
// the top of a digit to the baseline rather than across a cell with accent and
// descender slack in it. Values that looked right when that slack was padding
// them out are too small once it is gone.
const (
step = 4
spaceTight = step * 2 // neighbouring chips
spaceGroup = step * 4 // a figure and its label, chip padding, block to block
spaceRow = step * 8 // one labelled pair and the next
) )
const ( const (
uiMargin = 16 uiMargin = spaceGroup
uiPad = 14 uiPad = spaceGroup
uiBorder = 12 uiBorder = step * 3
rowGap = 5 pairGap = spaceGroup
blockGap = 16 blockGap = spaceGroup
btnW = 240 btnW = 300
btnH = 64 btnH = 80
btnRadius = 10
btnBorder = 2
holdDuration = time.Second holdDuration = time.Second
chipCols = 2 // Shared by the chips and the button, which are the same object drawn at
chipPadY = 9 // different sizes.
chipGap = 8 chipRadius = spaceTight
chipRadius = 8
chipBorder = 2 chipBorder = 2
// One grid for the panel: the figures and the chips beneath them stand in
// the same columns because they are placed by the same arithmetic.
gridCols = 2
chipPadY = spaceGroup
chipGap = spaceTight
statRowGap = spaceRow
) )
type rect struct { type rect struct {
@@ -50,14 +70,14 @@ func (r rect) contains(x, y int) bool {
type display struct { type display struct {
fb *framebuffer fb *framebuffer
huge *textFace big *textFace
grid *textFace grid *textFace
gridB *textFace gridB *textFace
nowPanel rect nowPanel rect
sincePanel rect sincePanel rect
nowH int nowYs []int
sinceH int sinceYs []int
resetBtn rect resetBtn rect
holdStart time.Time holdStart time.Time
holdFrac float64 holdFrac float64
@@ -75,9 +95,9 @@ func newDisplay() (*display, error) {
bold bool bold bool
size float64 size float64
}{ }{
{&d.huge, true, 60}, {&d.big, true, 40},
{&d.grid, false, 22}, {&d.grid, false, 34},
{&d.gridB, true, 22}, {&d.gridB, true, 34},
} { } {
face, err := loadFace(spec.bold, spec.size) face, err := loadFace(spec.bold, spec.size)
if err != nil { if err != nil {
@@ -91,28 +111,63 @@ 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 // Guessed heights collide on a screen this small, so the layout follows what
// the loaded faces actually measure. // the loaded faces actually measure.
gridLine := d.grid.cellH + rowGap now := []int{d.statsH(d.big, 2), d.chipsH()}
errH := len(errRows) * gridLine since := []int{d.statsH(d.gridB, 4), d.countsH(), btnH}
d.nowH = d.huge.cellH + rowGap + gridLine + blockGap + d.chipsH()
d.sinceH = 4*gridLine + blockGap + errH + blockGap + btnH // One gap for the whole screen rather than one per panel: whatever is left
// after the blocks is divided between every gap in both of them, so the
// space above the first figure, between each block, and below the last is
// the same distance everywhere. Each panel is then sized to exactly the
// blocks it holds plus its share, which is also what puts the button in the
// flow instead of pinned to the bottom with the remainder above it.
// Vertically the frame is the border and nothing else: the gap is the only
// whitespace there is. Insetting by uiPad as well would add it to the gaps
// at the top and bottom of a panel but not to the ones between blocks,
// which is not equal spacing however evenly the remainder is divided.
gaps := len(now) + len(since) + 2
spare := fb.h - 2*uiMargin - blockGap - 4*uiBorder - sum(now) - sum(since)
if spare < 0 {
fb.close()
return nil, fmt.Errorf("panel content is %dpx taller than the screen", -spare)
}
gap := spare / gaps
inner := fb.w - 2*uiMargin inner := fb.w - 2*uiMargin
chrome := 2 * (uiBorder + uiPad) nowH := 2*uiBorder + sum(now) + (len(now)+1)*gap
avail := fb.h - 2*uiMargin - blockGap sinceH := 2*uiBorder + sum(since) + (len(since)+1)*gap
h1 := (avail-2*chrome)*d.nowH/(d.nowH+d.sinceH) + chrome d.nowPanel = rect{uiMargin, uiMargin, inner, nowH}
d.nowPanel = rect{uiMargin, uiMargin, inner, h1} d.sincePanel = rect{uiMargin, uiMargin + nowH + blockGap, inner, sinceH}
d.sincePanel = rect{uiMargin, uiMargin + h1 + blockGap, inner, avail - h1}
d.nowYs = stack(d.nowPanel.y+uiBorder+gap, now, gap)
d.sinceYs = stack(d.sincePanel.y+uiBorder+gap, since, gap)
d.resetBtn = rect{ d.resetBtn = rect{
x: d.sincePanel.x + (inner-btnW)/2, x: d.sincePanel.x + (inner-btnW)/2,
y: d.sincePanel.y + d.sincePanel.h - uiBorder - uiPad - btnH, y: d.sinceYs[len(d.sinceYs)-1],
w: btnW, w: btnW,
h: btnH, h: btnH,
} }
return d, nil return d, nil
} }
func sum(hs []int) int {
var t int
for _, h := range hs {
t += h
}
return t
}
func stack(y int, hs []int, gap int) []int {
ys := make([]int, len(hs))
for i, h := range hs {
ys[i] = y
y += h + gap
}
return ys
}
// Tracks a press and hold on the reset button, returning true once it has been // Tracks a press and hold on the reset button, returning true once it has been
// held long enough. Lifting or sliding off cancels, and the press has to be // held long enough. Lifting or sliding off cancels, and the press has to be
// released before it can arm again. // released before it can arm again.
@@ -141,23 +196,23 @@ func (d *display) holdReset(x, y int, down bool, now time.Time) bool {
// something to press, not something being reported. // something to press, not something being reported.
func (d *display) drawResetButton() { func (d *display) drawResetButton() {
r := d.resetBtn r := d.resetBtn
d.fb.roundRect(r.x, r.y, r.w, r.h, btnRadius, uiCyan) d.fb.roundRect(r.x, r.y, r.w, r.h, chipRadius, uiCyan)
d.fb.roundRect(r.x+btnBorder, r.y+btnBorder, r.w-2*btnBorder, r.h-2*btnBorder, d.fb.roundRect(r.x+chipBorder, r.y+chipBorder, r.w-2*chipBorder, r.h-2*chipBorder,
btnRadius-btnBorder, uiBg) chipRadius-chipBorder, uiBg)
// The hold fills the well rather than the whole button, so the outline stays // The hold fills the well rather than the whole button, so the outline stays
// put and it reads as the button filling up. // put and it reads as the button filling up.
split := r.x + btnBorder split := r.x + chipBorder
if d.holdFrac > 0 { if d.holdFrac > 0 {
w := int(float64(r.w-2*btnBorder) * math.Min(d.holdFrac, 1)) w := int(float64(r.w-2*chipBorder) * math.Min(d.holdFrac, 1))
d.fb.roundRect(r.x+btnBorder, r.y+btnBorder, w, r.h-2*btnBorder, d.fb.roundRect(r.x+chipBorder, r.y+chipBorder, w, r.h-2*chipBorder,
btnRadius-btnBorder, uiCyan) chipRadius-chipBorder, uiCyan)
split += w split += w
} }
label := "RESET" label := "RESET"
lx := r.x + (r.w-len(label)*d.gridB.cellW)/2 lx := r.x + (r.w-len(label)*d.gridB.cellW)/2
ly := r.y + (r.h-d.gridB.cellH)/2 ly := r.y + (r.h-d.gridB.lineH)/2 - d.gridB.capTop
// The label straddles the fill, so each glyph takes the colour that reads // The label straddles the fill, so each glyph takes the colour that reads
// against whatever is behind it. // against whatever is behind it.
for i, c := range label { for i, c := range label {
@@ -174,20 +229,54 @@ func (d *display) close() {
d.fb.close() d.fb.close()
} }
func (d *display) right(f *textFace, xEnd, y int, s string, col rgb) { // y is the top of the line as read, so text and a bordered box placed the same
f.draw(d.fb, xEnd-len([]rune(s))*f.cellW, y, s, col) // distance apart are the same distance apart to look at.
func (d *display) centerIn(f *textFace, x, w, y int, s string, col rgb) int {
f.draw(d.fb, x+(w-len([]rune(s))*f.cellW)/2, y-f.capTop, s, col)
return y + f.lineH + pairGap
} }
func (d *display) row(f *textFace, x, w, y int, label, value string, col rgb) int { type statCell struct {
f.draw(d.fb, x, y, label, uiDim) value string
d.right(f, x+w, y, value, col) label string
return y + f.cellH + rowGap col rgb
} }
func (d *display) rateRow(x, w, y int, label string, gb, target float64) int { func (d *display) statPairH(vf *textFace) int {
d.gridB.draw(d.fb, x, y+(d.huge.cellH-d.gridB.cellH)/2, label, uiDim) return vf.lineH + pairGap + d.grid.lineH
d.right(d.huge, x+w, y, fmt.Sprintf("%.2f Gb/s", gb), rateColor(gb, target)) }
return y + d.huge.cellH + rowGap
func gridRows(n int) int { return (n + gridCols - 1) / gridCols }
func (d *display) statsH(vf *textFace, n int) int {
return gridRows(n)*(d.statPairH(vf)+statRowGap) - statRowGap
}
// Where cell i of n falls in the panel's grid. A last row that does not fill
// the grid is centred, so the odd one out balances the rows above rather than
// hanging off the left of them.
func gridCell(i, n, x, w int) (cx, cw int) {
cw = (w - (gridCols-1)*chipGap) / gridCols
inRow := min(n-(i/gridCols)*gridCols, gridCols)
cx = x + (w-(inRow*cw+(inRow-1)*chipGap))/2 + (i%gridCols)*(cw+chipGap)
return cx, cw
}
// A figure with its label directly underneath, two to a row. The gap between
// rows is wider than the one inside a pair, so which label belongs to which
// figure is a matter of spacing rather than of guessing. An empty value takes
// its space without drawing, so nothing below moves when it arrives.
func (d *display) stats(vf *textFace, x, w, y int, cells []statCell) int {
for i, c := range cells {
if c.value == "" {
continue
}
cx, cw := gridCell(i, len(cells), x, w)
cy := y + (i/gridCols)*(d.statPairH(vf)+statRowGap)
ly := d.centerIn(vf, cx, cw, cy, c.value, c.col)
d.centerIn(d.grid, cx, cw, ly, c.label, uiDim)
}
return y + d.statsH(vf, len(cells))
} }
var errRows = []struct { var errRows = []struct {
@@ -200,19 +289,31 @@ var errRows = []struct {
{"internal", func(e errs) uint64 { return e.internal }}, {"internal", func(e errs) uint64 { return e.internal }},
} }
func (d *display) errBlock(x, w, y int, e errs) int { func (d *display) chipH() int { return d.grid.lineH + 2*chipPadY }
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) chipH() int { return d.grid.cellH + 2*chipPadY } // Taller by a line, since these carry the count under the kind.
func (d *display) countChipH() int { return d.chipH() + d.gridB.lineH + pairGap }
func (d *display) chipsH() int { func (d *display) chipsH() int {
rows := (len(errRows) + chipCols - 1) / chipCols return gridRows(len(errRows))*(d.chipH()+chipGap) - chipGap
return rows*(d.chipH()+chipGap) - chipGap }
func (d *display) countsH() int {
return gridRows(len(errRows))*(d.countChipH()+chipGap) - chipGap
}
// Shared by both panels so they are demonstrably the same object, one carrying
// a count and one not.
func (d *display) chipAt(i, x, w, y, h int, c rgb) (int, int, int) {
cx, cw := gridCell(i, len(errRows), x, w)
cy := y + (i/gridCols)*(h+chipGap)
// Outlined by drawing the border colour and then sinking a smaller well of
// background into it, so both curves get the same antialiasing.
d.fb.roundRect(cx, cy, cw, h, chipRadius, c)
d.fb.roundRect(cx+chipBorder, cy+chipBorder,
cw-2*chipBorder, h-2*chipBorder, chipRadius-chipBorder, uiBg)
return cx, cw, cy
} }
// The same kinds as errBlock, but answering whether rather than how many, and // The same kinds as errBlock, but answering whether rather than how many, and
@@ -220,30 +321,28 @@ func (d *display) chipsH() int {
// a window this short a count is a number nobody can read before it changes; // a window this short a count is a number nobody can read before it changes;
// the only thing worth knowing at a glance is which kinds are happening now. // the only thing worth knowing at a glance is which kinds are happening now.
func (d *display) errChips(x, w, y int, e errs) int { func (d *display) errChips(x, w, y int, e errs) int {
ch := d.chipH()
cw := (w - (chipCols-1)*chipGap) / chipCols
for i, r := range errRows { for i, r := range errRows {
col, row := i%chipCols, i/chipCols
// A last row with nothing to sit beside is centred, so the odd one out
// balances the rows above rather than hanging off the left of them.
n := min(len(errRows)-row*chipCols, chipCols)
cx := x + (w-(n*cw+(n-1)*chipGap))/2 + col*(cw+chipGap)
cy := y + row*(ch+chipGap)
// Outlined by drawing the border colour and then sinking a smaller well
// of background into it, so both curves get the same antialiasing.
c := errColor(r.get(e)) c := errColor(r.get(e))
d.fb.roundRect(cx, cy, cw, ch, chipRadius, c) cx, cw, cy := d.chipAt(i, x, w, y, d.chipH(), c)
d.fb.roundRect(cx+chipBorder, cy+chipBorder, d.centerIn(d.grid, cx, cw, cy+chipPadY, r.label, c)
cw-2*chipBorder, ch-2*chipBorder, chipRadius-chipBorder, uiBg)
tx := cx + (cw-len([]rune(r.label))*d.grid.cellW)/2
d.grid.draw(d.fb, tx, cy+chipPadY, r.label, c)
} }
return y + d.chipsH() return y + d.chipsH()
} }
func (d *display) panel(p rect, e errs, contentH int) (int, int, int) { func (d *display) errCounts(x, w, y int, e errs) int {
for i, r := range errRows {
n := r.get(e)
c := errColor(n)
cx, cw, cy := d.chipAt(i, x, w, y, d.countChipH(), c)
ty := d.centerIn(d.gridB, cx, cw, cy+chipPadY, commas(n), c)
d.centerIn(d.grid, cx, cw, ty, r.label, c)
}
return y + d.countsH()
}
// Draws the frame and hands back the writable width inside it. Where the blocks
// sit within it was settled once at startup, since it never changes.
func (d *display) panel(p rect, e errs) (int, int) {
fill, edge := uiOKFill, uiOKEdge fill, edge := uiOKFill, uiOKEdge
if e.total() > 0 { if e.total() > 0 {
fill, edge = uiErrFil, uiErrEdg fill, edge = uiErrFil, uiErrEdg
@@ -253,23 +352,7 @@ func (d *display) panel(p rect, e errs, contentH int) (int, int, int) {
p.w-2*uiBorder, p.h-2*uiBorder, fill) p.w-2*uiBorder, p.h-2*uiBorder, fill)
inset := uiBorder + uiPad inset := uiBorder + uiPad
x, w := p.x+inset, p.w-2*inset return 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
// meaningfully. Without this the low digits churn every frame no matter how
// long the averaging window is.
func roundPPS(v float64) uint64 {
const unit = 1000
if v < 0 {
return 0
}
return uint64((v+unit/2)/unit) * unit
} }
func errColor(n uint64) rgb { func errColor(n uint64) rgb {
@@ -279,32 +362,25 @@ func errColor(n uint64) rgb {
return uiRed return uiRed
} }
func rateColor(gb, target float64) rgb { func (d *display) render(v view, elapsed time.Duration, cable string) error {
switch {
case gb >= target*rateGreenFrac:
return uiGreen
case gb >= target*rateYellowFrac:
return uiYellow
default:
return uiRed
}
}
func (d *display) render(v view, elapsed time.Duration, target float64, cable string) error {
fb := d.fb fb := d.fb
fb.fill(uiBg) fb.fill(uiBg)
x, w, y := d.panel(d.nowPanel, v.window, d.nowH) x, w := d.panel(d.nowPanel, v.window)
y = d.rateRow(x, w, y, "RX", v.rxGbps, target) d.stats(d.big, x, w, d.nowYs[0], []statCell{
y = d.row(d.grid, x, w, y, "packets/s", commas(roundPPS(v.rxPPS)), uiFg) {scaleSI(v.rxGbps * 1e9), "bits/s", uiFg},
d.errChips(x, w, y+blockGap, v.window) {scaleSI(v.rxPPS), "packets/s", uiFg},
})
d.errChips(x, w, d.nowYs[1], v.window)
x, w, y = d.panel(d.sincePanel, v.since, d.sinceH) x, w = d.panel(d.sincePanel, v.since)
y = d.row(d.grid, x, w, y, "uptime", uptime(elapsed), uiFg) d.stats(d.gridB, x, w, d.sinceYs[0], []statCell{
y = d.row(d.grid, x, w, y, "frames", commas(v.rxFrames), uiFg) {scaleTime(elapsed), "elapsed", uiFg},
y = d.row(d.grid, x, w, y, "data", humanBytes(v.rxGot), uiFg) {scaleSI(float64(v.rxFrames)), "packets", uiFg},
y = d.row(d.grid, x, w, y, "cable", cable, uiCyan) {scaleSI(float64(v.rxGot)), "bytes", uiFg},
d.errBlock(x, w, y+blockGap, v.since) {cable, "m", uiFg},
})
d.errCounts(x, w, d.sinceYs[1], v.since)
d.drawResetButton() d.drawResetButton()
return fb.flush() return fb.flush()