Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
726c4c2445 | ||
|
|
de2ff13eb6 | ||
|
|
3c8637f931 |
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"path/filepath"
|
||||
"unsafe"
|
||||
|
||||
@@ -449,6 +450,31 @@ func (fb *framebuffer) rect(x0, y0, w, h int, c rgb) {
|
||||
}
|
||||
}
|
||||
|
||||
// Distance to the rectangle the corner radius sweeps around, which is zero
|
||||
// across the whole flat middle and grows only near a corner. Taking coverage
|
||||
// from that rather than from a plain inside test keeps the curves smooth
|
||||
// instead of stepped.
|
||||
func (fb *framebuffer) roundRect(x0, y0, w, h, r int, c rgb) {
|
||||
// A radius past half the shorter side has no meaning and would put the
|
||||
// swept rectangle inside out, which matters while something is growing from
|
||||
// nothing.
|
||||
r = min(r, min(w, h)/2)
|
||||
ix0, iy0 := float64(x0+r), float64(y0+r)
|
||||
ix1, iy1 := float64(x0+w-1-r), float64(y0+h-1-r)
|
||||
for y := y0; y < y0+h; y++ {
|
||||
for x := x0; x < x0+w; x++ {
|
||||
fx, fy := float64(x), float64(y)
|
||||
dx := math.Max(math.Max(ix0-fx, fx-ix1), 0)
|
||||
dy := math.Max(math.Max(iy0-fy, fy-iy1), 0)
|
||||
cov := float64(r) - math.Sqrt(dx*dx+dy*dy) + 0.5
|
||||
if cov <= 0 {
|
||||
continue
|
||||
}
|
||||
fb.blend(x, y, c, uint8(math.Min(cov, 1)*255))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Blends src over the existing pixel, with cov as 0-255 coverage.
|
||||
func (fb *framebuffer) blend(x, y int, c rgb, cov uint8) {
|
||||
if x < 0 || y < 0 || x >= fb.w || y >= fb.h || cov == 0 {
|
||||
|
||||
@@ -94,24 +94,25 @@ func (d *direction) capture(t time.Time) counterSet {
|
||||
return counterSet{t: t, s: d.snapshot(), drops: d.drops, nic: d.nic.Load()}
|
||||
}
|
||||
|
||||
// Late is counted but left out of the total, since a reordered frame arrived.
|
||||
// What someone testing a cable is asking, rather than how each failure happened
|
||||
// to be noticed. Corruption arrives as three different symptoms and the kernel
|
||||
// drops frames for reasons that are ours rather than the cable's, but none of
|
||||
// that is a distinction worth reading off a panel.
|
||||
type errs struct {
|
||||
lost, late uint64
|
||||
crc, badMagic uint64
|
||||
badLen uint64
|
||||
kdrop, link uint64
|
||||
lost uint64
|
||||
corrupt uint64
|
||||
link uint64
|
||||
internal uint64
|
||||
}
|
||||
|
||||
func (e errs) total() uint64 {
|
||||
return e.lost + e.crc + e.badMagic + e.badLen + e.kdrop + e.link
|
||||
return e.lost + e.corrupt + e.link + e.internal
|
||||
}
|
||||
|
||||
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,
|
||||
lost: e.lost + o.lost, corrupt: e.corrupt + o.corrupt,
|
||||
link: e.link + o.link, internal: e.internal + o.internal,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,19 +303,17 @@ func gbps(bytes, frames uint64, secs float64) float64 {
|
||||
}
|
||||
|
||||
var intervalCols = []colSpec{
|
||||
{title: "UPTIME", width: 9, right: true},
|
||||
{title: "ELAPSED", width: 9, right: true},
|
||||
{title: "DIR", width: 5},
|
||||
{title: "TX pps", width: 9, right: true},
|
||||
{title: "TX Gb/s", width: 7, right: true},
|
||||
{title: "RX pps", width: 9, right: true},
|
||||
{title: "RX Gb/s", width: 7, right: true},
|
||||
{title: "LOST", width: 11, right: true},
|
||||
{title: "LATE", width: 9, right: true},
|
||||
{title: "CRC", width: 7, right: true},
|
||||
{title: "BADMAG", width: 7, right: true},
|
||||
{title: "KDROP", width: 11, right: true},
|
||||
{title: "LINK", width: 13, right: true},
|
||||
{title: "ERRORS", width: 13, right: true},
|
||||
{title: "TX packets/s", width: 12, right: true},
|
||||
{title: "TX bits/s", width: 10, right: true},
|
||||
{title: "RX packets/s", width: 12, right: true},
|
||||
{title: "RX bits/s", width: 10, right: true},
|
||||
{title: "LOST", width: 9, right: true},
|
||||
{title: "CORRUPT", width: 9, right: true},
|
||||
{title: "LINK", width: 9, right: true},
|
||||
{title: "INTERNAL", width: 9, right: true},
|
||||
{title: "ERRORS", width: 9, right: true},
|
||||
{title: "MIN ns", width: 9, right: true},
|
||||
{title: "LEN m", width: 6, right: true},
|
||||
}
|
||||
@@ -332,16 +331,21 @@ type view struct {
|
||||
|
||||
func errsBetween(b, n counterSet) errs {
|
||||
return errs{
|
||||
lost: n.s.lost - b.s.lost,
|
||||
late: n.s.late - b.s.late,
|
||||
crc: n.s.crcErr - b.s.crcErr,
|
||||
badMagic: n.s.badMagic - b.s.badMagic,
|
||||
badLen: n.s.badLen - b.s.badLen,
|
||||
kdrop: n.drops - b.drops,
|
||||
lost: n.s.lost - b.s.lost,
|
||||
// Three ways of noticing one thing: a payload that does not match its
|
||||
// checksum, a header that is not ours, and a length that cannot be.
|
||||
corrupt: (n.s.crcErr - b.s.crcErr) + (n.s.badMagic - b.s.badMagic) +
|
||||
(n.s.badLen - b.s.badLen),
|
||||
// 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: (n.nic - b.nic) + (n.s.txErrs - b.s.txErrs) + (n.s.rxErrs - b.s.rxErrs),
|
||||
// Ours rather than the cable's: frames the kernel threw away because we
|
||||
// did not drain the ring fast enough, and frames so far out of order
|
||||
// that our own bookkeeping had already written them off. The second is
|
||||
// unreachable while each stream has a flow rule to its own queue, which
|
||||
// is exactly why it is worth counting: it fires if that stops holding.
|
||||
internal: (n.drops - b.drops) + (n.s.late - b.s.late),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,18 +430,16 @@ func (d *direction) displayView(t time.Time) view {
|
||||
|
||||
func (d *direction) row(elapsed time.Duration, v view, target float64, length string) []string {
|
||||
return []string{
|
||||
uptime(elapsed),
|
||||
scaleTime(elapsed),
|
||||
paint(d.short, cCyan),
|
||||
commas(uint64(v.txPPS)),
|
||||
rateCell(v.txGbps, target),
|
||||
commas(uint64(v.rxPPS)),
|
||||
rateCell(v.rxGbps, target),
|
||||
scaleSI(v.txPPS),
|
||||
rateCell(v.txGbps*1e9, target*1e9),
|
||||
scaleSI(v.rxPPS),
|
||||
rateCell(v.rxGbps*1e9, target*1e9),
|
||||
statusCell(v.since.lost),
|
||||
statusCell(v.since.late),
|
||||
statusCell(v.since.crc),
|
||||
statusCell(v.since.badMagic),
|
||||
statusCell(v.since.kdrop),
|
||||
statusCell(v.since.corrupt),
|
||||
statusCell(v.since.link),
|
||||
statusCell(v.since.internal),
|
||||
statusCell(v.since.total()),
|
||||
paint(v.cable.minText(), cCyan),
|
||||
paint(length, cCyan),
|
||||
@@ -799,12 +801,13 @@ func run(aName, bName, sizesArg string,
|
||||
for i, d := range dirs {
|
||||
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 {
|
||||
cable = fmt.Sprintf("%.1f m", m)
|
||||
cable = fmt.Sprintf("%.1f", m)
|
||||
}
|
||||
if err := disp.render(totalView(views), now.Sub(start),
|
||||
target*float64(len(dirs)), cable); err != nil {
|
||||
if err := disp.render(totalView(views), now.Sub(start), cable); err != nil {
|
||||
return err
|
||||
}
|
||||
case now := <-tick.C:
|
||||
|
||||
@@ -57,11 +57,6 @@ func pad(s string, w int, right bool) string {
|
||||
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 {
|
||||
s := fmt.Sprintf("%d", v)
|
||||
if len(s) <= 3 {
|
||||
@@ -94,6 +89,37 @@ func humanBytes(b uint64) string {
|
||||
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 {
|
||||
title string
|
||||
width int
|
||||
@@ -216,6 +242,9 @@ func renderBox(title string, headers []string, rights []bool, rows [][]string) s
|
||||
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 {
|
||||
s := commas(v)
|
||||
if v == 0 {
|
||||
@@ -231,12 +260,12 @@ const (
|
||||
rateYellowFrac = 0.80
|
||||
)
|
||||
|
||||
func rateCell(gb float64, target float64) string {
|
||||
s := fmt.Sprintf("%.2f", gb)
|
||||
func rateCell(bits float64, target float64) string {
|
||||
s := scaleSI(bits)
|
||||
switch {
|
||||
case gb >= target*rateGreenFrac:
|
||||
case bits >= target*rateGreenFrac:
|
||||
return paint(s, cGreen)
|
||||
case gb >= target*rateYellowFrac:
|
||||
case bits >= target*rateYellowFrac:
|
||||
return paint(s, cYellow)
|
||||
default:
|
||||
return paint(s, cRed)
|
||||
|
||||
@@ -26,6 +26,30 @@ type textFace struct {
|
||||
cellH int
|
||||
ascent int
|
||||
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) {
|
||||
@@ -54,13 +78,17 @@ func loadFace(bold bool, sizePx float64) (*textFace, error) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("font has no digit glyphs")
|
||||
}
|
||||
return &textFace{
|
||||
t := &textFace{
|
||||
face: face,
|
||||
cellW: adv.Ceil(),
|
||||
cellH: (m.Ascent + m.Descent).Ceil(),
|
||||
ascent: m.Ascent.Ceil(),
|
||||
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
|
||||
|
||||
@@ -12,25 +12,52 @@ var (
|
||||
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{0x9a, 0xa2, 0xac}
|
||||
uiCyan = rgb{0x5c, 0xc8, 0xe0}
|
||||
uiGreen = rgb{0x6c, 0xdc, 0x86}
|
||||
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 (
|
||||
uiMargin = 16
|
||||
uiPad = 14
|
||||
uiBorder = 12
|
||||
rowGap = 5
|
||||
blockGap = 16
|
||||
uiMargin = spaceGroup
|
||||
uiPad = spaceGroup
|
||||
uiBorder = step * 3
|
||||
pairGap = spaceGroup
|
||||
blockGap = spaceGroup
|
||||
|
||||
btnW = 240
|
||||
btnH = 64
|
||||
btnW = 300
|
||||
btnH = 80
|
||||
holdDuration = time.Second
|
||||
|
||||
// Shared by the chips and the button, which are the same object drawn at
|
||||
// different sizes.
|
||||
chipRadius = spaceTight
|
||||
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 {
|
||||
@@ -43,14 +70,14 @@ func (r rect) contains(x, y int) bool {
|
||||
|
||||
type display struct {
|
||||
fb *framebuffer
|
||||
huge *textFace
|
||||
big *textFace
|
||||
grid *textFace
|
||||
gridB *textFace
|
||||
|
||||
nowPanel rect
|
||||
sincePanel rect
|
||||
nowH int
|
||||
sinceH int
|
||||
nowYs []int
|
||||
sinceYs []int
|
||||
resetBtn rect
|
||||
holdStart time.Time
|
||||
holdFrac float64
|
||||
@@ -68,9 +95,9 @@ func newDisplay() (*display, error) {
|
||||
bold bool
|
||||
size float64
|
||||
}{
|
||||
{&d.huge, true, 60},
|
||||
{&d.grid, false, 22},
|
||||
{&d.gridB, true, 22},
|
||||
{&d.big, true, 40},
|
||||
{&d.grid, false, 34},
|
||||
{&d.gridB, true, 34},
|
||||
} {
|
||||
face, err := loadFace(spec.bold, spec.size)
|
||||
if err != nil {
|
||||
@@ -84,28 +111,63 @@ 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
|
||||
// Guessed heights collide on a screen this small, so the layout 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
|
||||
now := []int{d.statsH(d.big, 2), d.chipsH()}
|
||||
since := []int{d.statsH(d.gridB, 4), d.countsH(), 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
|
||||
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}
|
||||
nowH := 2*uiBorder + sum(now) + (len(now)+1)*gap
|
||||
sinceH := 2*uiBorder + sum(since) + (len(since)+1)*gap
|
||||
d.nowPanel = rect{uiMargin, uiMargin, inner, nowH}
|
||||
d.sincePanel = rect{uiMargin, uiMargin + nowH + blockGap, inner, sinceH}
|
||||
|
||||
d.nowYs = stack(d.nowPanel.y+uiBorder+gap, now, gap)
|
||||
d.sinceYs = stack(d.sincePanel.y+uiBorder+gap, since, gap)
|
||||
d.resetBtn = rect{
|
||||
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,
|
||||
h: btnH,
|
||||
}
|
||||
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
|
||||
// held long enough. Lifting or sliding off cancels, and the press has to be
|
||||
// released before it can arm again.
|
||||
@@ -129,32 +191,33 @@ func (d *display) holdReset(x, y int, down bool, now time.Time) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Built like the error chips, since it sits among them: a coloured outline
|
||||
// around a dark well. Cyan rather than the status colours because it is
|
||||
// something to press, not something being reported.
|
||||
func (d *display) drawResetButton() {
|
||||
r := d.resetBtn
|
||||
d.fb.rect(r.x, r.y, r.w, r.h, uiButton)
|
||||
d.fb.roundRect(r.x, r.y, r.w, r.h, chipRadius, uiCyan)
|
||||
d.fb.roundRect(r.x+chipBorder, r.y+chipBorder, r.w-2*chipBorder, r.h-2*chipBorder,
|
||||
chipRadius-chipBorder, uiBg)
|
||||
|
||||
// The hold fills the well rather than the whole button, so the outline stays
|
||||
// put and it reads as the button filling up.
|
||||
split := r.x + chipBorder
|
||||
if d.holdFrac > 0 {
|
||||
w := int(float64(r.w) * math.Min(d.holdFrac, 1))
|
||||
d.fb.rect(r.x, r.y, w, r.h, uiCyan)
|
||||
}
|
||||
for _, e := range []rect{
|
||||
{r.x, r.y, r.w, 2},
|
||||
{r.x, r.y + r.h - 2, r.w, 2},
|
||||
{r.x, r.y, 2, r.h},
|
||||
{r.x + r.w - 2, r.y, 2, r.h},
|
||||
} {
|
||||
d.fb.rect(e.x, e.y, e.w, e.h, uiCyan)
|
||||
w := int(float64(r.w-2*chipBorder) * math.Min(d.holdFrac, 1))
|
||||
d.fb.roundRect(r.x+chipBorder, r.y+chipBorder, w, r.h-2*chipBorder,
|
||||
chipRadius-chipBorder, uiCyan)
|
||||
split += w
|
||||
}
|
||||
|
||||
label := "RESET"
|
||||
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
|
||||
// against whatever is behind it.
|
||||
split := r.x + int(float64(r.w)*math.Min(d.holdFrac, 1))
|
||||
for i, c := range label {
|
||||
gx := lx + i*d.gridB.cellW
|
||||
col := uiFg
|
||||
col := uiCyan
|
||||
if gx+d.gridB.cellW/2 < split {
|
||||
col = uiBg
|
||||
}
|
||||
@@ -166,20 +229,54 @@ func (d *display) close() {
|
||||
d.fb.close()
|
||||
}
|
||||
|
||||
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)
|
||||
// y is the top of the line as read, so text and a bordered box placed the same
|
||||
// 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 {
|
||||
f.draw(d.fb, x, y, label, uiDim)
|
||||
d.right(f, x+w, y, value, col)
|
||||
return y + f.cellH + rowGap
|
||||
type statCell struct {
|
||||
value string
|
||||
label string
|
||||
col rgb
|
||||
}
|
||||
|
||||
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
|
||||
func (d *display) statPairH(vf *textFace) int {
|
||||
return vf.lineH + pairGap + d.grid.lineH
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -187,23 +284,65 @@ var errRows = []struct {
|
||||
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 }},
|
||||
{"corrupt", func(e errs) uint64 { return e.corrupt }},
|
||||
{"link", func(e errs) uint64 { return e.link }},
|
||||
{"internal", func(e errs) uint64 { return e.internal }},
|
||||
}
|
||||
|
||||
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))
|
||||
func (d *display) chipH() int { return d.grid.lineH + 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 {
|
||||
return gridRows(len(errRows))*(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
|
||||
// carrying their own labels so nothing has to be matched up across a row. Over
|
||||
// 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.
|
||||
func (d *display) errChips(x, w, y int, e errs) int {
|
||||
for i, r := range errRows {
|
||||
c := errColor(r.get(e))
|
||||
cx, cw, cy := d.chipAt(i, x, w, y, d.chipH(), c)
|
||||
d.centerIn(d.grid, cx, cw, cy+chipPadY, r.label, c)
|
||||
}
|
||||
return y
|
||||
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
|
||||
if e.total() > 0 {
|
||||
fill, edge = uiErrFil, uiErrEdg
|
||||
@@ -213,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)
|
||||
|
||||
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
|
||||
// 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
|
||||
return p.x + inset, p.w - 2*inset
|
||||
}
|
||||
|
||||
func errColor(n uint64) rgb {
|
||||
@@ -239,32 +362,25 @@ func errColor(n uint64) rgb {
|
||||
return uiRed
|
||||
}
|
||||
|
||||
func rateColor(gb, target float64) rgb {
|
||||
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 {
|
||||
func (d *display) render(v view, elapsed time.Duration, cable string) error {
|
||||
fb := d.fb
|
||||
fb.fill(uiBg)
|
||||
|
||||
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)
|
||||
x, w := d.panel(d.nowPanel, v.window)
|
||||
d.stats(d.big, x, w, d.nowYs[0], []statCell{
|
||||
{scaleSI(v.rxGbps * 1e9), "bits/s", uiFg},
|
||||
{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)
|
||||
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)
|
||||
x, w = d.panel(d.sincePanel, v.since)
|
||||
d.stats(d.gridB, x, w, d.sinceYs[0], []statCell{
|
||||
{scaleTime(elapsed), "elapsed", uiFg},
|
||||
{scaleSI(float64(v.rxFrames)), "packets", uiFg},
|
||||
{scaleSI(float64(v.rxGot)), "bytes", uiFg},
|
||||
{cable, "m", uiFg},
|
||||
})
|
||||
d.errCounts(x, w, d.sinceYs[1], v.since)
|
||||
|
||||
d.drawResetButton()
|
||||
return fb.flush()
|
||||
|
||||
Reference in New Issue
Block a user