2026-07-25 17:58:54 -07:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"os"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
2026-07-31 15:45:03 -07:00
|
|
|
"sync/atomic"
|
|
|
|
|
"time"
|
2026-07-25 17:58:54 -07:00
|
|
|
)
|
|
|
|
|
|
2026-07-25 23:03:31 -07:00
|
|
|
// 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.
|
2026-07-25 22:54:04 -07:00
|
|
|
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",
|
|
|
|
|
}
|
|
|
|
|
)
|
2026-07-25 17:58:54 -07:00
|
|
|
|
|
|
|
|
type nicCounters struct {
|
2026-07-25 22:54:04 -07:00
|
|
|
tx uint64
|
|
|
|
|
rx uint64
|
|
|
|
|
carrierDown uint64
|
2026-07-25 17:58:54 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 22:54:04 -07:00
|
|
|
func sumFields(base string, fields []string) uint64 {
|
|
|
|
|
var total uint64
|
|
|
|
|
for _, f := range fields {
|
2026-07-25 17:58:54 -07:00
|
|
|
if v, ok := readUint(base + "/statistics/" + f); ok {
|
2026-07-25 22:54:04 -07:00
|
|
|
total += v
|
2026-07-25 17:58:54 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-07-25 22:54:04 -07:00
|
|
|
return total
|
2026-07-25 17:58:54 -07:00
|
|
|
}
|
|
|
|
|
|
2026-07-25 22:54:04 -07:00
|
|
|
func readNIC(ifname string) nicCounters {
|
|
|
|
|
base := "/sys/class/net/" + ifname
|
|
|
|
|
c := nicCounters{
|
|
|
|
|
tx: sumFields(base, nicTxFields),
|
|
|
|
|
rx: sumFields(base, nicRxFields),
|
2026-07-25 17:58:54 -07:00
|
|
|
}
|
2026-07-25 22:54:04 -07:00
|
|
|
c.carrierDown, _ = readUint(base + "/carrier_down_count")
|
|
|
|
|
return c
|
2026-07-25 17:58:54 -07:00
|
|
|
}
|
2026-07-31 15:45:03 -07:00
|
|
|
|
|
|
|
|
// 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()
|
|
|
|
|
}
|
|
|
|
|
}
|