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"
2026-07-31 15:45:03 -07:00
"slices"
2026-07-25 17:58:54 -07:00
"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 {
2026-07-25 18:13:58 -07:00
short string
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
2026-08-04 11:27:30 -07:00
statFD int
2026-07-25 22:38:52 -07:00
cable * cableStats
2026-07-31 16:15:16 -07:00
// 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
2026-07-31 15:45:03 -07:00
prevConsole counterSet
2026-07-25 21:30:37 -07:00
win * rateWindow
drops uint64
2026-07-31 15:45:03 -07:00
base counterSet
2026-07-31 16:15:16 -07:00
heldFrames heldValue
heldSent heldValue
nic atomic . Uint64
poller * nicPoller
2026-07-25 21:30:37 -07:00
}
// 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
}
2026-08-01 16:14:12 -07:00
// Everything the display reads, taken at one instant, so a pair of these
// describes both the rates and the errors over the span between them.
2026-07-31 15:45:03 -07:00
type counterSet struct {
t time . Time
s sample
drops uint64
nic uint64
}
func ( d * direction ) capture ( t time . Time ) counterSet {
d . sampleDrops ()
return counterSet { t : t , s : d . snapshot (), drops : d . drops , nic : d . nic . Load ()}
2026-07-25 21:30:37 -07:00
}
2026-07-31 17:10:25 -07:00
// What someone testing a cable is asking, rather than how each failure happened
2026-08-01 16:14:12 -07:00
// to be noticed.
2026-07-26 10:27:41 -07:00
type errs struct {
2026-07-31 17:10:25 -07:00
lost uint64
corrupt uint64
link uint64
internal uint64
2026-07-26 10:27:41 -07:00
}
func ( e errs ) total () uint64 {
2026-07-31 17:10:25 -07:00
return e . lost + e . corrupt + e . link + e . internal
2026-07-26 10:27:41 -07:00
}
func ( e errs ) add ( o errs ) errs {
return errs {
2026-07-31 17:10:25 -07:00
lost : e . lost + o . lost , corrupt : e . corrupt + o . corrupt ,
link : e . link + o . link , internal : e . internal + o . internal ,
2026-07-26 10:27:41 -07:00
}
}
2026-07-31 15:45:03 -07:00
// A ring of one bucket per drawn frame, spanning rateWindowSpan. Rates come
// from the gap between adjacent buckets and errors from the ends of the ring,
// so both slide forward every frame instead of stepping once a second.
2026-07-25 21:30:37 -07:00
type rateWindow struct {
2026-07-31 15:45:03 -07:00
buf [] counterSet
2026-07-25 21:30:37 -07:00
idx int
filled bool
2026-07-31 15:45:03 -07:00
scratch [] float64
2026-07-25 21:30:37 -07:00
}
func newRateWindow ( n int ) * rateWindow {
2026-07-31 15:45:03 -07:00
return & rateWindow { buf : make ([] counterSet , n )}
2026-07-25 21:30:37 -07:00
}
2026-07-31 15:45:03 -07:00
func ( w * rateWindow ) push ( c counterSet ) {
w . buf [ w . idx ] = c
2026-07-25 21:30:37 -07:00
w . idx ++
2026-07-31 15:45:03 -07:00
if w . idx == len ( w . buf ) {
2026-07-25 21:30:37 -07:00
w . idx = 0
w . filled = true
}
}
2026-07-31 15:45:03 -07:00
func ( w * rateWindow ) count () int {
if w . filled {
return len ( w . buf )
2026-07-25 21:30:37 -07:00
}
2026-07-31 15:45:03 -07:00
return w . idx
}
// Indexed oldest first, so a partly filled ring reads the same as a full one.
func ( w * rateWindow ) at ( i int ) counterSet {
2026-07-25 21:30:37 -07:00
if w . filled {
2026-07-31 15:45:03 -07:00
i += w . idx
}
return w . buf [ i % len ( w . buf )]
}
// The median is steady against a bursty sender yet only ever a rate some bucket
// actually measured, so a step change is shown as a step: the old value holds
// until half the ring has turned over and then the new one takes it, passing
// through at most the one bucket the crossing lands on. Averaging the ring
// instead would spend the whole span sliding through rates that never happened.
func ( w * rateWindow ) median ( rate func ( prev , cur counterSet , secs float64 ) float64 ) float64 {
n := w . count ()
if n < 2 {
return 0
}
w . scratch = w . scratch [: 0 ]
prev := w . at ( 0 )
for i := 1 ; i < n ; i ++ {
cur := w . at ( i )
if secs := cur . t . Sub ( prev . t ). Seconds (); secs > 0 {
w . scratch = append ( w . scratch , rate ( prev , cur , secs ))
}
prev = cur
2026-07-25 21:30:37 -07:00
}
2026-07-31 15:45:03 -07:00
if len ( w . scratch ) == 0 {
return 0
}
slices . Sort ( w . scratch )
return w . scratch [ len ( w . scratch ) / 2 ]
}
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 )
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
2026-08-04 12:45:12 -07:00
txErrs 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 ()
}
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-31 16:15:16 -07:00
d . mu . Lock ()
2026-07-31 15:45:03 -07:00
d . base = d . capture ( time . Now ())
2026-07-31 16:15:16 -07:00
d . mu . Unlock ()
2026-07-25 21:41:21 -07:00
d . heldFrames = heldValue {}
d . heldSent = heldValue {}
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-08-01 16:07:44 -07:00
{ title : "ELAPSED" , width : 9 , right : true },
2026-07-25 18:13:58 -07:00
{ title : "DIR" , width : 5 },
2026-08-01 16:07:44 -07:00
{ 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 },
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-08-01 16:14:12 -07:00
// Shared by the console table and the framebuffer so both show the same
// figures.
2026-07-25 19:41:53 -07:00
type view struct {
2026-07-26 10:27:41 -07:00
txPPS , rxPPS float64
txGbps , rxGbps float64
rxFrames , rxGot uint64
since errs
window errs
cable cableView
2026-07-25 19:41:53 -07:00
}
2026-07-31 15:45:03 -07:00
func errsBetween ( b , n counterSet ) errs {
return errs {
2026-07-31 17:10:25 -07:00
lost : n . s . lost - b . s . lost ,
// Three ways of noticing one thing: a payload that does not match its
// checksum, a header that is not ours, and a length that cannot be.
corrupt : ( n . s . crcErr - b . s . crcErr ) + ( n . s . badMagic - b . s . badMagic ) +
( n . s . badLen - b . s . badLen ),
2026-08-04 12:45:12 -07:00
// What the hardware reported. Nothing the host declined to send is here,
// so this one going red means the cable.
link : ( n . nic - b . nic ) + ( n . s . rxErrs - b . s . rxErrs ),
// Ours rather than the cable's. A late frame is unreachable while each
// stream has a flow rule to its own queue, which is exactly why it is
// worth counting.
internal : ( n . drops - b . drops ) + ( n . s . late - b . s . late ) +
( n . s . txErrs - b . s . txErrs ),
2026-07-31 15:45:03 -07:00
}
}
func ( d * direction ) counters ( now counterSet ) view {
2026-07-26 10:27:41 -07:00
return view {
2026-07-31 15:45:03 -07:00
rxFrames : now . s . rxFrames - d . base . s . rxFrames ,
rxGot : now . s . rxBytes - d . base . s . rxBytes ,
2026-07-25 22:38:52 -07:00
cable : d . cable . view (),
2026-07-31 15:45:03 -07:00
since : errsBetween ( d . base , now ),
2026-07-26 10:27:41 -07:00
}
}
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 . rxGot += v . rxGot
t . since = t . since . add ( v . since )
t . window = t . window . add ( v . window )
}
return t
2026-07-25 19:41:53 -07:00
}
2026-07-25 18:13:58 -07:00
2026-07-31 15:45:03 -07:00
func ( d * direction ) view ( t time . Time ) view {
2026-07-31 16:15:16 -07:00
d . mu . Lock ()
defer d . mu . Unlock ()
2026-07-31 15:45:03 -07:00
now := d . capture ( t )
p := d . prevConsole
d . prevConsole = now
2026-07-25 21:30:37 -07:00
v := d . counters ( now )
2026-07-31 15:45:03 -07:00
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 )
2026-07-25 21:30:37 -07:00
return v
}
2026-07-31 16:15:16 -07:00
func ( d * direction ) sample ( t time . Time ) {
d . mu . Lock ()
d . win . push ( d . capture ( t ))
d . mu . Unlock ()
}
2026-07-25 21:30:37 -07:00
2026-08-01 16:14:12 -07:00
// Draws what the sampler last put in the ring rather than reading the counters
// again, so the display never participates in the measurement.
2026-07-31 16:15:16 -07:00
func ( d * direction ) displayView ( t time . Time ) view {
d . mu . Lock ()
n := d . win . count ()
if n == 0 {
d . mu . Unlock ()
return view { cable : d . cable . view ()}
}
v := d . counters ( d . win . at ( n - 1 ))
if n >= 2 {
2026-07-31 15:45:03 -07:00
v . window = errsBetween ( d . win . at ( 0 ), d . win . at ( n - 1 ))
2026-07-25 21:30:37 -07:00
}
2026-07-31 15:45:03 -07:00
v . txPPS = d . win . median ( txRatePPS )
v . rxPPS = d . win . median ( rxRatePPS )
v . txGbps = d . win . median ( txRateGbps )
v . rxGbps = d . win . median ( rxRateGbps )
2026-07-31 16:15:16 -07:00
d . mu . Unlock ()
v . rxFrames = d . heldFrames . get ( t , v . rxFrames )
v . rxGot = d . heldSent . get ( t , v . rxGot )
2026-07-25 21:30:37 -07:00
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-08-01 16:07:44 -07:00
scaleTime ( elapsed ),
2026-07-25 18:13:58 -07:00
paint ( d . short , cCyan ),
2026-08-01 16:07:44 -07:00
scaleSI ( v . txPPS ),
rateCell ( v . txGbps * 1e9 , target * 1e9 ),
scaleSI ( v . rxPPS ),
rateCell ( v . rxGbps * 1e9 , target * 1e9 ),
2026-07-26 10:27:41 -07:00
statusCell ( v . since . lost ),
2026-07-31 17:10:25 -07:00
statusCell ( v . since . corrupt ),
2026-07-26 10:27:41 -07:00
statusCell ( v . since . link ),
2026-07-31 17:10:25 -07:00
statusCell ( v . since . internal ),
2026-07-26 10:27:41 -07:00
statusCell ( v . since . total ()),
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-26 10:27:41 -07:00
// Whatever the interfaces counted before now is not ours, and no interval has
// elapsed yet, so every baseline starts here and nothing is reported until the
// first one completes.
func ( d * direction ) primeCounters () {
2026-07-31 15:45:03 -07:00
d . poller . prime ()
2026-07-26 10:27:41 -07:00
d . reset ()
2026-07-31 15:45:03 -07:00
d . prevConsole = d . base
2026-07-25 18:13:58 -07:00
}
2026-07-25 23:03:31 -07:00
func buildDirection ( label string , tx , rx endpoint , sizes [] int , cfg config ) ( * direction , error ) {
2026-07-25 17:58:54 -07:00
d := & direction {
2026-07-25 18:13:58 -07:00
short : tx . tag + "→" + rx . tag ,
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-08-04 11:27:30 -07:00
// Held open for the life of the run: the stats ioctl is issued five times a
// second and reopening a socket for each one is pure overhead.
statFD , err := unix . Socket ( unix . AF_INET , unix . SOCK_DGRAM , 0 )
if err != nil {
return nil , fmt . Errorf ( "%s stats socket: %w" , label , err )
}
d . statFD = statFD
d . poller , err = newNICPoller ( statFD , tx . name , rx . name , & d . nic )
if err != nil {
return nil , fmt . Errorf ( "%s: %w" , label , err )
}
2026-07-31 16:15:16 -07:00
d . win = newRateWindow ( int ( rateWindowSpan / sampleInterval ) + 1 )
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 )
2026-07-25 23:03:31 -07:00
d . specs = append ( d . specs , newFrameSpec ( 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
2026-07-25 23:03:31 -07:00
// Deliberately given no flow rule: a few frames a second does not need a
// queue of its own, and the stamps are taken at the wire either way.
d . probeSpec = newFrameSpec ( rx . mac , tx . mac , cfg . probeEther , [] int { probeSize })
2026-07-25 22:38:52 -07:00
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
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-31 15:45:03 -07:00
wg . Add ( 1 )
go func () {
defer wg . Done ()
d . poller . run ( doneRx , startTx )
}()
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-08-04 11:27:30 -07:00
unix . Close ( d . statFD )
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
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 (
2026-07-26 15:50:55 -07:00
// The names the kernel gives the only two ports built into it, since as
// PID 1 there is no udev to rename them and no command line to pass.
aName = flag . String ( "a" , "eth0" , "first interface" )
bName = flag . String ( "b" , "eth1" , "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" )
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" )
2026-07-26 09:43:03 -07:00
2026-07-26 15:51:52 -07:00
nsPerM = flag . Float64 ( "ns-per-m" , 4.85 , "mean of both directions, per metre of cable" )
2026-07-25 17:58:54 -07:00
)
flag . Parse ()
2026-07-26 10:54:19 -07:00
// Nothing here is recoverable by the time it reaches this point, and as PID 1
// a plain exit would panic the kernel anyway with less to show for it.
2026-07-25 23:03:31 -07:00
if err := run ( * aName , * bName , * sizesArg ,
2026-07-26 09:43:03 -07:00
* streams , * batch , * nsPerM ); err != nil {
2026-07-26 10:54:19 -07:00
panic ( err )
2026-07-25 17:58:54 -07:00
}
}
2026-07-25 21:30:37 -07:00
const (
reportInterval = time . Second
2026-08-01 16:14:12 -07:00
// Deliberately not tied to the refresh: letting a slow or blocked draw set
// the sampling clock would stretch the window it reports.
2026-07-31 16:15:16 -07:00
sampleInterval = 16 * time . Millisecond
2026-08-01 16:14:12 -07:00
// How far back the shown errors reach and how many buckets the median runs
// over, so a step in the rate lands half this late.
2026-07-31 15:45:03 -07:00
rateWindowSpan = time . Second
2026-07-25 21:30:37 -07:00
totalsHold = 50 * time . Millisecond
)
2026-07-25 18:20:34 -07:00
2026-07-31 16:15:16 -07:00
// One sampler for both directions, so their buckets share an instant and the
2026-08-01 16:14:12 -07:00
// cable length, which needs a figure from each, never mixes two moments.
2026-07-31 16:15:16 -07:00
type sampler struct {
dirs [] * direction
}
func ( s * sampler ) run ( done * atomic . Bool , startTx <- chan struct {}) {
<- startTx
tick := time . NewTicker ( sampleInterval )
defer tick . Stop ()
for ! done . Load () {
now := <- tick . C
for _ , d := range s . dirs {
d . sample ( now )
}
}
}
2026-07-25 23:03:31 -07:00
func run ( aName , bName , sizesArg string ,
2026-07-26 09:43:03 -07:00
nStreams , batch int , nsPerM float64 ) error {
2026-07-25 17:58:54 -07:00
sizes , err := parseSizes ( sizesArg )
if err != nil {
return err
}
2026-07-26 10:54:19 -07:00
2026-07-26 15:50:55 -07:00
if err := reportChecks ( "BOOT" , bootstrap ()); err != nil {
2026-07-26 10:54:19 -07:00
return err
}
2026-07-25 17:58:54 -07:00
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-26 10:54:19 -07:00
if err := reportChecks ( "HOST SETTINGS" , configureSystem ( ifnames , ethertypes )); err != nil {
return err
2026-07-25 18:07:25 -07:00
}
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 ),
nsPerM : nsPerM ,
2026-07-25 17:58:54 -07:00
}
var dirs [] * direction
2026-07-25 23:03:31 -07:00
for _ , p := range [][ 2 ] endpoint {{ a , b }, { b , a }} {
d , err := buildDirection ( p [ 0 ]. name + "->" + p [ 1 ]. name , p [ 0 ], p [ 1 ], sizes , cfg )
2026-07-25 17:58:54 -07:00
if err != nil {
return err
}
2026-07-25 23:03:31 -07:00
dirs = append ( dirs , d )
2026-07-25 17:58:54 -07:00
}
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 )
}
2026-07-25 23:03:31 -07:00
fmt . Println ( renderBox ( "CONFIG" ,
2026-07-25 18:13:58 -07:00
[] string { "SETTING" , "VALUE" },
[] bool { false , false }, [][] string {
{ "frame sizes" , strings . Join ( sizeStrs , " " )},
2026-07-25 23:03:31 -07:00
{ "streams" , fmt . Sprintf ( "%d per direction, ethertypes 0x%04x-0x%04x" ,
2026-07-25 19:08:11 -07:00
nStreams , ethertypes [ 0 ], ethertypes [ len ( ethertypes ) - 1 ])},
2026-07-25 23:03:31 -07:00
{ "probe" , fmt . Sprintf ( "ethertype 0x%04x every %s" , cfg . probeEther , probeInterval )},
2026-07-25 18:13:58 -07:00
{ "batch" , fmt . Sprintf ( "%d frames per syscall" , batch )},
2026-07-26 09:43:03 -07:00
{ "calibration" , fmt . Sprintf ( "%g ns/m, zero taken from the shortest delay seen so far" , nsPerM )},
2026-07-25 23:03:31 -07:00
{ "buffers" , fmt . Sprintf ( "sndbuf %s, rcvbuf %s" ,
2026-07-25 18:13:58 -07:00
humanBytes ( uint64 ( sockBufSize ( dirs [ 0 ]. txFDs [ 0 ], unix . SO_SNDBUF ))),
humanBytes ( uint64 ( sockBufSize ( dirs [ 0 ]. rxFDs [ 0 ], unix . SO_RCVBUF ))))},
}))
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 )
}
2026-07-31 16:15:16 -07:00
samp := & sampler { dirs : dirs }
wg . Add ( 1 )
go func () {
defer wg . Done ()
samp . run ( & doneRx , startTx )
}()
2026-07-25 17:58:54 -07:00
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-26 10:27:41 -07:00
touch , err := watchTouch ( disp . fb . pw , disp . fb . ph )
2026-07-25 21:41:21 -07:00
if err != nil {
return fmt . Errorf ( "touchscreen: %w" , err )
}
2026-07-26 10:27:41 -07:00
for _ , d := range dirs {
d . primeCounters ()
}
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
views := make ([] view , len ( dirs ))
2026-07-25 22:38:52 -07:00
rows := make ([] view , len ( dirs ))
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-31 16:46:11 -07:00
case <- disp . fb . flips :
now := time . Now ()
2026-07-26 10:27:41 -07:00
px , py , down := touch . get ()
x , y := disp . fb . fromPanel ( px , py )
if disp . holdReset ( x , y , down , now ) {
2026-07-25 21:41:21 -07:00
start = resetAll ( dirs , stats )
}
2026-07-25 21:30:37 -07:00
for i , d := range dirs {
views [ i ] = d . displayView ( now )
}
2026-08-01 16:07:44 -07:00
// Empty until the probe has a stamp from each direction, so the
// panel shows nothing there rather than a placeholder.
cable := ""
2026-07-26 10:27:41 -07:00
if m , ok := cfg . cableMetres ( views ); ok {
2026-08-01 16:07:44 -07:00
cable = fmt . Sprintf ( "%.1f" , m )
2026-07-26 10:27:41 -07:00
}
2026-08-01 16:07:44 -07:00
if err := disp . render ( totalView ( views ), now . Sub ( start ), cable ); err != nil {
2026-07-31 16:46:11 -07:00
return err
}
2026-07-25 17:58:54 -07:00
case now := <- tick . C :
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-31 15:45:03 -07:00
rows [ i ] = d . view ( now )
2026-07-25 22:38:52 -07:00
}
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
}