BCM module diagnostics replace timestamp probes: bringup forces EEE off and runs ECD (re-run on every reset, re-baselining past its blip), 1 Hz SNR margin + corrected counters on panel and console; X520 bench divergences bypassed and tracked in open-questions

This commit is contained in:
flamingcow
2026-08-13 07:10:39 -07:00
parent a00f2216b7
commit 979dc7a772
18 changed files with 1240 additions and 416 deletions
+50 -77
View File
@@ -37,12 +37,7 @@ type direction struct {
streams []lossWindow
txFDs []int
rxFDs []int
probeSpec *frameSpec
probeTxFD int
probeRxFD int
statFD int
cable *cableStats
statFD int
// 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:
@@ -220,16 +215,17 @@ func (d *direction) reset() {
d.base = d.capture()
d.win.push(d.base)
d.mu.Unlock()
d.cable.reset()
}
// 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 {
func resetAll(dirs []*direction, mods []*phyModule, stats *streamTable) time.Time {
for _, d := range dirs {
d.reset()
}
for _, m := range mods {
m.reset()
}
stats.sinceHeader = 0
fmt.Println(stats.rule("counters reset"))
return time.Now()
@@ -248,15 +244,18 @@ func gbps(bytes, frames uint64, secs float64) float64 {
var intervalCols = []colSpec{
{group: "NOW", title: "bits/s", width: 9, right: true},
{group: "NOW", title: "packets/s", width: 9, right: true},
{group: "NOW", title: "snr", width: 6, right: true},
{group: "NOW", title: "lost", width: 7, right: true},
{group: "NOW", title: "corrupt", width: 7, right: true},
{group: "NOW", title: "link", width: 7, right: true},
{group: "NOW", title: "internal", width: 8, right: true},
{group: "NOW", title: "corrected", width: 9, right: true},
{group: "NOW", title: "noise", width: 7, right: true},
{group: "OVERALL", title: "elapsed", width: 9, right: true},
{group: "OVERALL", title: "packets", width: 9, right: true},
{group: "OVERALL", title: "bytes", width: 9, right: true},
{group: "OVERALL", title: "metres", width: 6, right: true},
{group: "OVERALL", title: "corrected", width: 9, right: true},
{group: "OVERALL", title: "lost", width: 9, right: true},
{group: "OVERALL", title: "corrupt", width: 9, right: true},
{group: "OVERALL", title: "link", width: 9, right: true},
@@ -271,7 +270,6 @@ type view struct {
rxFrames, rxBytes uint64
since errs
window errs
cable cableView
}
func errsBetween(b, n counterSet) errs {
@@ -297,7 +295,6 @@ func (d *direction) counters(now counterSet) view {
return view{
rxFrames: now.s.rxFrames - d.base.s.rxFrames,
rxBytes: now.s.rxBytes - d.base.s.rxBytes,
cable: d.cable.view(),
since: errsBetween(d.base, now),
}
}
@@ -329,7 +326,7 @@ func (d *direction) displayView() view {
n := d.win.count()
if n == 0 {
d.mu.Unlock()
return view{cable: d.cable.view()}
return view{}
}
v := d.counters(d.win.at(n - 1))
if n >= 2 {
@@ -344,19 +341,22 @@ func (d *direction) displayView() view {
// The same figures the panel draws, in the same order: the last second as
// rates and error flags with the noise cable riding at the end of them, then
// everything since the reset.
func totalRow(elapsed time.Duration, v view, target float64, length string, noiseMissing uint64) []string {
func totalRow(elapsed time.Duration, v view, target float64, phy phyDisplay, noiseMissing uint64) []string {
return []string{
rateCell(v.rxGbps*1e9, target*1e9),
scaleSI(v.rxPPS),
snrCell(phy),
flagCell(v.window.lost),
flagCell(v.window.corrupt),
flagCell(v.window.link),
flagCell(v.window.internal),
correctedFlag(phy.recent),
flagCell(noiseMissing),
scaleTime(elapsed),
scaleCount(v.rxFrames),
scaleCount(v.rxBytes),
length,
phy.metres,
correctedCell(phy.corrected),
statusCell(v.since.lost),
statusCell(v.since.corrupt),
statusCell(v.since.link),
@@ -382,7 +382,6 @@ func buildDirection(label string, tx, rx endpoint) (*direction, error) {
d := &direction{
txStats: txs,
streams: newLossWindows(txs),
cable: newCableStats(),
}
// 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.
@@ -412,8 +411,8 @@ func buildDirection(label string, tx, rx endpoint) (*direction, error) {
if err != nil {
return nil, fmt.Errorf("%s rx socket for 0x%04x: %w", label, et, err)
}
// The mac already stamps every frame for the probe's sake, so this only
// asks for the stamp to be delivered.
// The mac already stamps every frame; this only asks for the stamp to be
// delivered.
if err := enableRxTimestamps(fd); err != nil {
return nil, fmt.Errorf("%s rx timestamps for 0x%04x: %w", label, et, err)
}
@@ -421,26 +420,6 @@ func buildDirection(label string, tx, rx endpoint) (*direction, error) {
d.rxStats = append(d.rxStats, &rxStats{})
}
// 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, 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, 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
}
@@ -479,22 +458,6 @@ func (d *direction) start(wg *sync.WaitGroup, done *atomic.Bool, rxReady *sync.W
}()
}
sender := &probeSender{fd: d.probeTxFD, spec: d.probeSpec, stats: d.cable}
wg.Add(1)
go func() {
defer wg.Done()
defer holdPanic()
sender.run(done, startTx)
}()
receiver := &probeReceiver{fd: d.probeRxFD, stats: d.cable, ready: rxReady}
wg.Add(1)
go func() {
defer wg.Done()
defer holdPanic()
receiver.run(done)
}()
wg.Add(1)
go func() {
defer wg.Done()
@@ -510,8 +473,6 @@ func (d *direction) close() {
for _, fd := range d.rxFDs {
unix.Close(fd)
}
unix.Close(d.probeTxFD)
unix.Close(d.probeRxFD)
unix.Close(d.statFD)
}
@@ -519,8 +480,6 @@ const (
numStreams = 7
batchSize = 64
probeEther uint16 = etherBase + numStreams
testDriver = "ice"
// A constant rather than the negotiated speed, since this has to come up
@@ -538,10 +497,9 @@ func main() {
// which ethN shifts with every driver built into the kernel.
aName := flag.String("a", "", "first interface (default: the ice pair)")
bName := flag.String("b", "", "second interface")
nsPerM := flag.Float64("ns-per-m", 4.85, "mean of both directions, per metre of cable")
flag.Parse()
if err := run(*aName, *bName, *nsPerM); err != nil {
if err := run(*aName, *bName); err != nil {
fatal(err)
}
// A clean return is ctrl-alt-delete, which the kernel hands PID 1 as a
@@ -584,7 +542,7 @@ func (s *sampler) run(done *atomic.Bool, startTx <-chan struct{}) {
}
}
func run(aName, bName string, nsPerM float64) (err error) {
func run(aName, bName string) (err error) {
defer func() {
if p := recover(); p != nil {
if os.Getpid() != 1 {
@@ -640,6 +598,12 @@ func run(aName, bName string, nsPerM float64) (err error) {
return err
}
modules, cable, moduleChecks := moduleBringup([2]string{a.name, b.name})
if err := reportChecks("MODULES", moduleChecks); err != nil {
return err
}
diag := newCableDiag(modules, cable)
var dirs []*direction
for _, p := range [][2]endpoint{{a, b}, {b, a}} {
d, err := buildDirection(p[0].name+"->"+p[1].name, p[0], p[1])
@@ -673,7 +637,7 @@ func run(aName, bName string, nsPerM float64) (err error) {
var rxReady sync.WaitGroup
startTx := make(chan struct{})
for _, d := range dirs {
rxReady.Add(len(d.rxFDs) + 1)
rxReady.Add(len(d.rxFDs))
}
for _, d := range dirs {
d.start(&wg, &done, &rxReady, startTx)
@@ -693,6 +657,16 @@ func run(aName, bName string, nsPerM float64) (err error) {
defer holdPanic()
noise.run(&done)
}()
// Also ungated: SNR and temperature ride the module's own management bus,
// not the wire being measured.
for _, m := range modules {
wg.Add(1)
go func() {
defer wg.Done()
defer holdPanic()
m.run(&done)
}()
}
// Every return from here on stops the workers before the deferred closes
// pull their sockets out from under them: otherwise the sampler panics on a
// closed fd and can mask the error that actually ended the run. An error
@@ -754,41 +728,40 @@ func run(aName, bName string, nsPerM float64) (err error) {
return fmt.Errorf("%v", p)
case <-sig:
return nil
// A reset re-measures the cable first — the cable under a reset is
// usually a new one — and the counters re-baseline when the diag's own
// link blip is over, so it is never charged to the fresh run.
case <-space:
start = resetAll(dirs, stats)
if diag.kick(&done) {
fmt.Println(stats.rule("measuring cable"))
}
case <-diag.completed:
start = resetAll(dirs, modules, stats)
case <-disp.fb.flips:
now := time.Now()
px, py, down := touch.get()
x, y := disp.fb.fromPanel(px, py)
if disp.holdReset(x, y, down, now) {
start = resetAll(dirs, stats)
diag.kick(&done)
}
disp.showVersion = down && disp.versionSpot.contains(x, y)
for i, d := range dirs {
views[i] = d.displayView()
}
// Empty until the probe has a stamp from each direction, so the
// panel shows nothing there rather than a placeholder.
cable := ""
if m, ok := cableMetres(views, nsPerM); ok {
cable = fmt.Sprintf("%.1f", m)
}
if err := disp.render(totalView(views), now.Sub(start), cable,
info, measuring := diag.snapshot()
phy := phyDisplayFrom(info, measuring, modules[0].view(), modules[1].view())
if err := disp.render(totalView(views), now.Sub(start), phy,
noise.missing()); err != nil {
return err
}
case now := <-tick.C:
elapsed := now.Sub(start)
// Length needs both directions, so every row is sampled before any of
// them is printed.
for i, d := range dirs {
rows[i] = d.displayView()
}
length := "-"
if m, ok := cableMetres(rows, nsPerM); ok {
length = fmt.Sprintf("%.1f", m)
}
for _, line := range stats.emit(totalRow(elapsed, totalView(rows), target, length,
info, measuring := diag.snapshot()
phy := phyDisplayFrom(info, measuring, modules[0].view(), modules[1].view())
for _, line := range stats.emit(totalRow(elapsed, totalView(rows), target, phy,
noise.missing())) {
fmt.Println(line)
}