package main import ( "flag" "fmt" "net" "os" "os/signal" "sync" "sync/atomic" "syscall" "time" "golang.org/x/sys/unix" ) type endpoint struct { name string tag string idx int mac [6]byte mtu int } 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]) } 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) return endpoint{name: name, idx: ifi.Index, mac: mac, mtu: ifi.MTU}, nil } // Returns the new start time, so the uptime shown alongside the totals counts // from the reset rather than from launch; the elapsed clock restarting is the // visible mark of the re-baseline. func resetAll(dirs []*direction, mods []*phyModule) time.Time { for _, d := range dirs { d.reset() } for _, m := range mods { m.reset() } return time.Now() } // Nothing to hold or revert here: failures during a measure were never // counted, so the view is always the counters as they stand. func phyView(diag *cableDiag, modules []*phyModule) (phyDisplay, bool) { info, measuring := diag.snapshot() return phyDisplayFrom(info, measuring, modules[0].view(), modules[1].view()), measuring } 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: "cable", 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}, {group: "OVERALL", title: "internal", width: 9, right: true}, } // 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, phy phyDisplay, nv noiseView) []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), noiseCell(nv), scaleTime(elapsed), scaleCount(v.rxFrames), scaleCount(v.rxBytes), phy.metres, correctedCell(phy.corrected), statusCell(v.since.lost), statusCell(v.since.corrupt), statusCell(v.since.link), statusCell(v.since.internal), } } const ( numStreams = 7 batchSize = 64 testDriver = "ixgbe" // A constant rather than the negotiated speed, since this has to come up // with no cable in the port and nothing to negotiate. linkSpeed = 10.0 ) // The mac appends the fcs, so 60 and 1514 here are the smallest and largest // standard frames, 64 and 1518 on the wire; 9014 fills the 9000 MTU. var frameSizes = []int{60, 128, 256, 512, 1024, 1280, 1514, 9014} func main() { // Left empty, the test pair is found by driver name instead: as PID 1 there // is no udev to pin names and no command line to pass, and which port gets // which ethN shifts with every driver built into the kernel. aName := flag.String("a", "", "first interface (default: the ixgbe pair)") bName := flag.String("b", "", "second interface") flag.Parse() if err := run(*aName, *bName); err != nil { fatal(err) } // A clean return is ctrl-alt-delete, which the kernel hands PID 1 as a // SIGINT. Exiting on it would panic the kernel over the reboot it was asking // for, so init asks for the reboot by name. if os.Getpid() == 1 { if err := unix.Reboot(unix.LINUX_REBOOT_CMD_RESTART); err != nil { panic(err) } } } const ( reportInterval = time.Second // Deliberately not tied to the refresh: letting a slow or blocked draw set // the sampling clock would stretch the window it reports. sampleInterval = 16 * time.Millisecond // How far back the shown errors reach. The rate is not taken from this ring // but from the smeared receive buckets, which are keyed by the read clock. rateWindowSpan = time.Second ) // One sampler for both directions, so they are read back to back on one clock // rather than drifting apart on two. 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() { <-tick.C for _, d := range s.dirs { d.sample() } } } func run(aName, bName string) (err error) { defer func() { if p := recover(); p != nil { if os.Getpid() != 1 { panic(p) } err = fmt.Errorf("%v", p) } }() if err := reportChecks("BOOT", bootstrap()); err != nil { return err } if aName == "" || bName == "" { var err error aName, bName, err = driverPair(testDriver) if err != nil { return err } } a, err := lookupEndpoint(aName) if err != nil { return err } b, err := lookupEndpoint(bName) if err != nil { return err } noise, err := newNoiser() if err != nil { return err } defer noise.close() a.tag, b.tag = "TEST A", "TEST B" noise.eps[0].tag, noise.eps[1].tag = "NOISE A", "NOISE B" ifnames := []string{a.name, b.name} ethertypes := make([]uint16, numStreams) for i := range ethertypes { ethertypes[i] = uint16(etherBase + i) } modules, moduleIDs, err := openModules([2]string{a.name, b.name}) if err != nil { return err } checks := configureSystem(ifnames, ethertypes) checks = append(checks, moduleChecks(modules, [2]string{a.name, b.name})...) checks = append(checks, configureNoise(noise.names())...) if err := reportChecks("SETTINGS", checks); err != nil { return err } // The MTU check may have just raised them, so both are re-read before the // frame sizes are judged. for _, e := range []*endpoint{&a, &b} { fresh, err := lookupEndpoint(e.name) if err != nil { return err } e.mtu = fresh.mtu for _, s := range frameSizes { 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) } } } diag := newCableDiag(modules, cableInfo{}) 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], &diag.measuring) if err != nil { return err } dirs = append(dirs, d) } defer func() { for _, d := range dirs { d.close() } }() var linkRows [][]string for i, e := range []endpoint{a, b, noise.eps[0], noise.eps[1]} { mod := "" if i < len(moduleIDs) { mod = moduleIDs[i] } linkRows = append(linkRows, []string{ paint(e.tag, cCyan), e.name, e.macString(), fmt.Sprintf("%d", e.mtu), mod, }) } fmt.Println(renderBox("LINKS", []string{"TAG", "INTERFACE", "MAC", "MTU", "MODULE"}, []bool{false, false, false, true, false}, linkRows)) fmt.Println() // One row carries both directions, so line rate is both links at once. target := linkSpeed * float64(len(dirs)) var done 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, &done, &rxReady, startTx) } samp := &sampler{dirs: dirs} wg.Add(1) go func() { defer wg.Done() defer holdPanic() samp.run(&done, startTx) }() // Not gated on startTx: the cycle and the connected verdict are wanted the // moment the panel is, and nothing it does touches the measurement. wg.Add(1) go func() { defer wg.Done() defer holdPanic() noise.run(&done) }() 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 // before the gate opens closes it here, or the wait would hang on // goroutines still parked at startTx. defer func() { done.Store(true) select { case <-startTx: default: close(startTx) } wg.Wait() }() // A worker that panics before signalling ready would hang a bare Wait. ready := make(chan struct{}) go func() { rxReady.Wait() close(ready) }() select { case <-ready: case p := <-fatalCh: return fmt.Errorf("%v", p) } sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) space, restoreTerm := watchSpace() defer restoreTerm() disp, err := newDisplay() if err != nil { return fmt.Errorf("display: %w", err) } defer disp.close() touch, err := watchTouch(disp.fb.pw, disp.fb.ph) if err != nil { return fmt.Errorf("touchscreen: %w", err) } for _, d := range dirs { d.primeCounters() } start := time.Now() // Anchored to the same clock as the noise cycle, so slot boundaries and // phase transitions coincide. hist := newHistory(rateEpochStart) // A reset resets at the press, then re-measures; the measure's failures // are suppressed at their sources while it runs, so there is nothing to // hide or revert afterwards. kickMeasure := func() { if diag.kick(&done) { start = resetAll(dirs, modules) hist.reset() } } close(startTx) // The first measure rides the same async path as a reset, so startup never // waits on it. kickMeasure() tick := time.NewTicker(reportInterval) defer tick.Stop() views := make([]view, len(dirs)) rows := make([]view, len(dirs)) stats := &streamTable{cols: intervalCols, headerEvery: 20} for { select { case p := <-fatalCh: return fmt.Errorf("%v", p) case <-sig: return nil // The verdict lives in the cable cell, not its own line. case <-space: kickMeasure() 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) { kickMeasure() } disp.showVersion = down && disp.versionSpot.contains(x, y) for i, d := range dirs { views[i] = d.displayView() } v := totalView(views) phy, measuring := phyView(diag, modules) if err := disp.render(v, now.Sub(start), phy, measuring, noise.view(), hist.view(), target); err != nil { return err } case now := <-tick.C: elapsed := now.Sub(start) for i, d := range dirs { rows[i] = d.displayView() } v := totalView(rows) phy, measuring := phyView(diag, modules) hist.sample(now, v, phy, noise.view(), measuring, target) for _, m := range modules { for _, n := range m.takeNotes() { fmt.Println(stats.rule(n)) } } for _, line := range stats.emit(totalRow(elapsed, v, target, phy, noise.view())) { fmt.Println(line) } } } }