package main import ( "os" "strconv" "strings" "sync/atomic" "time" ) // Split by direction of travel so each counter belongs to exactly one // direction: the transmitting interface owns the tx fields and the receiving one // the rx fields. Reading both sets off both interfaces double counts every drop. var ( nicTxFields = []string{ "tx_errors", "tx_dropped", "tx_fifo_errors", "tx_carrier_errors", "tx_aborted_errors", "tx_window_errors", "collisions", } nicRxFields = []string{ "rx_errors", "rx_dropped", "rx_crc_errors", "rx_missed_errors", "rx_length_errors", "rx_over_errors", "rx_frame_errors", "rx_fifo_errors", } ) type nicCounters struct { tx uint64 rx uint64 carrierDown uint64 } func readUint(path string) (uint64, bool) { b, err := os.ReadFile(path) if err != nil { return 0, false } v, err := strconv.ParseUint(strings.TrimSpace(string(b)), 10, 64) if err != nil { return 0, false } return v, true } func sumFields(base string, fields []string) uint64 { var total uint64 for _, f := range fields { if v, ok := readUint(base + "/statistics/" + f); ok { total += v } } return total } func readNIC(ifname string) nicCounters { base := "/sys/class/net/" + ifname c := nicCounters{ tx: sumFields(base, nicTxFields), rx: sumFields(base, nicRxFields), } c.carrierDown, _ = readUint(base + "/carrier_down_count") return c } // A poll is thirty-odd sysfs attributes and each one costs microseconds of open // and read, so it runs here rather than on the goroutine that has a frame to // draw every sixteen milliseconds. Off that path the interval can be short // enough that an unplugged cable shows up as a link error while the hand is // still on it. const nicInterval = 200 * time.Millisecond type nicPoller struct { txName, rxName string total *atomic.Uint64 raw uint64 } func (p *nicPoller) read() uint64 { tx := readNIC(p.txName) rx := readNIC(p.rxName) return tx.tx + rx.rx + tx.carrierDown } // Sysfs nic counters run from boot and restart from zero whenever the driver // resets its statistics, so only their forward motion is accumulated. Taking // raw differences instead charges a boot's worth of errors to the first sample // and turns a reset into a near-2^64 underflow. func (p *nicPoller) poll() { raw := p.read() if raw > p.raw { p.total.Add(raw - p.raw) } p.raw = raw } // Whatever the interfaces counted before now is not ours, so the first poll has // something to measure forward motion from and adds nothing. func (p *nicPoller) prime() { p.raw = p.read() } // Held until the test starts, since polling before the baseline is primed would // charge everything the interfaces counted since boot to the first sample. func (p *nicPoller) run(done *atomic.Bool, startTx <-chan struct{}) { <-startTx tick := time.NewTicker(nicInterval) defer tick.Stop() for !done.Load() { <-tick.C p.poll() } }