Mirror the panel in the console table as one summed row in two sections

This commit is contained in:
flamingcow
2026-08-04 14:02:53 -07:00
parent fbfcabda7e
commit 63196eb691
4 changed files with 76 additions and 98 deletions
+32 -68
View File
@@ -32,7 +32,6 @@ func (e endpoint) macString() string {
} }
type direction struct { type direction struct {
short string
specs []*frameSpec specs []*frameSpec
txStats []*txStats txStats []*txStats
rxStats []*rxStats rxStats []*rxStats
@@ -50,7 +49,6 @@ type direction struct {
// clock and drawn on another, and the two must not read them at once: // clock and drawn on another, and the two must not read them at once:
// sampleDrops consumes what it reads, so a second caller would see a gap. // sampleDrops consumes what it reads, so a second caller would see a gap.
mu sync.Mutex mu sync.Mutex
prevConsole counterSet
win *rateWindow win *rateWindow
drops uint64 drops uint64
base counterSet base counterSet
@@ -144,24 +142,15 @@ func (w *rateWindow) latest(rate func(prev, cur counterSet, secs float64) float6
return rate(prev, cur, secs) return rate(prev, cur, secs)
} }
func txRatePPS(p, c counterSet, secs float64) float64 {
return float64(c.s.txFrames-p.s.txFrames) / secs
}
func rxRatePPS(p, c counterSet, secs float64) float64 { func rxRatePPS(p, c counterSet, secs float64) float64 {
return float64(c.s.rxFrames-p.s.rxFrames) / secs return float64(c.s.rxFrames-p.s.rxFrames) / secs
} }
func txRateGbps(p, c counterSet, secs float64) float64 {
return gbps(c.s.txBytes-p.s.txBytes, c.s.txFrames-p.s.txFrames, secs)
}
func rxRateGbps(p, c counterSet, secs float64) float64 { func rxRateGbps(p, c counterSet, secs float64) float64 {
return gbps(c.s.rxBytes-p.s.rxBytes, c.s.rxFrames-p.s.rxFrames, secs) return gbps(c.s.rxBytes-p.s.rxBytes, c.s.rxFrames-p.s.rxFrames, secs)
} }
type sample struct { type sample struct {
txFrames, txBytes uint64
rxFrames, rxBytes uint64 rxFrames, rxBytes uint64
lost, late uint64 lost, late uint64
crcErr, badMagic uint64 crcErr, badMagic uint64
@@ -193,8 +182,6 @@ func lookupEndpoint(name string) (endpoint, error) {
func (d *direction) snapshot() sample { func (d *direction) snapshot() sample {
var s sample var s sample
for _, t := range d.txStats { for _, t := range d.txStats {
s.txFrames += t.frames.Load()
s.txBytes += t.bytes.Load()
s.txErrs += t.errs.Load() s.txErrs += t.errs.Load()
} }
for _, r := range d.rxStats { for _, r := range d.rxStats {
@@ -247,26 +234,27 @@ func gbps(bytes, frames uint64, secs float64) float64 {
} }
var intervalCols = []colSpec{ var intervalCols = []colSpec{
{title: "ELAPSED", width: 9, right: true}, {group: "NOW", title: "bits/s", width: 9, right: true},
{title: "DIR", width: 5}, {group: "NOW", title: "packets/s", width: 9, right: true},
{title: "TX packets/s", width: 12, right: true}, {group: "NOW", title: "lost", width: 7, right: true},
{title: "TX bits/s", width: 10, right: true}, {group: "NOW", title: "corrupt", width: 7, right: true},
{title: "RX packets/s", width: 12, right: true}, {group: "NOW", title: "link", width: 7, right: true},
{title: "RX bits/s", width: 10, right: true}, {group: "NOW", title: "internal", width: 8, right: true},
{title: "LOST", width: 9, right: true}, {group: "OVERALL", title: "elapsed", width: 9, right: true},
{title: "CORRUPT", width: 9, right: true}, {group: "OVERALL", title: "packets", width: 9, right: true},
{title: "LINK", width: 9, right: true}, {group: "OVERALL", title: "bytes", width: 9, right: true},
{title: "INTERNAL", width: 9, right: true}, {group: "OVERALL", title: "metres", width: 6, right: true},
{title: "ERRORS", width: 9, right: true}, {group: "OVERALL", title: "lost", width: 9, right: true},
{title: "MIN ns", width: 9, right: true}, {group: "OVERALL", title: "corrupt", width: 9, right: true},
{title: "LEN m", width: 6, right: true}, {group: "OVERALL", title: "link", width: 9, right: true},
{group: "OVERALL", title: "internal", width: 9, right: true},
} }
// Shared by the console table and the framebuffer so both show the same // Shared by the console table and the framebuffer so both show the same
// figures. // figures.
type view struct { type view struct {
txPPS, rxPPS float64 rxPPS float64
txGbps, rxGbps float64 rxGbps float64
rxFrames, rxBytes uint64 rxFrames, rxBytes uint64
since errs since errs
window errs window errs
@@ -303,9 +291,7 @@ func (d *direction) counters(now counterSet) view {
func totalView(views []view) view { func totalView(views []view) view {
var t view var t view
for _, v := range views { for _, v := range views {
t.txPPS += v.txPPS
t.rxPPS += v.rxPPS t.rxPPS += v.rxPPS
t.txGbps += v.txGbps
t.rxGbps += v.rxGbps t.rxGbps += v.rxGbps
t.rxFrames += v.rxFrames t.rxFrames += v.rxFrames
t.rxBytes += v.rxBytes t.rxBytes += v.rxBytes
@@ -315,26 +301,6 @@ func totalView(views []view) view {
return t return t
} }
func (d *direction) view() view {
d.mu.Lock()
defer d.mu.Unlock()
now := d.capture()
p := d.prevConsole
d.prevConsole = now
v := d.counters(now)
secs := now.t.Sub(p.t).Seconds()
if secs <= 0 {
return v
}
v.txPPS = txRatePPS(p, now, secs)
v.rxPPS = rxRatePPS(p, now, secs)
v.txGbps = txRateGbps(p, now, secs)
v.rxGbps = rxRateGbps(p, now, secs)
return v
}
func (d *direction) sample() { func (d *direction) sample() {
d.mu.Lock() d.mu.Lock()
d.win.push(d.capture()) d.win.push(d.capture())
@@ -354,29 +320,30 @@ func (d *direction) displayView() view {
if n >= 2 { if n >= 2 {
v.window = errsBetween(d.win.at(0), d.win.at(n-1)) v.window = errsBetween(d.win.at(0), d.win.at(n-1))
} }
v.txPPS = d.win.latest(txRatePPS)
v.rxPPS = d.win.latest(rxRatePPS) v.rxPPS = d.win.latest(rxRatePPS)
v.txGbps = d.win.latest(txRateGbps)
v.rxGbps = d.win.latest(rxRateGbps) v.rxGbps = d.win.latest(rxRateGbps)
d.mu.Unlock() d.mu.Unlock()
return v return v
} }
func (d *direction) row(elapsed time.Duration, v view, target float64, length string) []string { // The same figures the panel draws, in the same order: the last second as rates
// and error flags, then everything since the reset.
func totalRow(elapsed time.Duration, v view, target float64, length string) []string {
return []string{ return []string{
scaleTime(elapsed),
paint(d.short, cCyan),
scaleSI(v.txPPS),
rateCell(v.txGbps*1e9, target*1e9),
scaleSI(v.rxPPS),
rateCell(v.rxGbps*1e9, target*1e9), rateCell(v.rxGbps*1e9, target*1e9),
scaleSI(v.rxPPS),
flagCell(v.window.lost),
flagCell(v.window.corrupt),
flagCell(v.window.link),
flagCell(v.window.internal),
scaleTime(elapsed),
scaleCount(v.rxFrames),
scaleCount(v.rxBytes),
length,
statusCell(v.since.lost), statusCell(v.since.lost),
statusCell(v.since.corrupt), statusCell(v.since.corrupt),
statusCell(v.since.link), statusCell(v.since.link),
statusCell(v.since.internal), statusCell(v.since.internal),
statusCell(v.since.total()),
paint(v.cable.minText(), cCyan),
paint(length, cCyan),
} }
} }
@@ -386,12 +353,10 @@ func (d *direction) row(elapsed time.Duration, v view, target float64, length st
func (d *direction) primeCounters() { func (d *direction) primeCounters() {
d.poller.prime() d.poller.prime()
d.reset() d.reset()
d.prevConsole = d.base
} }
func buildDirection(label string, tx, rx endpoint) (*direction, error) { func buildDirection(label string, tx, rx endpoint) (*direction, error) {
d := &direction{ d := &direction{
short: tx.tag + "→" + rx.tag,
streams: newLossWindows(numStreams), streams: newLossWindows(numStreams),
cable: newCableStats(), cable: newCableStats(),
} }
@@ -628,7 +593,8 @@ func run(aName, bName string, nsPerM float64) error {
[]string{"TAG", "INTERFACE", "MAC", "SPEED", "MTU"}, []string{"TAG", "INTERFACE", "MAC", "SPEED", "MTU"},
[]bool{false, false, false, true, true}, linkRows)) []bool{false, false, false, true, true}, linkRows))
target := a.speed // One row carries both directions, so line rate is both links at once.
target := a.speed * float64(len(dirs))
sizeStrs := make([]string, len(frameSizes)) sizeStrs := make([]string, len(frameSizes))
for i, s := range frameSizes { for i, s := range frameSizes {
sizeStrs[i] = fmt.Sprintf("%d", s) sizeStrs[i] = fmt.Sprintf("%d", s)
@@ -728,17 +694,15 @@ func run(aName, bName string, nsPerM float64) error {
// Length needs both directions, so every row is sampled before any of // Length needs both directions, so every row is sampled before any of
// them is printed. // them is printed.
for i, d := range dirs { for i, d := range dirs {
rows[i] = d.view() rows[i] = d.displayView()
} }
length := "-" length := "-"
if m, ok := cableMetres(rows, nsPerM); ok { if m, ok := cableMetres(rows, nsPerM); ok {
length = fmt.Sprintf("%.1f", m) length = fmt.Sprintf("%.1f", m)
} }
for i, d := range dirs { for _, line := range stats.emit(totalRow(elapsed, totalView(rows), target, length)) {
for _, line := range stats.emit(d.row(elapsed, rows[i], target, length)) {
fmt.Println(line) fmt.Println(line)
} }
} }
} }
} }
}
-7
View File
@@ -45,13 +45,6 @@ type cableView struct {
ok bool ok bool
} }
func (v cableView) minText() string {
if !v.ok {
return "-"
}
return commas(uint64(v.min))
}
// Averaging the two directions cancels the phy asymmetry between them, which is // Averaging the two directions cancels the phy asymmetry between them, which is
// about 790ns and swamps any cable, so one direction alone cannot give a length. // about 790ns and swamps any cable, so one direction alone cannot give a length.
func cableMetres(views []view, nsPerM float64) (float64, bool) { func cableMetres(views []view, nsPerM float64) (float64, bool) {
+38 -7
View File
@@ -111,6 +111,7 @@ func scaleTime(d time.Duration) string {
} }
type colSpec struct { type colSpec struct {
group string
title string title string
width int width int
right bool right bool
@@ -122,15 +123,33 @@ type streamTable struct {
headerEvery int headerEvery int
} }
// A group change is drawn as a vertical break, so the two halves of the row
// read apart without a second header line naming them.
func (t *streamTable) join(cells []string, brk string) string {
var b strings.Builder
for i, c := range cells {
if i > 0 {
if t.cols[i].group != t.cols[i-1].group {
b.WriteString(brk)
} else {
b.WriteByte(' ')
}
}
b.WriteString(c)
}
return b.String()
}
func (t *streamTable) headerLines() []string { func (t *streamTable) headerLines() []string {
var titles, rules []string titles := make([]string, len(t.cols))
for _, c := range t.cols { rules := make([]string, len(t.cols))
titles = append(titles, pad(c.title, c.width, c.right)) for i, c := range t.cols {
rules = append(rules, strings.Repeat("─", c.width)) titles[i] = paint(pad(c.title, c.width, c.right), cBold)
rules[i] = strings.Repeat("─", c.width)
} }
return []string{ return []string{
paint(strings.Join(titles, " "), cBold), t.join(titles, paint(" │ ", cGrey)),
paint(strings.Join(rules, " "), cGrey), paint(t.join(rules, "─┼─"), cGrey),
} }
} }
@@ -140,6 +159,9 @@ func (t *streamTable) width() int {
w += c.width w += c.width
if i > 0 { if i > 0 {
w++ w++
if t.cols[i].group != t.cols[i-1].group {
w += 2
}
} }
} }
return w return w
@@ -174,7 +196,7 @@ func (t *streamTable) emit(cells []string) []string {
padded = append(padded, pad(v, c.width, c.right)) padded = append(padded, pad(v, c.width, c.right))
} }
t.sinceHeader++ t.sinceHeader++
return append(out, strings.Join(padded, " ")) return append(out, t.join(padded, paint(" │ ", cGrey)))
} }
func renderBox(title string, headers []string, rights []bool, rows [][]string) string { func renderBox(title string, headers []string, rights []bool, rows [][]string) string {
@@ -232,6 +254,15 @@ func renderBox(title string, headers []string, rights []bool, rows [][]string) s
return b.String() return b.String()
} }
// Whether rather than how many, matching the panel's top chips: over a window
// this short a count changes faster than it can be read.
func flagCell(v uint64) string {
if v == 0 {
return paint("ok", cGreen)
}
return paint("ERR", cRed)
}
// Exact rather than scaled: scaled, one lost frame and a thousand both read as // Exact rather than scaled: scaled, one lost frame and a thousand both read as
// 1.00, separated only by a letter. // 1.00, separated only by a letter.
func statusCell(v uint64) string { func statusCell(v uint64) string {
-10
View File
@@ -7,8 +7,6 @@ import (
) )
type txStats struct { type txStats struct {
frames atomic.Uint64
bytes atomic.Uint64
errs atomic.Uint64 errs atomic.Uint64
} }
@@ -32,7 +30,6 @@ func (w *txWorker) run(done *atomic.Bool) {
w.spec.prefill(bufs[i], pats[i]) w.spec.prefill(bufs[i], pats[i])
} }
hdrs, iovs := newMmsghdrs(bufs) hdrs, iovs := newMmsghdrs(bufs)
sizes := make([]int, w.batch)
<-w.startTx <-w.startTx
@@ -45,19 +42,12 @@ func (w *txWorker) run(done *atomic.Bool) {
if si == len(w.spec.sizes) { if si == len(w.spec.sizes) {
si = 0 si = 0
} }
sizes[i] = size
putHeader(bufs[i], pats[i], w.stream, seq+uint64(i), size-minFrame) putHeader(bufs[i], pats[i], w.stream, seq+uint64(i), size-minFrame)
iovs[i].Len = uint64(size) iovs[i].Len = uint64(size)
} }
n, err := sendmmsg(w.fd, hdrs) n, err := sendmmsg(w.fd, hdrs)
if n > 0 { if n > 0 {
var b uint64
for i := 0; i < n; i++ {
b += uint64(sizes[i])
}
w.stats.frames.Add(uint64(n))
w.stats.bytes.Add(b)
seq += uint64(n) seq += uint64(n)
} }
// Taking fewer of the vector than offered is the ring's room, not a frame // Taking fewer of the vector than offered is the ring's room, not a frame