Mirror the panel in the console table as one summed row in two sections
This commit is contained in:
@@ -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
|
||||||
@@ -49,11 +48,10 @@ type direction struct {
|
|||||||
// Guards everything the sampler touches. The counters are read on their own
|
// 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:
|
// 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
|
|
||||||
|
|
||||||
nic atomic.Uint64
|
nic atomic.Uint64
|
||||||
poller *nicPoller
|
poller *nicPoller
|
||||||
@@ -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,16 +694,14 @@ 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type txStats struct {
|
type txStats struct {
|
||||||
frames atomic.Uint64
|
errs atomic.Uint64
|
||||||
bytes atomic.Uint64
|
|
||||||
errs atomic.Uint64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type txWorker struct {
|
type txWorker struct {
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user