diff --git a/main.go b/main.go index bd94699..3ba02be 100644 --- a/main.go +++ b/main.go @@ -32,7 +32,6 @@ func (e endpoint) macString() string { } type direction struct { - short string specs []*frameSpec txStats []*txStats rxStats []*rxStats @@ -49,11 +48,10 @@ type direction struct { // Guards everything the sampler touches. The counters are read on their own // 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. - mu sync.Mutex - prevConsole counterSet - win *rateWindow - drops uint64 - base counterSet + mu sync.Mutex + win *rateWindow + drops uint64 + base counterSet nic atomic.Uint64 poller *nicPoller @@ -144,24 +142,15 @@ func (w *rateWindow) latest(rate func(prev, cur counterSet, secs float64) float6 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 { 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 { return gbps(c.s.rxBytes-p.s.rxBytes, c.s.rxFrames-p.s.rxFrames, secs) } type sample struct { - txFrames, txBytes uint64 rxFrames, rxBytes uint64 lost, late uint64 crcErr, badMagic uint64 @@ -193,8 +182,6 @@ func lookupEndpoint(name string) (endpoint, error) { func (d *direction) snapshot() sample { var s sample for _, t := range d.txStats { - s.txFrames += t.frames.Load() - s.txBytes += t.bytes.Load() s.txErrs += t.errs.Load() } for _, r := range d.rxStats { @@ -247,26 +234,27 @@ func gbps(bytes, frames uint64, secs float64) float64 { } var intervalCols = []colSpec{ - {title: "ELAPSED", width: 9, right: true}, - {title: "DIR", width: 5}, - {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}, + {group: "NOW", title: "bits/s", width: 9, right: true}, + {group: "NOW", title: "packets/s", width: 9, right: true}, + {group: "NOW", title: "lost", width: 7, right: true}, + {group: "NOW", title: "corrupt", width: 7, right: true}, + {group: "NOW", title: "link", width: 7, right: true}, + {group: "NOW", title: "internal", width: 8, right: true}, + {group: "OVERALL", title: "elapsed", width: 9, right: true}, + {group: "OVERALL", title: "packets", width: 9, right: true}, + {group: "OVERALL", title: "bytes", width: 9, right: true}, + {group: "OVERALL", title: "metres", width: 6, right: true}, + {group: "OVERALL", title: "lost", width: 9, right: true}, + {group: "OVERALL", title: "corrupt", width: 9, 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 // figures. type view struct { - txPPS, rxPPS float64 - txGbps, rxGbps float64 + rxPPS float64 + rxGbps float64 rxFrames, rxBytes uint64 since errs window errs @@ -303,9 +291,7 @@ func (d *direction) counters(now counterSet) view { 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.rxBytes += v.rxBytes @@ -315,26 +301,6 @@ func totalView(views []view) view { 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() { d.mu.Lock() d.win.push(d.capture()) @@ -354,29 +320,30 @@ func (d *direction) displayView() view { if n >= 2 { 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.txGbps = d.win.latest(txRateGbps) v.rxGbps = d.win.latest(rxRateGbps) d.mu.Unlock() 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{ - scaleTime(elapsed), - paint(d.short, cCyan), - scaleSI(v.txPPS), - rateCell(v.txGbps*1e9, target*1e9), - scaleSI(v.rxPPS), 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.corrupt), statusCell(v.since.link), 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() { d.poller.prime() d.reset() - d.prevConsole = d.base } func buildDirection(label string, tx, rx endpoint) (*direction, error) { d := &direction{ - short: tx.tag + "→" + rx.tag, streams: newLossWindows(numStreams), cable: newCableStats(), } @@ -628,7 +593,8 @@ func run(aName, bName string, nsPerM float64) error { []string{"TAG", "INTERFACE", "MAC", "SPEED", "MTU"}, []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)) for i, s := range frameSizes { sizeStrs[i] = fmt.Sprintf("%d", s) @@ -728,16 +694,14 @@ func run(aName, bName string, nsPerM float64) error { // Length needs both directions, so every row is sampled before any of // them is printed. for i, d := range dirs { - rows[i] = d.view() + rows[i] = d.displayView() } length := "-" if m, ok := cableMetres(rows, nsPerM); ok { length = fmt.Sprintf("%.1f", m) } - for i, d := range dirs { - for _, line := range stats.emit(d.row(elapsed, rows[i], target, length)) { - fmt.Println(line) - } + for _, line := range stats.emit(totalRow(elapsed, totalView(rows), target, length)) { + fmt.Println(line) } } } diff --git a/probe.go b/probe.go index 1c0a6b3..308f9c8 100644 --- a/probe.go +++ b/probe.go @@ -45,13 +45,6 @@ type cableView struct { 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 // about 790ns and swamps any cable, so one direction alone cannot give a length. func cableMetres(views []view, nsPerM float64) (float64, bool) { diff --git a/render.go b/render.go index 1bf83c9..0ea3acb 100644 --- a/render.go +++ b/render.go @@ -111,6 +111,7 @@ func scaleTime(d time.Duration) string { } type colSpec struct { + group string title string width int right bool @@ -122,15 +123,33 @@ type streamTable struct { 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 { - var titles, rules []string - for _, c := range t.cols { - titles = append(titles, pad(c.title, c.width, c.right)) - rules = append(rules, strings.Repeat("─", c.width)) + titles := make([]string, len(t.cols)) + rules := make([]string, len(t.cols)) + for i, c := range t.cols { + titles[i] = paint(pad(c.title, c.width, c.right), cBold) + rules[i] = strings.Repeat("─", c.width) } return []string{ - paint(strings.Join(titles, " "), cBold), - paint(strings.Join(rules, " "), cGrey), + t.join(titles, paint(" │ ", cGrey)), + paint(t.join(rules, "─┼─"), cGrey), } } @@ -140,6 +159,9 @@ func (t *streamTable) width() int { w += c.width if i > 0 { w++ + if t.cols[i].group != t.cols[i-1].group { + w += 2 + } } } return w @@ -174,7 +196,7 @@ func (t *streamTable) emit(cells []string) []string { padded = append(padded, pad(v, c.width, c.right)) } 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 { @@ -232,6 +254,15 @@ func renderBox(title string, headers []string, rights []bool, rows [][]string) s 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 // 1.00, separated only by a letter. func statusCell(v uint64) string { diff --git a/tx.go b/tx.go index 757ee5e..a96f2c4 100644 --- a/tx.go +++ b/tx.go @@ -7,9 +7,7 @@ import ( ) type txStats struct { - frames atomic.Uint64 - bytes atomic.Uint64 - errs atomic.Uint64 + errs atomic.Uint64 } type txWorker struct { @@ -32,7 +30,6 @@ func (w *txWorker) run(done *atomic.Bool) { w.spec.prefill(bufs[i], pats[i]) } hdrs, iovs := newMmsghdrs(bufs) - sizes := make([]int, w.batch) <-w.startTx @@ -45,19 +42,12 @@ func (w *txWorker) run(done *atomic.Bool) { if si == len(w.spec.sizes) { si = 0 } - sizes[i] = size putHeader(bufs[i], pats[i], w.stream, seq+uint64(i), size-minFrame) iovs[i].Len = uint64(size) } n, err := sendmmsg(w.fd, hdrs) 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) } // Taking fewer of the vector than offered is the ring's room, not a frame