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 18:33:36 -07:00
prev sample
drops 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 parseCPUs ( s string ) ([] int , error ) {
if strings . TrimSpace ( s ) == "" {
return nil , nil
}
var out [] int
for _ , f := range strings . Split ( s , "," ) {
v , err := strconv . Atoi ( strings . TrimSpace ( f ))
if err != nil {
return nil , fmt . Errorf ( "bad cpu %q: %w" , f , err )
}
out = append ( out , v )
}
return out , nil
}
func cpuAt ( cpus [] int , i int ) int {
if len ( cpus ) == 0 {
return - 1
}
return cpus [ i % len ( cpus )]
}
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
}
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 18:33:36 -07:00
func ( d * direction ) intervalRow ( elapsed time . Duration , secs , target float64 ) [] string {
2026-07-25 17:58:54 -07:00
now := d . snapshot ()
p := d . prev
d . prev = now
txF := now . txFrames - p . txFrames
txB := now . txBytes - p . txBytes
rxF := now . rxFrames - p . rxFrames
rxB := now . rxBytes - p . rxBytes
d . sampleDrops ()
2026-07-25 18:33:36 -07:00
total := now . lost + now . crcErr + now . badMagic + now . badLen + d . drops
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 ),
commas ( uint64 ( float64 ( txF ) / secs )),
rateCell ( gbps ( txB , txF , secs ), target ),
commas ( uint64 ( float64 ( rxF ) / secs )),
rateCell ( gbps ( rxB , rxF , secs ), target ),
2026-07-25 18:45:47 -07:00
statusCell ( now . lost ),
statusCell ( now . late ),
statusCell ( now . crcErr ),
statusCell ( now . badMagic ),
statusCell ( d . drops ),
2026-07-25 18:33:36 -07:00
statusCell ( total ),
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 ,
cpu : cpuAt ( cfg . txCPUs , i ),
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 ,
cpu : cpuAt ( cfg . rxCPUs , i ),
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
txCPUs [] int
rxCPUs [] 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" )
txCPUs = flag . String ( "txcpus" , "" , "comma-separated CPUs to pin tx workers to" )
rxCPUs = flag . String ( "rxcpus" , "" , "comma-separated CPUs to pin rx workers to" )
)
flag . Parse ()
2026-07-25 19:08:11 -07:00
if err := run ( * aName , * bName , * sizesArg , * patArg , * txCPUs , * rxCPUs ,
* 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:08:11 -07:00
func run ( aName , bName , sizesArg , patArg , txCPUsArg , rxCPUsArg string ,
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
}
txCPUs , err := parseCPUs ( txCPUsArg )
if err != nil {
return err
}
rxCPUs , err := parseCPUs ( rxCPUsArg )
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 ,
txCPUs : txCPUs ,
rxCPUs : rxCPUs ,
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 18:45:47 -07:00
fmt . Println ( paint ( "rates are per interval; error counts are cumulative since start" , 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 )
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 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 17:58:54 -07:00
for _ , d := range dirs {
2026-07-25 18:13:58 -07:00
for _ , line := range stats . emit ( d . intervalRow ( elapsed , secs , target )) {
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 17:24:29 -07:00
}