Render the live status to the framebuffer in Atkinson Hyperlegible Mono
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
fbioGetVScreenInfo = 0x4600
|
||||
fbioBlank = 0x4611
|
||||
fbBlankUnblank = 0
|
||||
)
|
||||
|
||||
// The variable screen info is a flat run of 40 u32s, so it is read as an array
|
||||
// rather than a struct to sidestep any question of padding.
|
||||
const (
|
||||
viXres = 0
|
||||
viYres = 1
|
||||
viBitsPerPixel = 6
|
||||
viRedOffset = 8
|
||||
viGreenOffset = 11
|
||||
viBlueOffset = 14
|
||||
viScreenInfoLen = 40
|
||||
)
|
||||
|
||||
type framebuffer struct {
|
||||
file *os.File
|
||||
mem []byte
|
||||
back []byte
|
||||
w int
|
||||
h int
|
||||
stride int
|
||||
rShift uint
|
||||
gShift uint
|
||||
bShift uint
|
||||
}
|
||||
|
||||
func openFramebuffer(path string) (*framebuffer, error) {
|
||||
f, err := os.OpenFile(path, os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var vi [viScreenInfoLen]uint32
|
||||
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
|
||||
fbioGetVScreenInfo, uintptr(unsafe.Pointer(&vi[0]))); errno != 0 {
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("get screen info: %w", errno)
|
||||
}
|
||||
if vi[viBitsPerPixel] != 32 {
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("only 32bpp supported, got %d", vi[viBitsPerPixel])
|
||||
}
|
||||
|
||||
// Wake the panel; the console blanks it after a timeout.
|
||||
unix.Syscall(unix.SYS_IOCTL, f.Fd(), fbioBlank, fbBlankUnblank)
|
||||
|
||||
stride, ok := readUint("/sys/class/graphics/fb0/stride")
|
||||
if !ok {
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("cannot read framebuffer stride")
|
||||
}
|
||||
|
||||
fb := &framebuffer{
|
||||
file: f,
|
||||
w: int(vi[viXres]),
|
||||
h: int(vi[viYres]),
|
||||
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,
|
||||
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)
|
||||
return fb, nil
|
||||
}
|
||||
|
||||
func (fb *framebuffer) close() {
|
||||
unix.Munmap(fb.mem)
|
||||
fb.file.Close()
|
||||
}
|
||||
|
||||
func (fb *framebuffer) pixel(c rgb) uint32 {
|
||||
return uint32(c.r)<<fb.rShift | uint32(c.g)<<fb.gShift | uint32(c.b)<<fb.bShift
|
||||
}
|
||||
|
||||
type rgb struct {
|
||||
r, g, b uint8
|
||||
}
|
||||
|
||||
func (fb *framebuffer) fill(c rgb) {
|
||||
v := fb.pixel(c)
|
||||
row := make([]byte, fb.stride)
|
||||
for x := 0; x+4 <= fb.stride; x += 4 {
|
||||
row[x+0] = byte(v)
|
||||
row[x+1] = byte(v >> 8)
|
||||
row[x+2] = byte(v >> 16)
|
||||
row[x+3] = byte(v >> 24)
|
||||
}
|
||||
for y := 0; y < fb.h; y++ {
|
||||
copy(fb.back[y*fb.stride:], row)
|
||||
}
|
||||
}
|
||||
|
||||
func (fb *framebuffer) rect(x0, y0, w, h int, c rgb) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return
|
||||
}
|
||||
o := y*fb.stride + x*4
|
||||
if cov == 255 {
|
||||
v := fb.pixel(c)
|
||||
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)
|
||||
return
|
||||
}
|
||||
a := uint32(cov)
|
||||
old := uint32(fb.back[o+0]) | uint32(fb.back[o+1])<<8 |
|
||||
uint32(fb.back[o+2])<<16 | uint32(fb.back[o+3])<<24
|
||||
orr := uint8(old >> fb.rShift)
|
||||
og := uint8(old >> fb.gShift)
|
||||
ob := uint8(old >> fb.bShift)
|
||||
mix := rgb{
|
||||
r: uint8((uint32(c.r)*a + uint32(orr)*(255-a)) / 255),
|
||||
g: uint8((uint32(c.g)*a + uint32(og)*(255-a)) / 255),
|
||||
b: uint8((uint32(c.b)*a + uint32(ob)*(255-a)) / 255),
|
||||
}
|
||||
v := fb.pixel(mix)
|
||||
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)
|
||||
}
|
||||
|
||||
func (fb *framebuffer) flush() {
|
||||
copy(fb.mem, fb.back)
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -2,4 +2,8 @@ module g.fc.run/theater/cabletest
|
||||
|
||||
go 1.26.5
|
||||
|
||||
require golang.org/x/sys v0.47.0 // indirect
|
||||
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
|
||||
)
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
|
||||
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
|
||||
@@ -156,38 +156,60 @@ var intervalCols = []colSpec{
|
||||
{title: "ERRORS", width: 11, right: true},
|
||||
}
|
||||
|
||||
func (d *direction) intervalRow(elapsed time.Duration, secs, target float64) []string {
|
||||
// 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, errors uint64
|
||||
}
|
||||
|
||||
func (d *direction) view(secs float64) view {
|
||||
now := d.snapshot()
|
||||
p := d.prev
|
||||
d.prev = now
|
||||
d.sampleDrops()
|
||||
|
||||
txF := now.txFrames - p.txFrames
|
||||
txB := now.txBytes - p.txBytes
|
||||
rxF := now.rxFrames - p.rxFrames
|
||||
rxB := now.rxBytes - p.rxBytes
|
||||
|
||||
d.sampleDrops()
|
||||
b := d.errBase
|
||||
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
|
||||
drops := d.drops - d.dropBase
|
||||
v := view{
|
||||
txPPS: float64(txF) / secs,
|
||||
rxPPS: float64(rxF) / secs,
|
||||
txGbps: gbps(now.txBytes-p.txBytes, txF, secs),
|
||||
rxGbps: gbps(now.rxBytes-p.rxBytes, rxF, secs),
|
||||
txFrames: now.txFrames,
|
||||
txSent: now.txBytes,
|
||||
rxFrames: now.rxFrames,
|
||||
rxGot: now.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,
|
||||
}
|
||||
v.errors = v.lost + v.crc + v.badMagic + (now.badLen - b.badLen) + v.kdrop
|
||||
return v
|
||||
}
|
||||
|
||||
func (d *direction) row(elapsed time.Duration, v view, target float64) []string {
|
||||
return []string{
|
||||
uptime(elapsed),
|
||||
paint(d.short, cCyan),
|
||||
commas(uint64(float64(txF) / secs)),
|
||||
rateCell(gbps(txB, txF, secs), target),
|
||||
commas(uint64(float64(rxF) / secs)),
|
||||
rateCell(gbps(rxB, rxF, secs), target),
|
||||
statusCell(lost),
|
||||
statusCell(late),
|
||||
statusCell(crc),
|
||||
statusCell(badMagic),
|
||||
statusCell(drops),
|
||||
statusCell(lost + crc + badMagic + badLen + drops),
|
||||
commas(uint64(v.txPPS)),
|
||||
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.errors),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,6 +464,14 @@ func run(aName, bName, sizesArg, patArg string,
|
||||
space, restoreTerm := watchSpace()
|
||||
defer restoreTerm()
|
||||
|
||||
disp, err := newDisplay()
|
||||
if err != nil {
|
||||
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)
|
||||
tick := time.NewTicker(reportInterval)
|
||||
@@ -466,8 +496,10 @@ func run(aName, bName, sizesArg, patArg string,
|
||||
secs := now.Sub(last).Seconds()
|
||||
last = now
|
||||
elapsed := now.Sub(start)
|
||||
for _, d := range dirs {
|
||||
for _, line := range stats.emit(d.intervalRow(elapsed, secs, target)) {
|
||||
views := make([]view, len(dirs))
|
||||
for i, d := range dirs {
|
||||
views[i] = d.view(secs)
|
||||
for _, line := range stats.emit(d.row(elapsed, views[i], target)) {
|
||||
fmt.Println(line)
|
||||
}
|
||||
for _, line := range d.reportNIC() {
|
||||
@@ -483,6 +515,7 @@ func run(aName, bName, sizesArg, patArg string,
|
||||
break
|
||||
}
|
||||
}
|
||||
disp.render(dirs, views, elapsed, target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/font/opentype"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
//go:embed font/AtkinsonHyperlegibleMono-Regular.ttf font/AtkinsonHyperlegibleMono-Bold.ttf
|
||||
var fontFS embed.FS
|
||||
|
||||
type glyph struct {
|
||||
// Coverage values, cellW*cellH, origin at the cell's top left.
|
||||
cov []uint8
|
||||
w, h int
|
||||
}
|
||||
|
||||
type textFace struct {
|
||||
face font.Face
|
||||
cellW int
|
||||
cellH int
|
||||
ascent int
|
||||
cache map[rune]*glyph
|
||||
}
|
||||
|
||||
func loadFace(bold bool, sizePx float64) (*textFace, error) {
|
||||
name := "font/AtkinsonHyperlegibleMono-Regular.ttf"
|
||||
if bold {
|
||||
name = "font/AtkinsonHyperlegibleMono-Bold.ttf"
|
||||
}
|
||||
raw, err := fontFS.ReadFile(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parsed, err := opentype.Parse(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
face, err := opentype.NewFace(parsed, &opentype.FaceOptions{
|
||||
Size: sizePx,
|
||||
DPI: 72,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := face.Metrics()
|
||||
adv, ok := face.GlyphAdvance('0')
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("font has no digit glyphs")
|
||||
}
|
||||
return &textFace{
|
||||
face: face,
|
||||
cellW: adv.Ceil(),
|
||||
cellH: (m.Ascent + m.Descent).Ceil(),
|
||||
ascent: m.Ascent.Ceil(),
|
||||
cache: make(map[rune]*glyph),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Rasterises once per rune and keeps the coverage mask, since the same few
|
||||
// dozen runes are redrawn every frame.
|
||||
func (t *textFace) glyph(r rune) *glyph {
|
||||
if g, ok := t.cache[r]; ok {
|
||||
return g
|
||||
}
|
||||
g := &glyph{w: t.cellW, h: t.cellH, cov: make([]uint8, t.cellW*t.cellH)}
|
||||
|
||||
dr, mask, maskp, _, ok := t.face.Glyph(
|
||||
fixed.P(0, t.ascent), r)
|
||||
if ok {
|
||||
alpha := image.NewAlpha(image.Rect(0, 0, t.cellW, t.cellH))
|
||||
draw.DrawMask(alpha, dr, image.NewUniform(image.White.C), image.Point{},
|
||||
mask, maskp, draw.Src)
|
||||
for y := 0; y < t.cellH; y++ {
|
||||
for x := 0; x < t.cellW; x++ {
|
||||
g.cov[y*t.cellW+x] = alpha.AlphaAt(x, y).A
|
||||
}
|
||||
}
|
||||
}
|
||||
t.cache[r] = g
|
||||
return g
|
||||
}
|
||||
|
||||
func (t *textFace) draw(fb *framebuffer, x, y int, s string, c rgb) int {
|
||||
for _, r := range s {
|
||||
if r == ' ' {
|
||||
x += t.cellW
|
||||
continue
|
||||
}
|
||||
g := t.glyph(r)
|
||||
for gy := 0; gy < g.h; gy++ {
|
||||
row := gy * g.w
|
||||
for gx := 0; gx < g.w; gx++ {
|
||||
if cov := g.cov[row+gx]; cov != 0 {
|
||||
fb.blend(x+gx, y+gy, c, cov)
|
||||
}
|
||||
}
|
||||
}
|
||||
x += t.cellW
|
||||
}
|
||||
return x
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
uiBg = rgb{0x12, 0x14, 0x18}
|
||||
uiPanel = rgb{0x1c, 0x20, 0x26}
|
||||
uiFg = rgb{0xe6, 0xe8, 0xea}
|
||||
uiDim = rgb{0x7a, 0x82, 0x8c}
|
||||
uiCyan = rgb{0x5c, 0xc8, 0xe0}
|
||||
uiGreen = rgb{0x4c, 0xc2, 0x6a}
|
||||
uiRed = rgb{0xe0, 0x4b, 0x4b}
|
||||
uiYellow = rgb{0xe0, 0xb0, 0x40}
|
||||
)
|
||||
|
||||
type display struct {
|
||||
fb *framebuffer
|
||||
huge *textFace
|
||||
big *textFace
|
||||
small *textFace
|
||||
config string
|
||||
}
|
||||
|
||||
func newDisplay() (*display, error) {
|
||||
fb, err := openFramebuffer("/dev/fb0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
huge, err := loadFace(true, 46)
|
||||
if err != nil {
|
||||
fb.close()
|
||||
return nil, err
|
||||
}
|
||||
big, err := loadFace(true, 24)
|
||||
if err != nil {
|
||||
fb.close()
|
||||
return nil, err
|
||||
}
|
||||
small, err := loadFace(false, 18)
|
||||
if err != nil {
|
||||
fb.close()
|
||||
return nil, err
|
||||
}
|
||||
return &display{fb: fb, huge: huge, big: big, small: small}, 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) 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 errColor(n uint64) rgb {
|
||||
if n == 0 {
|
||||
return uiGreen
|
||||
}
|
||||
return uiRed
|
||||
}
|
||||
|
||||
func (d *display) 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(dirs []*direction, views []view, elapsed time.Duration, target float64) {
|
||||
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)
|
||||
|
||||
var total uint64
|
||||
for _, v := range views {
|
||||
total += v.errors
|
||||
}
|
||||
|
||||
// Status band, readable from across the room.
|
||||
bandY, bandH := 44, 74
|
||||
fb.rect(0, bandY, fb.w, bandH, uiPanel)
|
||||
fb.rect(0, bandY, 6, 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)
|
||||
|
||||
panelY := bandY + bandH + 12
|
||||
footerH := d.small.cellH + 16
|
||||
panelH := (fb.h - panelY - footerH) / max(len(dirs), 1)
|
||||
for i, dir := range dirs {
|
||||
d.renderDirection(dir, views[i], 12, panelY+i*panelH, fb.w-24, panelH-10, target)
|
||||
}
|
||||
d.footer(fb.h - footerH + 4)
|
||||
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) 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) 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))
|
||||
}
|
||||
Reference in New Issue
Block a user