Measure cable length from hardware transmit and receive timestamps
This commit is contained in:
@@ -46,6 +46,11 @@ type direction struct {
|
||||
rxFDs []int
|
||||
reports chan string
|
||||
|
||||
probeSpec *frameSpec
|
||||
probeTxFD int
|
||||
probeRxFD int
|
||||
cable *cableStats
|
||||
|
||||
prevConsole sample
|
||||
win *rateWindow
|
||||
est *rateEstimators
|
||||
@@ -259,6 +264,7 @@ func (d *direction) reset() {
|
||||
d.dropBase = d.drops
|
||||
d.heldFrames = heldValue{}
|
||||
d.heldSent = heldValue{}
|
||||
d.cable.reset()
|
||||
}
|
||||
|
||||
// Returns the new start time, so the uptime shown alongside the totals counts
|
||||
@@ -295,6 +301,8 @@ var intervalCols = []colSpec{
|
||||
{title: "BADMAG", width: 7, right: true},
|
||||
{title: "KDROP", width: 11, right: true},
|
||||
{title: "ERRORS", width: 11, right: true},
|
||||
{title: "MIN ns", width: 9, right: true},
|
||||
{title: "LEN m", width: 6, right: true},
|
||||
}
|
||||
|
||||
// One interval's numbers, shared by the console table and the framebuffer so
|
||||
@@ -307,6 +315,7 @@ type view struct {
|
||||
lost, late uint64
|
||||
crc, badMagic uint64
|
||||
kdrop, errors uint64
|
||||
cable cableView
|
||||
}
|
||||
|
||||
// Cumulative fields, which need no rate window and are identical for both the
|
||||
@@ -323,6 +332,7 @@ func (d *direction) counters(now sample) view {
|
||||
crc: now.crcErr - b.crcErr,
|
||||
badMagic: now.badMagic - b.badMagic,
|
||||
kdrop: d.drops - d.dropBase,
|
||||
cable: d.cable.view(),
|
||||
}
|
||||
v.errors = v.lost + v.crc + v.badMagic + (now.badLen - b.badLen) + v.kdrop
|
||||
return v
|
||||
@@ -375,7 +385,7 @@ func (d *direction) displayView(t time.Time) view {
|
||||
return v
|
||||
}
|
||||
|
||||
func (d *direction) row(elapsed time.Duration, v view, target float64) []string {
|
||||
func (d *direction) row(elapsed time.Duration, v view, target float64, length string) []string {
|
||||
return []string{
|
||||
uptime(elapsed),
|
||||
paint(d.short, cCyan),
|
||||
@@ -389,6 +399,8 @@ func (d *direction) row(elapsed time.Duration, v view, target float64) []string
|
||||
statusCell(v.badMagic),
|
||||
statusCell(v.kdrop),
|
||||
statusCell(v.errors),
|
||||
paint(v.cable.minText(), cCyan),
|
||||
paint(length, cCyan),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,6 +449,28 @@ func buildDirection(label string, tx, rx endpoint, patIdx int, sizes []int, cfg
|
||||
d.rxFDs = append(d.rxFDs, fd)
|
||||
d.rxStats = append(d.rxStats, &rxStats{})
|
||||
}
|
||||
|
||||
// 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.cable = newCableStats()
|
||||
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
|
||||
return d, nil
|
||||
}
|
||||
|
||||
@@ -472,6 +506,20 @@ func (d *direction) start(wg *sync.WaitGroup, doneTx, doneRx *atomic.Bool, cfg c
|
||||
w.run(doneRx)
|
||||
}()
|
||||
}
|
||||
|
||||
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)
|
||||
}()
|
||||
}
|
||||
|
||||
func (d *direction) close() {
|
||||
@@ -481,11 +529,16 @@ func (d *direction) close() {
|
||||
for _, fd := range d.rxFDs {
|
||||
unix.Close(fd)
|
||||
}
|
||||
unix.Close(d.probeTxFD)
|
||||
unix.Close(d.probeRxFD)
|
||||
}
|
||||
|
||||
type config struct {
|
||||
streams int
|
||||
batch int
|
||||
streams int
|
||||
batch int
|
||||
probeEther uint16
|
||||
zeroNS float64
|
||||
nsPerM float64
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -497,11 +550,13 @@ func main() {
|
||||
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")
|
||||
batch = flag.Int("batch", 64, "frames per sendmmsg/recvmmsg call")
|
||||
duplex = flag.Bool("duplex", true, "run both directions simultaneously")
|
||||
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")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if err := run(*aName, *bName, *sizesArg, *patArg,
|
||||
*streams, *batch, *duplex); err != nil {
|
||||
*streams, *batch, *duplex, *zeroNS, *nsPerM); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -519,7 +574,7 @@ const (
|
||||
)
|
||||
|
||||
func run(aName, bName, sizesArg, patArg string,
|
||||
nStreams, batch int, duplex bool) error {
|
||||
nStreams, batch int, duplex bool, zeroNS, nsPerM float64) error {
|
||||
|
||||
if aName == "" || bName == "" {
|
||||
return fmt.Errorf("both -a and -b are required")
|
||||
@@ -575,8 +630,11 @@ func run(aName, bName, sizesArg, patArg string,
|
||||
}
|
||||
|
||||
cfg := config{
|
||||
streams: nStreams,
|
||||
batch: batch,
|
||||
streams: nStreams,
|
||||
batch: batch,
|
||||
probeEther: uint16(etherBase + nStreams),
|
||||
zeroNS: zeroNS,
|
||||
nsPerM: nsPerM,
|
||||
}
|
||||
|
||||
var dirs []*direction
|
||||
@@ -626,6 +684,8 @@ func run(aName, bName, sizesArg, patArg string,
|
||||
nStreams, ethertypes[0], ethertypes[len(ethertypes)-1])},
|
||||
{"batch", fmt.Sprintf("%d frames per syscall", batch)},
|
||||
{"payload verify", "crc32c on every frame"},
|
||||
{"cable probe", fmt.Sprintf("%d-byte frame on ethertype 0x%04x every %s, hardware stamped at both macs",
|
||||
probeSize, cfg.probeEther, probeInterval)},
|
||||
{"duplex", fmt.Sprintf("%v", duplex)},
|
||||
{"socket buffers", fmt.Sprintf("sndbuf %s, rcvbuf %s (granted)",
|
||||
humanBytes(uint64(sockBufSize(dirs[0].txFDs[0], unix.SO_SNDBUF))),
|
||||
@@ -639,7 +699,7 @@ func run(aName, bName, sizesArg, patArg string,
|
||||
var rxReady sync.WaitGroup
|
||||
startTx := make(chan struct{})
|
||||
for _, d := range dirs {
|
||||
rxReady.Add(len(d.rxFDs))
|
||||
rxReady.Add(len(d.rxFDs) + 1)
|
||||
}
|
||||
for _, d := range dirs {
|
||||
d.start(&wg, &doneTx, &doneRx, cfg, &rxReady, startTx)
|
||||
@@ -672,6 +732,7 @@ func run(aName, bName, sizesArg, patArg string,
|
||||
|
||||
last := time.Now()
|
||||
views := make([]view, len(dirs))
|
||||
rows := make([]view, len(dirs))
|
||||
for _, d := range dirs {
|
||||
d.win = newRateWindow(int(rateWindowSpan/displayInterval) + 1)
|
||||
d.est = newRateEstimators()
|
||||
@@ -693,14 +754,22 @@ func run(aName, bName, sizesArg, patArg string,
|
||||
for i, d := range dirs {
|
||||
views[i] = d.displayView(now)
|
||||
}
|
||||
disp.render(dirs, views, now.Sub(start), target)
|
||||
disp.render(dirs, views, now.Sub(start), target, cfg.cableText(views))
|
||||
case now := <-tick.C:
|
||||
secs := now.Sub(last).Seconds()
|
||||
last = now
|
||||
elapsed := now.Sub(start)
|
||||
for _, d := range dirs {
|
||||
v := d.view(&d.prevConsole, secs)
|
||||
for _, line := range stats.emit(d.row(elapsed, v, target)) {
|
||||
// Length needs both directions, so every row is sampled before any of
|
||||
// them is printed.
|
||||
for i, d := range dirs {
|
||||
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)) {
|
||||
fmt.Println(line)
|
||||
}
|
||||
for _, line := range d.reportNIC() {
|
||||
|
||||
Reference in New Issue
Block a user