Compare commits
3
Commits
e77574712d
...
5e2b36bdaa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e2b36bdaa | ||
|
|
102d53fb9b | ||
|
|
4d102f377e |
@@ -31,6 +31,9 @@ const (
|
|||||||
viScreenInfoLen = 40
|
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 {
|
type framebuffer struct {
|
||||||
file *os.File
|
file *os.File
|
||||||
tty *os.File
|
tty *os.File
|
||||||
@@ -38,12 +41,22 @@ type framebuffer struct {
|
|||||||
back []byte
|
back []byte
|
||||||
w int
|
w int
|
||||||
h int
|
h int
|
||||||
|
pw int
|
||||||
|
ph int
|
||||||
stride int
|
stride int
|
||||||
rShift uint
|
rShift uint
|
||||||
gShift uint
|
gShift uint
|
||||||
bShift 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
|
// The kernel console draws into the same framebuffer, including a blinking
|
||||||
// cursor and any keyboard echo, which fights every frame we write. Putting the
|
// 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.
|
// active console into graphics mode stops it touching the framebuffer at all.
|
||||||
@@ -88,21 +101,23 @@ func openFramebuffer(path string) (*framebuffer, error) {
|
|||||||
|
|
||||||
fb := &framebuffer{
|
fb := &framebuffer{
|
||||||
file: f,
|
file: f,
|
||||||
w: int(vi[viXres]),
|
pw: int(vi[viXres]),
|
||||||
h: int(vi[viYres]),
|
ph: int(vi[viYres]),
|
||||||
|
w: int(vi[viYres]),
|
||||||
|
h: int(vi[viXres]),
|
||||||
stride: int(stride),
|
stride: int(stride),
|
||||||
rShift: uint(vi[viRedOffset]),
|
rShift: uint(vi[viRedOffset]),
|
||||||
gShift: uint(vi[viGreenOffset]),
|
gShift: uint(vi[viGreenOffset]),
|
||||||
bShift: uint(vi[viBlueOffset]),
|
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)
|
unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.Close()
|
f.Close()
|
||||||
return nil, fmt.Errorf("mmap: %w", err)
|
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()
|
fb.tty, err = takeConsole()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -139,28 +154,28 @@ func (fb *framebuffer) fill(c rgb) {
|
|||||||
row[x+2] = byte(v >> 16)
|
row[x+2] = byte(v >> 16)
|
||||||
row[x+3] = byte(v >> 24)
|
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)
|
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) {
|
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)
|
v := fb.pixel(c)
|
||||||
for y := y0; y < y0+h; y++ {
|
span := make([]byte, (y1-y0)*4)
|
||||||
if y < 0 || y >= fb.h {
|
for i := 0; i+4 <= len(span); i += 4 {
|
||||||
continue
|
span[i+0] = byte(v)
|
||||||
}
|
span[i+1] = byte(v >> 8)
|
||||||
base := y * fb.stride
|
span[i+2] = byte(v >> 16)
|
||||||
for x := x0; x < x0+w; x++ {
|
span[i+3] = byte(v >> 24)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
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 {
|
if x < 0 || y < 0 || x >= fb.w || y >= fb.h || cov == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
o := y*fb.stride + x*4
|
o := fb.offset(x, y)
|
||||||
if cov == 255 {
|
if cov == 255 {
|
||||||
v := fb.pixel(c)
|
v := fb.pixel(c)
|
||||||
fb.back[o+0] = byte(v)
|
fb.back[o+0] = byte(v)
|
||||||
|
|||||||
@@ -60,14 +60,6 @@ var patterns = []pattern{
|
|||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
func patternNames() []string {
|
|
||||||
names := make([]string, len(patterns))
|
|
||||||
for i, p := range patterns {
|
|
||||||
names[i] = p.name
|
|
||||||
}
|
|
||||||
return names
|
|
||||||
}
|
|
||||||
|
|
||||||
// Every pattern is laid out at full length up front with its checksum at each
|
// Every pattern is laid out at full length up front with its checksum at each
|
||||||
// size, so a sender picks a pattern by choosing a buffer, never by writing one.
|
// size, so a sender picks a pattern by choosing a buffer, never by writing one.
|
||||||
type frameSpec struct {
|
type frameSpec struct {
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ module g.fc.run/theater/cabletest
|
|||||||
go 1.26.5
|
go 1.26.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/image v0.44.0 // indirect
|
golang.org/x/image v0.44.0
|
||||||
golang.org/x/sys v0.47.0 // indirect
|
golang.org/x/sys v0.47.0
|
||||||
golang.org/x/text v0.40.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require golang.org/x/text v0.40.0 // indirect
|
||||||
|
|||||||
@@ -58,8 +58,14 @@ type direction struct {
|
|||||||
drops uint64
|
drops uint64
|
||||||
errBase sample
|
errBase sample
|
||||||
dropBase uint64
|
dropBase uint64
|
||||||
|
nicRaw uint64
|
||||||
nicNow uint64
|
nicNow uint64
|
||||||
nicBase uint64
|
nicBase uint64
|
||||||
|
|
||||||
|
recent errs
|
||||||
|
recentBase sample
|
||||||
|
recentDrops uint64
|
||||||
|
recentNic uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smoothing has to be steady against high-frequency noise yet still chase a
|
// Smoothing has to be steady against high-frequency noise yet still chase a
|
||||||
@@ -84,7 +90,13 @@ type rateEstimator struct {
|
|||||||
n int
|
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 {
|
if e.n == 0 {
|
||||||
e.est, e.shown, e.n = x, x, 1
|
e.est, e.shown, e.n = x, x, 1
|
||||||
return
|
return
|
||||||
@@ -147,6 +159,27 @@ type rateSample struct {
|
|||||||
txFrames, txBytes, rxFrames, rxBytes uint64
|
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
|
// 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
|
// across the whole window, so the figure moves every frame while still being
|
||||||
// measured over a long enough span to be steady.
|
// measured over a long enough span to be steady.
|
||||||
@@ -313,12 +346,9 @@ var intervalCols = []colSpec{
|
|||||||
type view struct {
|
type view struct {
|
||||||
txPPS, rxPPS float64
|
txPPS, rxPPS float64
|
||||||
txGbps, rxGbps float64
|
txGbps, rxGbps float64
|
||||||
txFrames, txSent uint64
|
|
||||||
rxFrames, rxGot uint64
|
rxFrames, rxGot uint64
|
||||||
lost, late uint64
|
since errs
|
||||||
crc, badMagic uint64
|
window errs
|
||||||
kdrop, link uint64
|
|
||||||
errors uint64
|
|
||||||
cable cableView
|
cable cableView
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,25 +356,39 @@ type view struct {
|
|||||||
// console and the display.
|
// console and the display.
|
||||||
func (d *direction) counters(now sample) view {
|
func (d *direction) counters(now sample) view {
|
||||||
b := d.errBase
|
b := d.errBase
|
||||||
v := view{
|
return view{
|
||||||
txFrames: now.txFrames - b.txFrames,
|
|
||||||
txSent: now.txBytes - b.txBytes,
|
|
||||||
rxFrames: now.rxFrames - b.rxFrames,
|
rxFrames: now.rxFrames - b.rxFrames,
|
||||||
rxGot: now.rxBytes - b.rxBytes,
|
rxGot: now.rxBytes - b.rxBytes,
|
||||||
|
cable: d.cable.view(),
|
||||||
|
since: errs{
|
||||||
lost: now.lost - b.lost,
|
lost: now.lost - b.lost,
|
||||||
late: now.late - b.late,
|
late: now.late - b.late,
|
||||||
crc: now.crcErr - b.crcErr,
|
crc: now.crcErr - b.crcErr,
|
||||||
badMagic: now.badMagic - b.badMagic,
|
badMagic: now.badMagic - b.badMagic,
|
||||||
|
badLen: now.badLen - b.badLen,
|
||||||
kdrop: d.drops - d.dropBase,
|
kdrop: d.drops - d.dropBase,
|
||||||
cable: d.cable.view(),
|
|
||||||
}
|
|
||||||
// A frame the stack refused and a frame the driver dropped are the same
|
// 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:
|
// failure seen from either side of the ring, and never the same frame
|
||||||
// a send that fails never reaches the driver to be dropped.
|
// twice: a send that fails never reaches the driver to be dropped.
|
||||||
v.link = d.nicNow - d.nicBase +
|
link: d.nicNow - d.nicBase +
|
||||||
(now.txErrs - b.txErrs) + (now.rxErrs - b.rxErrs)
|
(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
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
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})
|
d.win.push(rateSample{t, now.txFrames, now.txBytes, now.rxFrames, now.rxBytes})
|
||||||
|
|
||||||
v := d.counters(now)
|
v := d.counters(now)
|
||||||
v.txFrames = d.heldFrames.get(t, v.txFrames)
|
v.window = d.recent
|
||||||
v.txSent = d.heldSent.get(t, v.txSent)
|
v.rxFrames = d.heldFrames.get(t, v.rxFrames)
|
||||||
|
v.rxGot = d.heldSent.get(t, v.rxGot)
|
||||||
|
|
||||||
o, n, ok := d.win.span()
|
o, n, ok := d.win.span()
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -382,10 +427,11 @@ func (d *direction) displayView(t time.Time) view {
|
|||||||
}
|
}
|
||||||
txF := n.txFrames - o.txFrames
|
txF := n.txFrames - o.txFrames
|
||||||
rxF := n.rxFrames - o.rxFrames
|
rxF := n.rxFrames - o.rxFrames
|
||||||
d.est.txPPS.update(float64(txF) / secs)
|
full := d.win.filled
|
||||||
d.est.rxPPS.update(float64(rxF) / secs)
|
d.est.txPPS.update(float64(txF)/secs, full)
|
||||||
d.est.txGbps.update(gbps(n.txBytes-o.txBytes, txF, secs))
|
d.est.rxPPS.update(float64(rxF)/secs, full)
|
||||||
d.est.rxGbps.update(gbps(n.rxBytes-o.rxBytes, rxF, secs))
|
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.txPPS = d.est.txPPS.value()
|
||||||
v.rxPPS = d.est.rxPPS.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),
|
rateCell(v.txGbps, target),
|
||||||
commas(uint64(v.rxPPS)),
|
commas(uint64(v.rxPPS)),
|
||||||
rateCell(v.rxGbps, target),
|
rateCell(v.rxGbps, target),
|
||||||
statusCell(v.lost),
|
statusCell(v.since.lost),
|
||||||
statusCell(v.late),
|
statusCell(v.since.late),
|
||||||
statusCell(v.crc),
|
statusCell(v.since.crc),
|
||||||
statusCell(v.badMagic),
|
statusCell(v.since.badMagic),
|
||||||
statusCell(v.kdrop),
|
statusCell(v.since.kdrop),
|
||||||
statusCell(v.link),
|
statusCell(v.since.link),
|
||||||
statusCell(v.errors),
|
statusCell(v.since.total()),
|
||||||
paint(v.cable.minText(), cCyan),
|
paint(v.cable.minText(), cCyan),
|
||||||
paint(length, cCyan),
|
paint(length, cCyan),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sampled once a second, since these are sysfs reads; the display uses whatever
|
// Sampled once a second, since these are sysfs reads. The recent errors roll
|
||||||
// the last sample left behind.
|
// 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() {
|
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)
|
tx := readNIC(d.tx.name)
|
||||||
rx := readNIC(d.rx.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) {
|
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
|
d.probeRxFD = fd
|
||||||
|
|
||||||
// Whatever the interfaces have counted before now is not ours.
|
|
||||||
d.sampleNIC()
|
|
||||||
d.nicBase = d.nicNow
|
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,7 +622,6 @@ type config struct {
|
|||||||
streams int
|
streams int
|
||||||
batch int
|
batch int
|
||||||
probeEther uint16
|
probeEther uint16
|
||||||
zeroNS float64
|
|
||||||
nsPerM float64
|
nsPerM float64
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,13 +632,13 @@ func main() {
|
|||||||
sizesArg = flag.String("sizes", "64,128,256,512,1024,1280,1514", "frame sizes in bytes, excluding FCS, cycled per packet")
|
sizesArg = flag.String("sizes", "64,128,256,512,1024,1280,1514", "frame sizes in bytes, excluding FCS, cycled per packet")
|
||||||
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")
|
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")
|
batch = flag.Int("batch", 64, "frames per sendmmsg/recvmmsg call")
|
||||||
zeroNS = flag.Float64("zero-ns", 2163.77, "mean of both directions at zero cable length; belongs to the media adapters, recalibrate when they change")
|
|
||||||
nsPerM = flag.Float64("ns-per-m", 5.4545, "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()
|
flag.Parse()
|
||||||
|
|
||||||
if err := run(*aName, *bName, *sizesArg,
|
if err := run(*aName, *bName, *sizesArg,
|
||||||
*streams, *batch, *zeroNS, *nsPerM); err != nil {
|
*streams, *batch, *nsPerM); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "error:", err)
|
fmt.Fprintln(os.Stderr, "error:", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -574,7 +656,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func run(aName, bName, sizesArg string,
|
func run(aName, bName, sizesArg string,
|
||||||
nStreams, batch int, zeroNS, nsPerM float64) error {
|
nStreams, batch int, nsPerM float64) error {
|
||||||
|
|
||||||
if aName == "" || bName == "" {
|
if aName == "" || bName == "" {
|
||||||
return fmt.Errorf("both -a and -b are required")
|
return fmt.Errorf("both -a and -b are required")
|
||||||
@@ -629,7 +711,6 @@ func run(aName, bName, sizesArg string,
|
|||||||
streams: nStreams,
|
streams: nStreams,
|
||||||
batch: batch,
|
batch: batch,
|
||||||
probeEther: uint16(etherBase + nStreams),
|
probeEther: uint16(etherBase + nStreams),
|
||||||
zeroNS: zeroNS,
|
|
||||||
nsPerM: nsPerM,
|
nsPerM: nsPerM,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,7 +755,7 @@ func run(aName, bName, sizesArg string,
|
|||||||
nStreams, ethertypes[0], ethertypes[len(ethertypes)-1])},
|
nStreams, ethertypes[0], ethertypes[len(ethertypes)-1])},
|
||||||
{"probe", fmt.Sprintf("ethertype 0x%04x every %s", cfg.probeEther, probeInterval)},
|
{"probe", fmt.Sprintf("ethertype 0x%04x every %s", cfg.probeEther, probeInterval)},
|
||||||
{"batch", fmt.Sprintf("%d frames per syscall", batch)},
|
{"batch", fmt.Sprintf("%d frames per syscall", batch)},
|
||||||
{"calibration", fmt.Sprintf("%g ns at zero length, %g ns/m", zeroNS, nsPerM)},
|
{"calibration", fmt.Sprintf("%g ns/m, zero taken from the shortest delay seen so far", nsPerM)},
|
||||||
{"buffers", fmt.Sprintf("sndbuf %s, rcvbuf %s",
|
{"buffers", fmt.Sprintf("sndbuf %s, rcvbuf %s",
|
||||||
humanBytes(uint64(sockBufSize(dirs[0].txFDs[0], unix.SO_SNDBUF))),
|
humanBytes(uint64(sockBufSize(dirs[0].txFDs[0], unix.SO_SNDBUF))),
|
||||||
humanBytes(uint64(sockBufSize(dirs[0].rxFDs[0], unix.SO_RCVBUF))))},
|
humanBytes(uint64(sockBufSize(dirs[0].rxFDs[0], unix.SO_RCVBUF))))},
|
||||||
@@ -705,11 +786,15 @@ func run(aName, bName, sizesArg string,
|
|||||||
}
|
}
|
||||||
defer disp.close()
|
defer disp.close()
|
||||||
|
|
||||||
touch, err := watchTouch(disp.fb.w, disp.fb.h)
|
touch, err := watchTouch(disp.fb.pw, disp.fb.ph)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("touchscreen: %w", err)
|
return fmt.Errorf("touchscreen: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, d := range dirs {
|
||||||
|
d.primeCounters()
|
||||||
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
close(startTx)
|
close(startTx)
|
||||||
tick := time.NewTicker(reportInterval)
|
tick := time.NewTicker(reportInterval)
|
||||||
@@ -735,13 +820,20 @@ func run(aName, bName, sizesArg string,
|
|||||||
case <-space:
|
case <-space:
|
||||||
start = resetAll(dirs, stats)
|
start = resetAll(dirs, stats)
|
||||||
case now := <-frame.C:
|
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)
|
start = resetAll(dirs, stats)
|
||||||
}
|
}
|
||||||
for i, d := range dirs {
|
for i, d := range dirs {
|
||||||
views[i] = d.displayView(now)
|
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:
|
case now := <-tick.C:
|
||||||
secs := now.Sub(last).Seconds()
|
secs := now.Sub(last).Seconds()
|
||||||
last = now
|
last = now
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@@ -34,6 +33,7 @@ type cableStats struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
min int64
|
min int64
|
||||||
samples uint64
|
samples uint64
|
||||||
|
floor int64
|
||||||
|
|
||||||
txPend map[uint64]int64
|
txPend map[uint64]int64
|
||||||
rxPend map[uint64]int64
|
rxPend map[uint64]int64
|
||||||
@@ -41,11 +41,12 @@ type cableStats struct {
|
|||||||
|
|
||||||
type cableView struct {
|
type cableView struct {
|
||||||
min int64
|
min int64
|
||||||
samples uint64
|
floor int64
|
||||||
|
ok bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v cableView) minText() string {
|
func (v cableView) minText() string {
|
||||||
if v.samples == 0 {
|
if !v.ok {
|
||||||
return "-"
|
return "-"
|
||||||
}
|
}
|
||||||
return commasInt(v.min)
|
return commasInt(v.min)
|
||||||
@@ -57,23 +58,14 @@ func (c config) cableMetres(views []view) (float64, bool) {
|
|||||||
if len(views) == 0 {
|
if len(views) == 0 {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
var sum float64
|
var excess float64
|
||||||
for _, v := range views {
|
for _, v := range views {
|
||||||
if v.cable.samples == 0 {
|
if !v.cable.ok {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
sum += float64(v.cable.min)
|
excess += float64(v.cable.min - v.cable.floor)
|
||||||
}
|
}
|
||||||
mean := sum / float64(len(views))
|
return excess / float64(len(views)) / c.nsPerM, true
|
||||||
return (mean - c.zeroNS) / 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 {
|
func newCableStats() *cableStats {
|
||||||
@@ -114,13 +106,16 @@ func (c *cableStats) put(seq uint64, ts int64, tx bool) {
|
|||||||
if c.samples == 0 || delta < c.min {
|
if c.samples == 0 || delta < c.min {
|
||||||
c.min = delta
|
c.min = delta
|
||||||
}
|
}
|
||||||
|
if c.floor == 0 || delta < c.floor {
|
||||||
|
c.floor = delta
|
||||||
|
}
|
||||||
c.samples++
|
c.samples++
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cableStats) view() cableView {
|
func (c *cableStats) view() cableView {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
return cableView{c.min, c.samples}
|
return cableView{c.min, c.floor, c.samples > 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cableStats) reset() {
|
func (c *cableStats) reset() {
|
||||||
|
|||||||
@@ -199,14 +199,18 @@ func renderBox(title string, headers []string, rights []bool, rows [][]string) s
|
|||||||
}
|
}
|
||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
if title != "" {
|
writeln := func(s string) {
|
||||||
b.WriteString(paint(title, cBold+cCyan) + "\n")
|
b.WriteString(s)
|
||||||
|
b.WriteByte('\n')
|
||||||
}
|
}
|
||||||
b.WriteString(line("┌", "┬", "┐") + "\n")
|
if title != "" {
|
||||||
b.WriteString(rowText(headers, true) + "\n")
|
writeln(paint(title, cBold+cCyan))
|
||||||
b.WriteString(line("├", "┼", "┤") + "\n")
|
}
|
||||||
|
writeln(line("┌", "┬", "┐"))
|
||||||
|
writeln(rowText(headers, true))
|
||||||
|
writeln(line("├", "┼", "┤"))
|
||||||
for _, r := range rows {
|
for _, r := range rows {
|
||||||
b.WriteString(rowText(r, false) + "\n")
|
writeln(rowText(r, false))
|
||||||
}
|
}
|
||||||
b.WriteString(line("└", "┴", "┘"))
|
b.WriteString(line("└", "┴", "┘"))
|
||||||
return b.String()
|
return b.String()
|
||||||
|
|||||||
@@ -8,39 +8,27 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
uiBg = rgb{0x12, 0x14, 0x18}
|
uiBg = rgb{0x12, 0x14, 0x18}
|
||||||
uiPanel = rgb{0x1c, 0x20, 0x26}
|
uiOKFill = rgb{0x18, 0x42, 0x26}
|
||||||
uiRule = rgb{0x2e, 0x34, 0x3c}
|
uiOKEdge = rgb{0x3c, 0xe0, 0x70}
|
||||||
|
uiErrFil = rgb{0x54, 0x18, 0x1c}
|
||||||
|
uiErrEdg = rgb{0xff, 0x46, 0x46}
|
||||||
uiButton = rgb{0x25, 0x2b, 0x33}
|
uiButton = rgb{0x25, 0x2b, 0x33}
|
||||||
uiFg = rgb{0xe6, 0xe8, 0xea}
|
uiFg = rgb{0xe6, 0xe8, 0xea}
|
||||||
uiDim = rgb{0x7a, 0x82, 0x8c}
|
uiDim = rgb{0x9a, 0xa2, 0xac}
|
||||||
uiCyan = rgb{0x5c, 0xc8, 0xe0}
|
uiCyan = rgb{0x5c, 0xc8, 0xe0}
|
||||||
uiGreen = rgb{0x4c, 0xc2, 0x6a}
|
uiGreen = rgb{0x6c, 0xdc, 0x86}
|
||||||
uiRed = rgb{0xe0, 0x4b, 0x4b}
|
uiRed = rgb{0xf0, 0x6b, 0x6b}
|
||||||
uiYellow = rgb{0xe0, 0xb0, 0x40}
|
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 (
|
const (
|
||||||
uiMargin = 16
|
uiMargin = 16
|
||||||
|
uiPad = 14
|
||||||
|
uiBorder = 12
|
||||||
|
rowGap = 5
|
||||||
|
blockGap = 16
|
||||||
|
|
||||||
cellDirA = 0
|
btnW = 240
|
||||||
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
|
|
||||||
btnH = 64
|
btnH = 64
|
||||||
holdDuration = time.Second
|
holdDuration = time.Second
|
||||||
)
|
)
|
||||||
@@ -58,8 +46,11 @@ type display struct {
|
|||||||
huge *textFace
|
huge *textFace
|
||||||
grid *textFace
|
grid *textFace
|
||||||
gridB *textFace
|
gridB *textFace
|
||||||
small *textFace
|
|
||||||
|
|
||||||
|
nowPanel rect
|
||||||
|
sincePanel rect
|
||||||
|
nowH int
|
||||||
|
sinceH int
|
||||||
resetBtn rect
|
resetBtn rect
|
||||||
holdStart time.Time
|
holdStart time.Time
|
||||||
holdFrac float64
|
holdFrac float64
|
||||||
@@ -77,10 +68,9 @@ func newDisplay() (*display, error) {
|
|||||||
bold bool
|
bold bool
|
||||||
size float64
|
size float64
|
||||||
}{
|
}{
|
||||||
{&d.huge, true, 72},
|
{&d.huge, true, 60},
|
||||||
{&d.grid, false, 24},
|
{&d.grid, false, 22},
|
||||||
{&d.gridB, true, 24},
|
{&d.gridB, true, 22},
|
||||||
{&d.small, false, 18},
|
|
||||||
} {
|
} {
|
||||||
face, err := loadFace(spec.bold, spec.size)
|
face, err := loadFace(spec.bold, spec.size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -94,9 +84,22 @@ 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
|
||||||
|
// 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{
|
d.resetBtn = rect{
|
||||||
x: fb.w - uiMargin - btnW,
|
x: d.sincePanel.x + (inner-btnW)/2,
|
||||||
y: fb.h - uiMargin - btnH,
|
y: d.sincePanel.y + d.sincePanel.h - uiBorder - uiPad - btnH,
|
||||||
w: btnW,
|
w: btnW,
|
||||||
h: btnH,
|
h: btnH,
|
||||||
}
|
}
|
||||||
@@ -163,28 +166,59 @@ func (d *display) close() {
|
|||||||
d.fb.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) {
|
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)
|
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) {
|
func (d *display) row(f *textFace, x, w, y int, label, value string, col rgb) int {
|
||||||
f.draw(d.fb, (d.fb.w-len([]rune(s))*f.cellW)/2, y, s, col)
|
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
|
// 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 := d.fb
|
||||||
fb.fill(uiBg)
|
fb.fill(uiBg)
|
||||||
|
|
||||||
d.small.draw(fb, uiMargin, 10, "cabletest", uiCyan)
|
x, w, y := d.panel(d.nowPanel, v.window, d.nowH)
|
||||||
d.small.draw(fb, uiMargin+11*d.small.cellW, 10,
|
y = d.rateRow(x, w, y, "RX", v.rxGbps, target)
|
||||||
fmt.Sprintf("%s %s %s %s", dirs[0].tx.tag, dirs[0].tx.name,
|
y = d.row(d.grid, x, w, y, "packets/s", commas(roundPPS(v.rxPPS)), uiFg)
|
||||||
dirs[0].rx.tag, dirs[0].rx.name), uiDim)
|
d.errBlock(x, w, y+blockGap, v.window)
|
||||||
d.right(d.small, fb.w-uiMargin, 10, uptime(elapsed), uiDim)
|
|
||||||
|
|
||||||
var total uint64
|
x, w, y = d.panel(d.sincePanel, v.since, d.sinceH)
|
||||||
for _, v := range views {
|
y = d.row(d.grid, x, w, y, "uptime", uptime(elapsed), uiFg)
|
||||||
total += v.errors
|
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)
|
||||||
bandY, bandH := 40, 150
|
d.errBlock(x, w, y+blockGap, v.since)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
d.drawResetButton()
|
d.drawResetButton()
|
||||||
fb.flush()
|
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