2026-07-25 17:24:29 -07:00
package main
2026-07-25 17:58:54 -07:00
import (
"flag"
"fmt"
2026-07-25 21:30:37 -07:00
"math"
2026-07-25 17:58:54 -07:00
"net"
"os"
"os/signal"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"golang.org/x/sys/unix"
)
const wireOverhead = 24
type endpoint struct {
2026-07-25 18:13:58 -07:00
name string
tag string
idx int
mac [ 6 ] byte
mtu int
speed float64
}
func ( e endpoint ) macString () string {
return fmt . Sprintf ( "%02x:%02x:%02x:%02x:%02x:%02x" ,
e . mac [ 0 ], e . mac [ 1 ], e . mac [ 2 ], e . mac [ 3 ], e . mac [ 4 ], e . mac [ 5 ])
2026-07-25 17:58:54 -07:00
}
type direction struct {
label string
2026-07-25 18:13:58 -07:00
short string
2026-07-25 17:58:54 -07:00
tx endpoint
rx endpoint
2026-07-25 19:08:11 -07:00
specs [] * frameSpec
2026-07-25 17:58:54 -07:00
txStats [] * txStats
rxStats [] * rxStats
2026-07-25 18:33:36 -07:00
streams [] lossWindow
2026-07-25 17:58:54 -07:00
txFDs [] int
rxFDs [] int
2026-07-25 22:38:52 -07:00
probeSpec * frameSpec
probeTxFD int
probeRxFD int
cable * cableStats
2026-07-25 21:30:37 -07:00
prevConsole sample
win * rateWindow
est * rateEstimators
heldFrames heldValue
heldSent heldValue
drops uint64
errBase sample
dropBase uint64
2026-07-25 22:54:04 -07:00
nicNow uint64
nicBase uint64
2026-07-25 21:30:37 -07:00
}
// Smoothing has to be steady against high-frequency noise yet still chase a
// real change quickly, with bounded state. So the gain is not fixed: the
// innovation is compared against a running estimate of the noise itself (mean
// absolute deviation, as in TCP's rtt/rttvar), and only an innovation that
// stands out above that noise is chased hard.
const (
estAlphaCalm = 0.015
estAlphaSnap = 0.45
estMADBeta = 0.05
estNoiseK = 3.0
)
type rateEstimator struct {
minStep float64
relStep float64
est float64
mad float64
shown float64
n int
}
func ( e * rateEstimator ) update ( x float64 ) {
if e . n == 0 {
e . est , e . shown , e . n = x , x , 1
return
}
err := x - e . est
abs := math . Abs ( err )
if e . n == 1 {
e . mad , e . n = abs , 2
} else {
e . mad += ( abs - e . mad ) * estMADBeta
}
a := estAlphaCalm
if e . mad > 0 {
if excess := abs / ( estNoiseK * e . mad ) - 1 ; excess > 0 {
a = estAlphaCalm + ( estAlphaSnap - estAlphaCalm ) * math . Min ( excess , 1 )
}
}
e . est += err * a
// A deadband on top, so the drawn text only changes when the estimate has
// actually moved rather than on every frame.
if math . Abs ( e . est - e . shown ) > math . Max ( e . minStep , e . relStep * math . Abs ( e . est )) {
e . shown = e . est
}
}
func ( e * rateEstimator ) value () float64 { return e . shown }
type rateEstimators struct {
txGbps , rxGbps rateEstimator
txPPS , rxPPS rateEstimator
}
func newRateEstimators () * rateEstimators {
return & rateEstimators {
txGbps : rateEstimator { minStep : 0.02 , relStep : 0.001 },
rxGbps : rateEstimator { minStep : 0.02 , relStep : 0.001 },
txPPS : rateEstimator { minStep : 2000 , relStep : 0.002 },
rxPPS : rateEstimator { minStep : 2000 , relStep : 0.002 },
}
}
// Monotonic totals climb by tens of thousands per frame, which is unreadable
// churn at 60Hz, so the drawn value is held and refreshed a few times a second.
type heldValue struct {
v uint64
at time . Time
}
func ( h * heldValue ) get ( now time . Time , cur uint64 ) uint64 {
if now . Sub ( h . at ) >= totalsHold {
h . v , h . at = cur , now
}
return h . v
}
type rateSample struct {
t time . Time
txFrames , txBytes , rxFrames , rxBytes uint64
}
// 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
// measured over a long enough span to be steady.
type rateWindow struct {
samples [] rateSample
idx int
filled bool
}
func newRateWindow ( n int ) * rateWindow {
return & rateWindow { samples : make ([] rateSample , n )}
}
func ( w * rateWindow ) push ( s rateSample ) {
w . samples [ w . idx ] = s
w . idx ++
if w . idx == len ( w . samples ) {
w . idx = 0
w . filled = true
}
}
func ( w * rateWindow ) span () ( oldest , newest rateSample , ok bool ) {
if ! w . filled && w . idx < 2 {
return oldest , newest , false
}
n := w . idx - 1
if n < 0 {
n = len ( w . samples ) - 1
}
o := 0
if w . filled {
o = w . idx
}
return w . samples [ o ], w . samples [ n ], true
2026-07-25 17:58:54 -07:00
}
type sample struct {
txFrames , txBytes uint64
rxFrames , rxBytes uint64
2026-07-25 18:33:36 -07:00
lost , late uint64
2026-07-25 17:58:54 -07:00
crcErr , badMagic uint64
badLen uint64
txErrs , txShort uint64
2026-07-25 22:54:04 -07:00
rxErrs uint64
2026-07-25 17:58:54 -07:00
}
func lookupEndpoint ( name string ) ( endpoint , error ) {
ifi , err := net . InterfaceByName ( name )
if err != nil {
return endpoint {}, err
}
if len ( ifi . HardwareAddr ) != 6 {
return endpoint {}, fmt . Errorf ( "%s: expected 6-byte MAC, got %q" , name , ifi . HardwareAddr )
}
var mac [ 6 ] byte
copy ( mac [:], ifi . HardwareAddr )
2026-07-25 18:13:58 -07:00
e := endpoint { name : name , idx : ifi . Index , mac : mac , mtu : ifi . MTU }
if v , ok := readUint ( "/sys/class/net/" + name + "/speed" ); ok {
e . speed = float64 ( v ) / 1000
}
return e , nil
2026-07-25 17:58:54 -07:00
}
func parseSizes ( s string ) ([] int , error ) {
var out [] int
for _ , f := range strings . Split ( s , "," ) {
f = strings . TrimSpace ( f )
if f == "" {
continue
}
v , err := strconv . Atoi ( f )
if err != nil {
return nil , fmt . Errorf ( "bad size %q: %w" , f , err )
}
if v < minFrame {
return nil , fmt . Errorf ( "size %d below minimum %d" , v , minFrame )
}
out = append ( out , v )
}
if len ( out ) == 0 {
return nil , fmt . Errorf ( "no sizes given" )
}
return out , nil
}
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 ()
s . txShort += t . short . Load ()
}
for _ , r := range d . rxStats {
s . rxFrames += r . frames . Load ()
s . rxBytes += r . bytes . Load ()
s . crcErr += r . crcErr . Load ()
s . badMagic += r . badMagic . Load ()
s . badLen += r . badLen . Load ()
2026-07-25 22:54:04 -07:00
s . rxErrs += r . rxErrs . Load ()
2026-07-25 17:58:54 -07:00
}
for i := range d . streams {
2026-07-25 18:33:36 -07:00
s . lost += d . streams [ i ]. lost . Load ()
s . late += d . streams [ i ]. late . Load ()
2026-07-25 17:58:54 -07:00
}
return s
}
2026-07-25 19:29:00 -07:00
// Counters keep climbing in the workers, so resetting just moves the origin
2026-07-25 21:41:21 -07:00
// everything is measured from. Rates are deliberately left running, since they
// are instantaneous and would only blink to zero and back.
func ( d * direction ) reset () {
2026-07-25 19:29:00 -07:00
d . sampleDrops ()
d . errBase = d . snapshot ()
d . dropBase = d . drops
2026-07-25 21:41:21 -07:00
d . heldFrames = heldValue {}
d . heldSent = heldValue {}
2026-07-25 22:54:04 -07:00
d . nicBase = d . nicNow
2026-07-25 22:38:52 -07:00
d . cable . reset ()
2026-07-25 21:41:21 -07:00
}
// Returns the new start time, so the uptime shown alongside the totals counts
// from the reset rather than from launch.
func resetAll ( dirs [] * direction , stats * streamTable ) time . Time {
for _ , d := range dirs {
d . reset ()
}
stats . sinceHeader = 0
fmt . Println ( stats . rule ( "counters reset" ))
return time . Now ()
2026-07-25 19:29:00 -07:00
}
2026-07-25 17:58:54 -07:00
func ( d * direction ) sampleDrops () {
for _ , fd := range d . rxFDs {
d . drops += packetDrops ( fd )
}
}
func gbps ( bytes , frames uint64 , secs float64 ) float64 {
return float64 (( bytes + frames * wireOverhead ) * 8 ) / secs / 1e9
}
2026-07-25 18:13:58 -07:00
var intervalCols = [] colSpec {
2026-07-25 18:33:36 -07:00
{ title : "UPTIME" , width : 9 , right : true },
2026-07-25 18:13:58 -07:00
{ title : "DIR" , width : 5 },
{ title : "TX pps" , width : 9 , right : true },
{ title : "TX Gb/s" , width : 7 , right : true },
{ title : "RX pps" , width : 9 , right : true },
{ title : "RX Gb/s" , width : 7 , right : true },
2026-07-25 18:45:47 -07:00
{ title : "LOST" , width : 11 , right : true },
{ title : "LATE" , width : 9 , right : true },
{ title : "CRC" , width : 7 , right : true },
{ title : "BADMAG" , width : 7 , right : true },
{ title : "KDROP" , width : 11 , right : true },
2026-07-25 22:54:04 -07:00
{ title : "LINK" , width : 13 , right : true },
{ title : "ERRORS" , width : 13 , right : true },
2026-07-25 22:38:52 -07:00
{ title : "MIN ns" , width : 9 , right : true },
{ title : "LEN m" , width : 6 , right : true },
2026-07-25 18:13:58 -07:00
}
2026-07-25 19:41:53 -07:00
// 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
2026-07-25 22:54:04 -07:00
kdrop , link uint64
errors uint64
2026-07-25 22:38:52 -07:00
cable cableView
2026-07-25 19:41:53 -07:00
}
2026-07-25 21:30:37 -07:00
// Cumulative fields, which need no rate window and are identical for both the
// console and the display.
func ( d * direction ) counters ( now sample ) view {
2026-07-25 19:29:00 -07:00
b := d . errBase
2026-07-25 19:41:53 -07:00
v := view {
2026-07-25 21:41:21 -07:00
txFrames : now . txFrames - b . txFrames ,
txSent : now . txBytes - b . txBytes ,
rxFrames : now . rxFrames - b . rxFrames ,
rxGot : now . rxBytes - b . rxBytes ,
2026-07-25 19:41:53 -07:00
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 ,
2026-07-25 22:38:52 -07:00
cable : d . cable . view (),
2026-07-25 19:41:53 -07:00
}
2026-07-25 22:54:04 -07:00
// 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:
// a send that fails never reaches the driver to be dropped.
v . link = d . nicNow - d . nicBase +
( now . txErrs - b . txErrs ) + ( now . rxErrs - b . rxErrs )
v . errors = v . lost + v . crc + v . badMagic + ( now . badLen - b . badLen ) + v . kdrop + v . link
2026-07-25 19:41:53 -07:00
return v
}
2026-07-25 18:13:58 -07:00
2026-07-25 21:30:37 -07:00
func ( d * direction ) view ( prev * sample , secs float64 ) view {
now := d . snapshot ()
p := * prev
* prev = now
d . sampleDrops ()
v := d . counters ( now )
txF := now . txFrames - p . txFrames
rxF := now . rxFrames - p . rxFrames
v . txPPS = float64 ( txF ) / secs
v . rxPPS = float64 ( rxF ) / secs
v . txGbps = gbps ( now . txBytes - p . txBytes , txF , secs )
v . rxGbps = gbps ( now . rxBytes - p . rxBytes , rxF , secs )
return v
}
func ( d * direction ) displayView ( t time . Time ) view {
now := d . snapshot ()
d . sampleDrops ()
d . win . push ( rateSample { t , now . txFrames , now . txBytes , now . rxFrames , now . rxBytes })
v := d . counters ( now )
v . txFrames = d . heldFrames . get ( t , v . txFrames )
v . txSent = d . heldSent . get ( t , v . txSent )
o , n , ok := d . win . span ()
if ! ok {
return v
}
secs := n . t . Sub ( o . t ). Seconds ()
if secs <= 0 {
return v
}
txF := n . txFrames - o . txFrames
rxF := n . rxFrames - o . rxFrames
d . est . txPPS . update ( float64 ( txF ) / secs )
d . est . rxPPS . update ( float64 ( rxF ) / secs )
d . est . txGbps . update ( gbps ( n . txBytes - o . txBytes , txF , secs ))
d . est . rxGbps . update ( gbps ( n . rxBytes - o . rxBytes , rxF , secs ))
v . txPPS = d . est . txPPS . value ()
v . rxPPS = d . est . rxPPS . value ()
v . txGbps = d . est . txGbps . value ()
v . rxGbps = d . est . rxGbps . value ()
return v
}
2026-07-25 22:38:52 -07:00
func ( d * direction ) row ( elapsed time . Duration , v view , target float64 , length string ) [] string {
2026-07-25 18:13:58 -07:00
return [] string {
2026-07-25 18:33:36 -07:00
uptime ( elapsed ),
2026-07-25 18:13:58 -07:00
paint ( d . short , cCyan ),
2026-07-25 19:41:53 -07:00
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 ),
2026-07-25 22:54:04 -07:00
statusCell ( v . link ),
2026-07-25 19:41:53 -07:00
statusCell ( v . errors ),
2026-07-25 22:38:52 -07:00
paint ( v . cable . minText (), cCyan ),
paint ( length , cCyan ),
2026-07-25 18:13:58 -07:00
}
2026-07-25 17:58:54 -07:00
}
2026-07-25 22:54:04 -07:00
// Read once a second rather than per frame, since these are sysfs files; the
// display uses whatever the last sample left behind.
func ( d * direction ) sampleNIC () {
2026-07-25 17:58:54 -07:00
tx := readNIC ( d . tx . name )
rx := readNIC ( d . rx . name )
2026-07-25 22:54:04 -07:00
d . nicNow = tx . tx + rx . rx + tx . carrierDown
2026-07-25 18:13:58 -07:00
}
2026-07-25 17:58:54 -07:00
func buildDirection ( label string , tx , rx endpoint , patIdx int , sizes [] int , cfg config ) ( * direction , error ) {
d := & direction {
label : label ,
2026-07-25 18:13:58 -07:00
short : tx . tag + "→" + rx . tag ,
2026-07-25 17:58:54 -07:00
tx : tx ,
rx : rx ,
2026-07-25 19:08:11 -07:00
streams : newLossWindows ( cfg . streams ),
2026-07-25 22:54:04 -07:00
cable : newCableStats (),
2026-07-25 17:58:54 -07:00
}
2026-07-25 19:08:11 -07:00
for i := 0 ; i < cfg . streams ; i ++ {
et := uint16 ( etherBase + i )
d . specs = append ( d . specs , newFrameSpec ( patIdx , rx . mac , tx . mac , et , sizes ))
2026-07-25 17:58:54 -07:00
2026-07-25 18:20:34 -07:00
fd , err := openTxSocket ( tx . idx )
2026-07-25 17:58:54 -07:00
if err != nil {
return nil , fmt . Errorf ( "%s tx socket: %w" , label , err )
}
d . txFDs = append ( d . txFDs , fd )
d . txStats = append ( d . txStats , & txStats {})
2026-07-25 19:08:11 -07:00
fd , err = openRxSocket ( rx . idx , et )
2026-07-25 17:58:54 -07:00
if err != nil {
2026-07-25 19:08:11 -07:00
return nil , fmt . Errorf ( "%s rx socket for 0x%04x: %w" , label , et , err )
2026-07-25 17:58:54 -07:00
}
d . rxFDs = append ( d . rxFDs , fd )
d . rxStats = append ( d . rxStats , & rxStats {})
}
2026-07-25 22:38:52 -07:00
// The probe carries its own ethertype so it lands on a socket of its own, but
// it is left unsteered: it is a few frames a second and does not need a queue
// to itself, and the stamps are taken at the wire either way.
d . probeSpec = newFrameSpec ( patIdx , rx . mac , tx . mac , cfg . probeEther , [] int { probeSize })
fd , err := openTxSocket ( tx . idx )
if err != nil {
return nil , fmt . Errorf ( "%s probe tx socket: %w" , label , err )
}
if err := enableTxTimestamps ( fd ); err != nil {
return nil , fmt . Errorf ( "%s probe tx timestamps: %w" , label , err )
}
d . probeTxFD = fd
fd , err = openRxSocket ( rx . idx , cfg . probeEther )
if err != nil {
return nil , fmt . Errorf ( "%s probe rx socket: %w" , label , err )
}
if err := enableRxTimestamps ( fd ); err != nil {
return nil , fmt . Errorf ( "%s probe rx timestamps: %w" , label , err )
}
d . probeRxFD = fd
2026-07-25 22:54:04 -07:00
// Whatever the interfaces have counted before now is not ours.
d . sampleNIC ()
d . nicBase = d . nicNow
2026-07-25 17:58:54 -07:00
return d , nil
}
func ( d * direction ) start ( wg * sync . WaitGroup , doneTx , doneRx * atomic . Bool , cfg config , rxReady * sync . WaitGroup , startTx <- chan struct {}) {
for i , fd := range d . txFDs {
w := & txWorker {
fd : fd ,
stream : uint16 ( i ),
2026-07-25 19:08:11 -07:00
spec : d . specs [ i ],
2026-07-25 17:58:54 -07:00
batch : cfg . batch ,
stats : d . txStats [ i ],
startTx : startTx ,
}
wg . Add ( 1 )
go func () {
defer wg . Done ()
w . run ( doneTx )
}()
}
for i , fd := range d . rxFDs {
w := & rxWorker {
fd : fd ,
batch : cfg . batch ,
2026-07-25 19:08:11 -07:00
spec : d . specs [ i ],
2026-07-25 17:58:54 -07:00
stats : d . rxStats [ i ],
streams : d . streams ,
ready : rxReady ,
}
wg . Add ( 1 )
go func () {
defer wg . Done ()
w . run ( doneRx )
}()
}
2026-07-25 22:38:52 -07:00
sender := & probeSender { fd : d . probeTxFD , spec : d . probeSpec , stats : d . cable }
wg . Add ( 1 )
go func () {
defer wg . Done ()
sender . run ( doneTx , startTx )
}()
receiver := & probeReceiver { fd : d . probeRxFD , stats : d . cable , ready : rxReady }
wg . Add ( 1 )
go func () {
defer wg . Done ()
receiver . run ( doneRx )
}()
2026-07-25 17:58:54 -07:00
}
func ( d * direction ) close () {
for _ , fd := range d . txFDs {
unix . Close ( fd )
}
for _ , fd := range d . rxFDs {
unix . Close ( fd )
}
2026-07-25 22:38:52 -07:00
unix . Close ( d . probeTxFD )
unix . Close ( d . probeRxFD )
2026-07-25 17:58:54 -07:00
}
type config struct {
2026-07-25 22:38:52 -07:00
streams int
batch int
probeEther uint16
zeroNS float64
nsPerM float64
2026-07-25 17:58:54 -07:00
}
2026-07-25 17:24:29 -07:00
func main () {
2026-07-25 17:58:54 -07:00
var (
aName = flag . String ( "a" , "" , "first interface" )
bName = flag . String ( "b" , "" , "second interface" )
2026-07-25 19:15:20 -07:00
sizesArg = flag . String ( "sizes" , "64,128,256,512,1024,1280,1514" , "frame sizes in bytes, excluding FCS, cycled per packet" )
2026-07-25 17:58:54 -07:00
patArg = flag . String ( "pattern" , "prbs" , "payload pattern" )
2026-07-25 19:15:20 -07:00
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" )
2026-07-25 17:58:54 -07:00
batch = flag . Int ( "batch" , 64 , "frames per sendmmsg/recvmmsg call" )
duplex = flag . Bool ( "duplex" , true , "run both directions simultaneously" )
2026-07-25 22:38:52 -07:00
zeroNS = flag . Float64 ( "zero-ns" , 4327.5 , "both directions summed at zero cable length; belongs to the media adapters, recalibrate when they change" )
nsPerM = flag . Float64 ( "ns-per-m" , 10.909 , "both directions summed, per metre of cable" )
2026-07-25 17:58:54 -07:00
)
flag . Parse ()
2026-07-25 19:24:10 -07:00
if err := run ( * aName , * bName , * sizesArg , * patArg ,
2026-07-25 22:38:52 -07:00
* streams , * batch , * duplex , * zeroNS , * nsPerM ); err != nil {
2026-07-25 17:58:54 -07:00
fmt . Fprintln ( os . Stderr , "error:" , err )
os . Exit ( 1 )
}
}
2026-07-25 21:30:37 -07:00
const (
reportInterval = time . Second
// Redraw fast so the panel feels live, but measure rates over a much longer
// window than a frame, since a frame's worth of a bursty sender is noise.
displayInterval = 16 * time . Millisecond
// Only long enough to take the edge off one frame's sample; the estimator
// does the real smoothing, so this stays small and bounded.
rateWindowSpan = 250 * time . Millisecond
totalsHold = 50 * time . Millisecond
)
2026-07-25 18:20:34 -07:00
2026-07-25 19:24:10 -07:00
func run ( aName , bName , sizesArg , patArg string ,
2026-07-25 22:38:52 -07:00
nStreams , batch int , duplex bool , zeroNS , nsPerM float64 ) error {
2026-07-25 17:58:54 -07:00
if aName == "" || bName == "" {
return fmt . Errorf ( "both -a and -b are required" )
}
sizes , err := parseSizes ( sizesArg )
if err != nil {
return err
}
patIdx , err := patternIndex ( patArg )
if err != nil {
return err
}
a , err := lookupEndpoint ( aName )
if err != nil {
return err
}
b , err := lookupEndpoint ( bName )
if err != nil {
return err
}
for _ , e := range [] endpoint { a , b } {
for _ , s := range sizes {
if s > e . mtu + ethHdrLen {
return fmt . Errorf ( "size %d exceeds %s MTU %d (max frame %d)" , s , e . name , e . mtu , e . mtu + ethHdrLen )
}
}
}
2026-07-25 18:13:58 -07:00
a . tag , b . tag = "A" , "B"
2026-07-25 18:07:25 -07:00
ifnames := [] string { a . name , b . name }
2026-07-25 18:13:58 -07:00
2026-07-25 19:08:11 -07:00
if nStreams < 1 {
return fmt . Errorf ( "need at least one stream" )
}
ethertypes := make ([] uint16 , nStreams )
for i := range ethertypes {
ethertypes [ i ] = uint16 ( etherBase + i )
}
2026-07-25 18:07:25 -07:00
var fatal [] string
2026-07-25 18:13:58 -07:00
var tuneRows [][] string
2026-07-25 19:08:11 -07:00
for _ , r := range configureSystem ( ifnames , ethertypes ) {
2026-07-25 18:13:58 -07:00
tuneRows = append ( tuneRows , [] string { r . item , r . status (), r . detail ()})
2026-07-25 18:07:25 -07:00
if r . fatal {
fatal = append ( fatal , r . item )
}
}
2026-07-25 18:13:58 -07:00
fmt . Println ( renderBox ( "HOST SETTINGS" ,
[] string { "CHECK" , "STATUS" , "DETAIL" },
[] bool { false , false , false }, tuneRows ))
2026-07-25 18:07:25 -07:00
if len ( fatal ) > 0 {
return fmt . Errorf ( "cannot test with %s in this state" , strings . Join ( fatal , ", " ))
}
2026-07-25 17:58:54 -07:00
cfg := config {
2026-07-25 22:38:52 -07:00
streams : nStreams ,
batch : batch ,
probeEther : uint16 ( etherBase + nStreams ),
zeroNS : zeroNS ,
nsPerM : nsPerM ,
2026-07-25 17:58:54 -07:00
}
var dirs [] * direction
2026-07-25 19:08:11 -07:00
d0 , err := buildDirection ( a . name + "->" + b . name , a , b , patIdx , sizes , cfg )
2026-07-25 17:58:54 -07:00
if err != nil {
return err
}
dirs = append ( dirs , d0 )
if duplex {
2026-07-25 19:08:11 -07:00
d1 , err := buildDirection ( b . name + "->" + a . name , b , a , patIdx , sizes , cfg )
2026-07-25 17:58:54 -07:00
if err != nil {
return err
}
dirs = append ( dirs , d1 )
}
defer func () {
for _ , d := range dirs {
d . close ()
}
}()
2026-07-25 18:13:58 -07:00
var linkRows [][] string
for _ , e := range [] endpoint { a , b } {
linkRows = append ( linkRows , [] string {
paint ( e . tag , cCyan ), e . name , e . macString (),
fmt . Sprintf ( "%.0f Gb/s" , e . speed ), fmt . Sprintf ( "%d" , e . mtu ),
})
}
fmt . Println ( renderBox ( "LINKS" ,
[] string { "TAG" , "INTERFACE" , "MAC" , "SPEED" , "MTU" },
[] bool { false , false , false , true , true }, linkRows ))
target := a . speed
if target <= 0 {
target = 10
}
sizeStrs := make ([] string , len ( sizes ))
for i , s := range sizes {
sizeStrs [ i ] = fmt . Sprintf ( "%d" , s )
}
fmt . Println ( renderBox ( "TEST" ,
[] string { "SETTING" , "VALUE" },
[] bool { false , false }, [][] string {
{ "pattern" , patterns [ patIdx ]. name },
{ "frame sizes" , strings . Join ( sizeStrs , " " )},
2026-07-25 19:08:11 -07:00
{ "streams" , fmt . Sprintf ( "%d per direction, ethertypes 0x%04x-0x%04x, one rx queue each" ,
nStreams , ethertypes [ 0 ], ethertypes [ len ( ethertypes ) - 1 ])},
2026-07-25 18:13:58 -07:00
{ "batch" , fmt . Sprintf ( "%d frames per syscall" , batch )},
2026-07-25 18:20:34 -07:00
{ "payload verify" , "crc32c on every frame" },
2026-07-25 22:38:52 -07:00
{ "cable probe" , fmt . Sprintf ( "%d-byte frame on ethertype 0x%04x every %s, hardware stamped at both macs" ,
probeSize , cfg . probeEther , probeInterval )},
2026-07-25 18:13:58 -07:00
{ "duplex" , fmt . Sprintf ( "%v" , duplex )},
{ "socket buffers" , fmt . Sprintf ( "sndbuf %s, rcvbuf %s (granted)" ,
humanBytes ( uint64 ( sockBufSize ( dirs [ 0 ]. txFDs [ 0 ], unix . SO_SNDBUF ))),
humanBytes ( uint64 ( sockBufSize ( dirs [ 0 ]. rxFDs [ 0 ], unix . SO_RCVBUF ))))},
}))
2026-07-25 19:29:00 -07:00
fmt . Println ( paint ( "rates are per interval; error counts are cumulative, press space to reset them" , cDim ))
2026-07-25 18:13:58 -07:00
fmt . Println ()
2026-07-25 17:58:54 -07:00
var doneTx , doneRx atomic . Bool
var wg sync . WaitGroup
var rxReady sync . WaitGroup
startTx := make ( chan struct {})
for _ , d := range dirs {
2026-07-25 22:38:52 -07:00
rxReady . Add ( len ( d . rxFDs ) + 1 )
2026-07-25 17:58:54 -07:00
}
for _ , d := range dirs {
d . start ( & wg , & doneTx , & doneRx , cfg , & rxReady , startTx )
}
rxReady . Wait ()
sig := make ( chan os . Signal , 1 )
signal . Notify ( sig , syscall . SIGINT , syscall . SIGTERM )
2026-07-25 19:29:00 -07:00
space , restoreTerm := watchSpace ()
defer restoreTerm ()
2026-07-25 19:41:53 -07:00
disp , err := newDisplay ()
if err != nil {
return fmt . Errorf ( "display: %w" , err )
}
defer disp . close ()
2026-07-25 21:41:21 -07:00
touch , err := watchTouch ( disp . fb . w , disp . fb . h )
if err != nil {
return fmt . Errorf ( "touchscreen: %w" , err )
}
2026-07-25 17:58:54 -07:00
start := time . Now ()
close ( startTx )
2026-07-25 18:33:36 -07:00
tick := time . NewTicker ( reportInterval )
2026-07-25 17:58:54 -07:00
defer tick . Stop ()
2026-07-25 21:30:37 -07:00
frame := time . NewTicker ( displayInterval )
defer frame . Stop ()
2026-07-25 17:58:54 -07:00
last := time . Now ()
2026-07-25 21:30:37 -07:00
views := make ([] view , len ( dirs ))
2026-07-25 22:38:52 -07:00
rows := make ([] view , len ( dirs ))
2026-07-25 21:30:37 -07:00
for _ , d := range dirs {
d . win = newRateWindow ( int ( rateWindowSpan / displayInterval ) + 1 )
d . est = newRateEstimators ()
}
2026-07-25 18:13:58 -07:00
stats := & streamTable { cols : intervalCols , headerEvery : 20 }
2026-07-25 17:58:54 -07:00
for {
select {
case <- sig :
2026-07-25 18:33:36 -07:00
doneTx . Store ( true )
doneRx . Store ( true )
wg . Wait ()
return nil
2026-07-25 19:29:00 -07:00
case <- space :
2026-07-25 21:41:21 -07:00
start = resetAll ( dirs , stats )
2026-07-25 21:30:37 -07:00
case now := <- frame . C :
2026-07-25 21:41:21 -07:00
if x , y , down := touch . get (); disp . holdReset ( x , y , down , now ) {
start = resetAll ( dirs , stats )
}
2026-07-25 21:30:37 -07:00
for i , d := range dirs {
views [ i ] = d . displayView ( now )
}
2026-07-25 22:38:52 -07:00
disp . render ( dirs , views , now . Sub ( start ), target , cfg . cableText ( views ))
2026-07-25 17:58:54 -07:00
case now := <- tick . C :
secs := now . Sub ( last ). Seconds ()
last = now
2026-07-25 18:33:36 -07:00
elapsed := now . Sub ( start )
2026-07-25 22:38:52 -07:00
// Length needs both directions, so every row is sampled before any of
// them is printed.
for i , d := range dirs {
2026-07-25 22:54:04 -07:00
d . sampleNIC ()
2026-07-25 22:38:52 -07:00
rows [ i ] = d . view ( & d . prevConsole , secs )
}
length := "-"
if m , ok := cfg . cableMetres ( rows ); ok {
length = fmt . Sprintf ( "%.1f" , m )
}
for i , d := range dirs {
for _ , line := range stats . emit ( d . row ( elapsed , rows [ i ], target , length )) {
2026-07-25 18:13:58 -07:00
fmt . Println ( line )
}
2026-07-25 17:58:54 -07:00
}
}
}
2026-07-25 17:24:29 -07:00
}