2026-07-25 17:58:54 -07:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"os"
|
2026-08-13 07:10:39 -07:00
|
|
|
"path/filepath"
|
2026-07-25 17:58:54 -07:00
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
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-08-04 11:27:30 -07:00
|
|
|
//
|
|
|
|
|
// The two sides come from different places, and deliberately not from both at
|
|
|
|
|
// once. dev_get_stats adds the core's own drop counts on top of whatever the
|
|
|
|
|
// driver reports, so a sysfs field is the driver's counter plus the stack's and
|
|
|
|
|
// overlaps any driver counter read beside it.
|
|
|
|
|
//
|
|
|
|
|
// Sending is the stack's story: what it refused, and what the qdisc discarded
|
|
|
|
|
// because we outran it, neither of which the driver ever sees.
|
|
|
|
|
var nicTxFields = []string{
|
|
|
|
|
"tx_errors", "tx_dropped", "tx_fifo_errors",
|
|
|
|
|
"tx_carrier_errors", "tx_aborted_errors", "tx_window_errors",
|
|
|
|
|
"collisions",
|
|
|
|
|
}
|
2026-07-25 17:58:54 -07:00
|
|
|
|
2026-08-17 12:45:41 -07:00
|
|
|
// Receiving is the hardware's, and is taken from the driver's own array:
|
|
|
|
|
// sysfs flattens and overlaps it (crc errors fold into rx_errors, so summing
|
|
|
|
|
// rx_errors alongside rx_crc_errors charges every bad frame twice). Named
|
|
|
|
|
// individually, so nothing contains anything else in the list; the 82599
|
|
|
|
|
// exposes no jabber, fragment, illegal-byte or fault counters (docs/nics/x520/).
|
|
|
|
|
var nicRxFields = []string{
|
|
|
|
|
"rx_crc_errors",
|
|
|
|
|
"rx_missed_errors",
|
|
|
|
|
"rx_length_errors",
|
|
|
|
|
"rx_long_length_errors",
|
|
|
|
|
"rx_short_length_errors",
|
2026-08-13 07:10:39 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ifDriver(name string) (string, error) {
|
|
|
|
|
link, err := os.Readlink("/sys/class/net/" + name + "/device/driver")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return filepath.Base(link), nil
|
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-08-04 11:27:30 -07:00
|
|
|
// Every counter here was read once before the test began, so one that stops
|
|
|
|
|
// reading now is the interface going away underneath us. Stopping is the only
|
|
|
|
|
// honest answer: a tester that quietly gives up counting errors goes on
|
|
|
|
|
// reporting a clean link, which is the one lie it must never tell.
|
|
|
|
|
func mustReadUint(path string) uint64 {
|
|
|
|
|
v, ok := readUint(path)
|
|
|
|
|
if !ok {
|
|
|
|
|
panic("reading " + path)
|
|
|
|
|
}
|
|
|
|
|
return v
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 22:54:04 -07:00
|
|
|
func sumFields(base string, fields []string) uint64 {
|
|
|
|
|
var total uint64
|
|
|
|
|
for _, f := range fields {
|
2026-08-04 11:27:30 -07:00
|
|
|
total += mustReadUint(base + "/statistics/" + f)
|
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-08-31 09:35:32 -07:00
|
|
|
// A read is an ioctl and a handful of sysfs attributes, each costing
|
|
|
|
|
// microseconds of open and read, so it rides the sampler's goroutine and
|
|
|
|
|
// clock rather than the one that has a frame to draw every sixteen
|
|
|
|
|
// milliseconds; a link error lands on the panel as quickly as a lost frame.
|
|
|
|
|
type nicCounters struct {
|
|
|
|
|
txBase string
|
|
|
|
|
rx *statReader
|
|
|
|
|
raw uint64
|
2026-08-04 11:27:30 -07:00
|
|
|
}
|
|
|
|
|
|
2026-08-31 09:35:32 -07:00
|
|
|
func newNICCounters(fd int, txName, rxName string) (*nicCounters, error) {
|
2026-08-17 12:45:41 -07:00
|
|
|
rx, err := newStatReader(fd, rxName, nicRxFields)
|
2026-08-04 11:27:30 -07:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-08-31 09:35:32 -07:00
|
|
|
return &nicCounters{
|
|
|
|
|
txBase: "/sys/class/net/" + txName,
|
|
|
|
|
rx: rx,
|
2026-08-04 11:27:30 -07:00
|
|
|
}, nil
|
2026-07-31 15:45:03 -07:00
|
|
|
}
|
|
|
|
|
|
2026-08-04 11:27:30 -07:00
|
|
|
// Carrier is a property of the netdev rather than a counter of either side, and
|
|
|
|
|
// is taken from the transmitting interface only: across both directions each
|
|
|
|
|
// interface transmits exactly once, so every flap is counted once.
|
2026-08-31 09:35:32 -07:00
|
|
|
func (c *nicCounters) read() uint64 {
|
|
|
|
|
return sumFields(c.txBase, nicTxFields) + c.rx.sum() +
|
|
|
|
|
mustReadUint(c.txBase+"/carrier_down_count")
|
2026-07-31 15:45:03 -07:00
|
|
|
}
|
|
|
|
|
|
2026-08-04 11:27:30 -07:00
|
|
|
// Nic counters run from boot and restart from zero whenever the driver resets
|
2026-08-31 09:35:32 -07:00
|
|
|
// its statistics, so only forward motion is the delta: a raw difference would
|
|
|
|
|
// charge a boot's worth of errors to the first collection and turn a reset
|
|
|
|
|
// into a near-2^64 underflow.
|
|
|
|
|
func (c *nicCounters) delta() uint64 {
|
|
|
|
|
raw := c.read()
|
|
|
|
|
var d uint64
|
|
|
|
|
if raw > c.raw {
|
|
|
|
|
d = raw - c.raw
|
2026-07-31 15:45:03 -07:00
|
|
|
}
|
2026-08-31 09:35:32 -07:00
|
|
|
c.raw = raw
|
|
|
|
|
return d
|
2026-07-31 15:45:03 -07:00
|
|
|
}
|