2026-07-25 17:24:29 -07:00
package main
2026-07-25 17:58:54 -07:00
import (
"flag"
"fmt"
"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
reports chan string
2026-07-25 19:29:00 -07:00
prev sample
drops uint64
errBase sample
dropBase uint64
nicTX nicCounters
nicRX nicCounters
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
}
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 ()
}
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
// the display subtracts from.
func ( d * direction ) resetErrors () {
d . sampleDrops ()
d . errBase = d . snapshot ()
d . dropBase = d . drops
}
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 },
{ title : "ERRORS" , width : 11 , 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
kdrop , errors uint64
}
func ( d * direction ) view ( secs float64 ) view {
2026-07-25 17:58:54 -07:00
now := d . snapshot ()
p := d . prev
d . prev = now
2026-07-25 19:41:53 -07:00
d . sampleDrops ()
2026-07-25 17:58:54 -07:00
txF := now . txFrames - p . txFrames
rxF := now . rxFrames - p . rxFrames
2026-07-25 19:29:00 -07:00
b := d . errBase
2026-07-25 19:41:53 -07:00
v := view {
txPPS : float64 ( txF ) / secs ,
rxPPS : float64 ( rxF ) / secs ,
txGbps : gbps ( now . txBytes - p . txBytes , txF , secs ),
rxGbps : gbps ( now . rxBytes - p . rxBytes , rxF , secs ),
txFrames : now . txFrames ,
txSent : now . txBytes ,
rxFrames : now . rxFrames ,
rxGot : now . rxBytes ,
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 ,
}
v . errors = v . lost + v . crc + v . badMagic + ( now . badLen - b . badLen ) + v . kdrop
return v
}
2026-07-25 18:13:58 -07:00
2026-07-25 19:41:53 -07:00
func ( d * direction ) row ( elapsed time . Duration , v view , target float64 ) [] 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 ),
statusCell ( v . errors ),
2026-07-25 18:13:58 -07:00
}
2026-07-25 17:58:54 -07:00
}
2026-07-25 18:13:58 -07:00
func ( d * direction ) reportNIC () [] string {
2026-07-25 17:58:54 -07:00
tx := readNIC ( d . tx . name )
rx := readNIC ( d . rx . name )
var parts [] string
if s := tx . diff ( d . nicTX ); s != "" {
2026-07-25 18:13:58 -07:00
parts = append ( parts , paint ( " ▲ " + d . tx . name + " tx-side: " + s , cYellow ))
2026-07-25 17:58:54 -07:00
}
if s := rx . diff ( d . nicRX ); s != "" {
2026-07-25 18:13:58 -07:00
parts = append ( parts , paint ( " ▲ " + d . rx . name + " rx-side: " + s , cYellow ))
2026-07-25 17:58:54 -07:00
}
d . nicTX = tx
d . nicRX = rx
2026-07-25 18:13:58 -07:00
return parts
}
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 17:58:54 -07:00
reports : make ( chan string , 64 ),
}
d . nicTX = readNIC ( tx . name )
d . nicRX = readNIC ( rx . name )
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 {})
}
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 ,
reports : d . reports ,
ready : rxReady ,
}
wg . Add ( 1 )
go func () {
defer wg . Done ()
w . run ( doneRx )
}()
}
}
func ( d * direction ) close () {
for _ , fd := range d . txFDs {
unix . Close ( fd )
}
for _ , fd := range d . rxFDs {
unix . Close ( fd )
}
}
type config struct {
2026-07-25 19:08:11 -07:00
streams int
batch int
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" )
)
flag . Parse ()
2026-07-25 19:24:10 -07:00
if err := run ( * aName , * bName , * sizesArg , * patArg ,
2026-07-25 19:08:11 -07:00
* streams , * batch , * duplex ); err != nil {
2026-07-25 17:58:54 -07:00
fmt . Fprintln ( os . Stderr , "error:" , err )
os . Exit ( 1 )
}
}
2026-07-25 18:33:36 -07:00
const reportInterval = 500 * 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 19:08:11 -07:00
nStreams , batch int , duplex bool ) 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 19:08:11 -07:00
streams : nStreams ,
batch : batch ,
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 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 {
rxReady . Add ( len ( d . rxFDs ))
}
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 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 ()
last := time . Now ()
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 :
for _ , d := range dirs {
d . resetErrors ()
}
stats . sinceHeader = 0
2026-07-25 19:31:03 -07:00
fmt . Println ( stats . rule ( "error counts reset" ))
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 19:41:53 -07:00
views := make ([] view , len ( dirs ))
for i , d := range dirs {
views [ i ] = d . view ( secs )
for _ , line := range stats . emit ( d . row ( elapsed , views [ i ], target )) {
2026-07-25 18:13:58 -07:00
fmt . Println ( line )
}
for _ , line := range d . reportNIC () {
fmt . Println ( line )
2026-07-25 17:58:54 -07:00
}
for {
select {
case msg := <- d . reports :
2026-07-25 18:13:58 -07:00
fmt . Println ( paint ( " ✗ " + d . short + ": " + msg , cRed ))
2026-07-25 17:58:54 -07:00
continue
default :
}
break
}
}
2026-07-25 19:41:53 -07:00
disp . render ( dirs , views , elapsed , target )
2026-07-25 17:58:54 -07:00
}
}
2026-07-25 17:24:29 -07:00
}