Compare commits
2
Commits
5eea6856fb
...
21b20d0071
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21b20d0071 | ||
|
|
49a1a2f829 |
+75
-43
@@ -11,22 +11,39 @@ import (
|
|||||||
// Split by direction of travel so each counter belongs to exactly one
|
// Split by direction of travel so each counter belongs to exactly one
|
||||||
// direction: the transmitting interface owns the tx fields and the receiving 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.
|
// the rx fields. Reading both sets off both interfaces double counts every drop.
|
||||||
var (
|
//
|
||||||
nicTxFields = []string{
|
// The two sides come from different places, and deliberately not from both at
|
||||||
"tx_errors", "tx_dropped", "tx_fifo_errors",
|
// once. dev_get_stats adds the core's own drop counts on top of whatever the
|
||||||
"tx_carrier_errors", "tx_aborted_errors", "tx_window_errors",
|
// driver reports, so a sysfs field is the driver's counter plus the stack's and
|
||||||
"collisions",
|
// overlaps any driver counter read beside it.
|
||||||
}
|
//
|
||||||
nicRxFields = []string{
|
// Sending is the stack's story: what it refused, and what the qdisc discarded
|
||||||
"rx_errors", "rx_dropped", "rx_crc_errors", "rx_missed_errors",
|
// because we outran it, neither of which the driver ever sees.
|
||||||
"rx_length_errors", "rx_over_errors", "rx_frame_errors", "rx_fifo_errors",
|
var nicTxFields = []string{
|
||||||
}
|
"tx_errors", "tx_dropped", "tx_fifo_errors",
|
||||||
)
|
"tx_carrier_errors", "tx_aborted_errors", "tx_window_errors",
|
||||||
|
"collisions",
|
||||||
|
}
|
||||||
|
|
||||||
type nicCounters struct {
|
// Receiving is the hardware's, and is taken from the driver's own array, which
|
||||||
tx uint64
|
// sysfs both flattens and overlaps: ice folds crc errors and jabbers into
|
||||||
rx uint64
|
// rx_errors, so the old sum of rx_errors alongside rx_crc_errors charged every
|
||||||
carrierDown uint64
|
// bad frame check twice. Named individually here, so nothing contains anything
|
||||||
|
// else in the list.
|
||||||
|
var nicRxStats = []string{
|
||||||
|
"rx_crc_errors.nic",
|
||||||
|
"rx_jabber.nic",
|
||||||
|
"rx_undersize.nic",
|
||||||
|
"rx_oversize.nic",
|
||||||
|
"rx_fragments.nic",
|
||||||
|
"rx_dropped.nic",
|
||||||
|
// Below the frame: a 64b/66b block that decoded to no legal symbol, and the
|
||||||
|
// fault ordered sets the pcs sends when it loses sync. A cable going
|
||||||
|
// marginal moves these while every frame still arrives intact, which is as
|
||||||
|
// close to a bit error rate as this link will report.
|
||||||
|
"illegal_bytes.nic",
|
||||||
|
"mac_local_faults.nic",
|
||||||
|
"mac_remote_faults.nic",
|
||||||
}
|
}
|
||||||
|
|
||||||
func readUint(path string) (uint64, bool) {
|
func readUint(path string) (uint64, bool) {
|
||||||
@@ -41,49 +58,64 @@ func readUint(path string) (uint64, bool) {
|
|||||||
return v, true
|
return v, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
func sumFields(base string, fields []string) uint64 {
|
func sumFields(base string, fields []string) uint64 {
|
||||||
var total uint64
|
var total uint64
|
||||||
for _, f := range fields {
|
for _, f := range fields {
|
||||||
if v, ok := readUint(base + "/statistics/" + f); ok {
|
total += mustReadUint(base + "/statistics/" + f)
|
||||||
total += v
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
func readNIC(ifname string) nicCounters {
|
// A poll is an ioctl and a handful of sysfs attributes, each costing
|
||||||
base := "/sys/class/net/" + ifname
|
// microseconds of open and read, so it runs here rather than on the goroutine
|
||||||
c := nicCounters{
|
// that has a frame to draw every sixteen milliseconds. Off that path the
|
||||||
tx: sumFields(base, nicTxFields),
|
// interval can be short enough that an unplugged cable shows up as a link error
|
||||||
rx: sumFields(base, nicRxFields),
|
// while the hand is still on it.
|
||||||
}
|
|
||||||
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
|
const nicInterval = 200 * time.Millisecond
|
||||||
|
|
||||||
type nicPoller struct {
|
type nicPoller struct {
|
||||||
txName, rxName string
|
txBase string
|
||||||
total *atomic.Uint64
|
rx *statReader
|
||||||
raw uint64
|
total *atomic.Uint64
|
||||||
|
raw uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newNICPoller(fd int, txName, rxName string, total *atomic.Uint64) (*nicPoller, error) {
|
||||||
|
rx, err := newStatReader(fd, rxName, nicRxStats)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &nicPoller{
|
||||||
|
txBase: "/sys/class/net/" + txName,
|
||||||
|
rx: rx,
|
||||||
|
total: total,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
func (p *nicPoller) read() uint64 {
|
func (p *nicPoller) read() uint64 {
|
||||||
tx := readNIC(p.txName)
|
return sumFields(p.txBase, nicTxFields) + p.rx.sum() +
|
||||||
rx := readNIC(p.rxName)
|
mustReadUint(p.txBase+"/carrier_down_count")
|
||||||
return tx.tx + rx.rx + tx.carrierDown
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sysfs nic counters run from boot and restart from zero whenever the driver
|
// Nic counters run from boot and restart from zero whenever the driver resets
|
||||||
// resets its statistics, so only their forward motion is accumulated. Taking
|
// its statistics, so only their forward motion is accumulated. Taking raw
|
||||||
// raw differences instead charges a boot's worth of errors to the first sample
|
// differences instead charges a boot's worth of errors to the first sample and
|
||||||
// and turns a reset into a near-2^64 underflow.
|
// turns a reset into a near-2^64 underflow.
|
||||||
func (p *nicPoller) poll() {
|
func (p *nicPoller) poll() {
|
||||||
raw := p.read()
|
raw := p.read()
|
||||||
if raw > p.raw {
|
if raw > p.raw {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ type direction struct {
|
|||||||
probeSpec *frameSpec
|
probeSpec *frameSpec
|
||||||
probeTxFD int
|
probeTxFD int
|
||||||
probeRxFD int
|
probeRxFD int
|
||||||
|
statFD int
|
||||||
cable *cableStats
|
cable *cableStats
|
||||||
|
|
||||||
// Guards everything the sampler touches. The counters are read on their own
|
// Guards everything the sampler touches. The counters are read on their own
|
||||||
@@ -456,7 +457,18 @@ func buildDirection(label string, tx, rx endpoint, sizes []int, cfg config) (*di
|
|||||||
streams: newLossWindows(cfg.streams),
|
streams: newLossWindows(cfg.streams),
|
||||||
cable: newCableStats(),
|
cable: newCableStats(),
|
||||||
}
|
}
|
||||||
d.poller = &nicPoller{txName: tx.name, rxName: rx.name, total: &d.nic}
|
// 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.
|
||||||
|
statFD, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s stats socket: %w", label, err)
|
||||||
|
}
|
||||||
|
d.statFD = statFD
|
||||||
|
|
||||||
|
d.poller, err = newNICPoller(statFD, tx.name, rx.name, &d.nic)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %w", label, err)
|
||||||
|
}
|
||||||
d.win = newRateWindow(int(rateWindowSpan/sampleInterval) + 1)
|
d.win = newRateWindow(int(rateWindowSpan/sampleInterval) + 1)
|
||||||
|
|
||||||
for i := 0; i < cfg.streams; i++ {
|
for i := 0; i < cfg.streams; i++ {
|
||||||
@@ -563,6 +575,7 @@ func (d *direction) close() {
|
|||||||
}
|
}
|
||||||
unix.Close(d.probeTxFD)
|
unix.Close(d.probeTxFD)
|
||||||
unix.Close(d.probeRxFD)
|
unix.Close(d.probeRxFD)
|
||||||
|
unix.Close(d.statFD)
|
||||||
}
|
}
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -105,6 +106,16 @@ func scaleSI(v float64) string {
|
|||||||
return fmt.Sprintf("%.2f P", v)
|
return fmt.Sprintf("%.2f P", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The integer counterpart, for whole things counted rather than a rate
|
||||||
|
// measured. Below a thousand the figure is the count itself, since two decimals
|
||||||
|
// on a quantity that cannot have them read as precision that is not there.
|
||||||
|
func scaleCount(v uint64) string {
|
||||||
|
if v < 1000 {
|
||||||
|
return strconv.FormatUint(v, 10)
|
||||||
|
}
|
||||||
|
return scaleSI(float64(v))
|
||||||
|
}
|
||||||
|
|
||||||
// The same shape for time, whose magnitudes are sixties and twenty-fours.
|
// The same shape for time, whose magnitudes are sixties and twenty-fours.
|
||||||
func scaleTime(d time.Duration) string {
|
func scaleTime(d time.Duration) string {
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -327,6 +328,135 @@ func getCoalesce(fd int, ifname string) (ethtoolCoalesce, error) {
|
|||||||
return ec, err
|
return ec, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ethSSStats = 1
|
||||||
|
ethGstringLen = 32
|
||||||
|
)
|
||||||
|
|
||||||
|
// Each of these has a __u32 or __u8 tail the kernel fills past the end of the
|
||||||
|
// struct, so they are only ever the header on a larger buffer.
|
||||||
|
type ethtoolSsetInfo struct {
|
||||||
|
cmd uint32
|
||||||
|
reserved uint32
|
||||||
|
ssetMask uint64
|
||||||
|
data [1]uint32
|
||||||
|
_ [4]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type ethtoolGstrings struct {
|
||||||
|
cmd uint32
|
||||||
|
stringSet uint32
|
||||||
|
len uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type ethtoolStatsHdr struct {
|
||||||
|
cmd uint32
|
||||||
|
nStats uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// The stats payload is an array of __u64 immediately after the header, so the
|
||||||
|
// buffer is allocated as uint64 to guarantee it lands on an eight byte
|
||||||
|
// boundary. A []byte carries no such guarantee.
|
||||||
|
func statsBuf(n int) []uint64 { return make([]uint64, n) }
|
||||||
|
|
||||||
|
func statCount(fd int, ifname string) (uint32, error) {
|
||||||
|
req := ethtoolSsetInfo{cmd: unix.ETHTOOL_GSSET_INFO, ssetMask: 1 << ethSSStats}
|
||||||
|
if err := ethtoolCall(fd, ifname, unsafe.Pointer(&req)); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
// A cleared mask bit means the driver does not have that string set at all,
|
||||||
|
// in which case no count was written into the tail.
|
||||||
|
if req.ssetMask == 0 {
|
||||||
|
return 0, fmt.Errorf("driver has no ETH_SS_STATS string set")
|
||||||
|
}
|
||||||
|
return req.data[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func statNames(fd int, ifname string) ([]string, error) {
|
||||||
|
n, err := statCount(fd, ifname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
return nil, fmt.Errorf("driver reports zero statistics")
|
||||||
|
}
|
||||||
|
|
||||||
|
hdrLen := int(unsafe.Sizeof(ethtoolGstrings{}))
|
||||||
|
buf := statsBuf((hdrLen + int(n)*ethGstringLen + 7) / 8)
|
||||||
|
hdr := (*ethtoolGstrings)(unsafe.Pointer(&buf[0]))
|
||||||
|
hdr.cmd = unix.ETHTOOL_GSTRINGS
|
||||||
|
hdr.stringSet = ethSSStats
|
||||||
|
hdr.len = n
|
||||||
|
if err := ethtoolCall(fd, ifname, unsafe.Pointer(&buf[0])); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := unsafe.Slice((*byte)(unsafe.Pointer(&buf[0])), len(buf)*8)[hdrLen:]
|
||||||
|
names := make([]string, hdr.len)
|
||||||
|
for i := range names {
|
||||||
|
s := raw[i*ethGstringLen : (i+1)*ethGstringLen]
|
||||||
|
if k := bytes.IndexByte(s, 0); k >= 0 {
|
||||||
|
s = s[:k]
|
||||||
|
}
|
||||||
|
names[i] = string(s)
|
||||||
|
}
|
||||||
|
return names, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func statValues(fd int, ifname string, n uint32, into []uint64) error {
|
||||||
|
hdr := (*ethtoolStatsHdr)(unsafe.Pointer(&into[0]))
|
||||||
|
hdr.cmd = unix.ETHTOOL_GSTATS
|
||||||
|
hdr.nStats = n
|
||||||
|
return ethtoolCall(fd, ifname, unsafe.Pointer(&into[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ioctl indexes counters by position, and the ordering is a property of the
|
||||||
|
// driver build, so names are resolved to indices once rather than per poll.
|
||||||
|
type statReader struct {
|
||||||
|
fd int
|
||||||
|
ifname string
|
||||||
|
n uint32
|
||||||
|
want []int
|
||||||
|
buf []uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStatReader(fd int, ifname string, want []string) (*statReader, error) {
|
||||||
|
names, err := statNames(fd, ifname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s statistics: %w", ifname, err)
|
||||||
|
}
|
||||||
|
idx := make(map[string]int, len(names))
|
||||||
|
for i, s := range names {
|
||||||
|
idx[s] = i
|
||||||
|
}
|
||||||
|
r := &statReader{
|
||||||
|
fd: fd,
|
||||||
|
ifname: ifname,
|
||||||
|
n: uint32(len(names)),
|
||||||
|
buf: statsBuf(1 + len(names)),
|
||||||
|
}
|
||||||
|
for _, w := range want {
|
||||||
|
i, ok := idx[w]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%s has no statistic %q", ifname, w)
|
||||||
|
}
|
||||||
|
r.want = append(r.want, i)
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *statReader) sum() uint64 {
|
||||||
|
if err := statValues(r.fd, r.ifname, r.n, r.buf); err != nil {
|
||||||
|
panic(fmt.Sprintf("reading %s statistics: %v", r.ifname, err))
|
||||||
|
}
|
||||||
|
vals := r.buf[1:]
|
||||||
|
var total uint64
|
||||||
|
for _, i := range r.want {
|
||||||
|
total += vals[i]
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
func checkGovernor(want string) checkResult {
|
func checkGovernor(want string) checkResult {
|
||||||
res := checkResult{item: "cpu governor"}
|
res := checkResult{item: "cpu governor"}
|
||||||
paths, err := filepath.Glob("/sys/devices/system/cpu/cpu*/cpufreq/scaling_governor")
|
paths, err := filepath.Glob("/sys/devices/system/cpu/cpu*/cpufreq/scaling_governor")
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ func (d *display) errCounts(x, w, y int, e errs) int {
|
|||||||
n := r.get(e)
|
n := r.get(e)
|
||||||
c := errColor(n)
|
c := errColor(n)
|
||||||
cx, cw, cy := d.chipAt(i, x, w, y, d.countChipH(), c)
|
cx, cw, cy := d.chipAt(i, x, w, y, d.countChipH(), c)
|
||||||
ty := d.centerIn(d.gridB, cx, cw, cy+chipPadY, commas(n), c)
|
ty := d.centerIn(d.gridB, cx, cw, cy+chipPadY, scaleCount(n), c)
|
||||||
d.centerIn(d.grid, cx, cw, ty, r.label, c)
|
d.centerIn(d.grid, cx, cw, ty, r.label, c)
|
||||||
}
|
}
|
||||||
return y + d.countsH()
|
return y + d.countsH()
|
||||||
@@ -343,8 +343,8 @@ func (d *display) render(v view, elapsed time.Duration, cable string) error {
|
|||||||
x, w = d.panel(d.sincePanel, v.since)
|
x, w = d.panel(d.sincePanel, v.since)
|
||||||
d.stats(d.gridB, x, w, d.sinceYs[0], []statCell{
|
d.stats(d.gridB, x, w, d.sinceYs[0], []statCell{
|
||||||
{scaleTime(elapsed), "elapsed", uiFg},
|
{scaleTime(elapsed), "elapsed", uiFg},
|
||||||
{scaleSI(float64(v.rxFrames)), "packets", uiFg},
|
{scaleCount(v.rxFrames), "packets", uiFg},
|
||||||
{scaleSI(float64(v.rxGot)), "bytes", uiFg},
|
{scaleCount(v.rxGot), "bytes", uiFg},
|
||||||
{cable, "m", uiFg},
|
{cable, "m", uiFg},
|
||||||
})
|
})
|
||||||
d.errCounts(x, w, d.sinceYs[1], v.since)
|
d.errCounts(x, w, d.sinceYs[1], v.since)
|
||||||
|
|||||||
Reference in New Issue
Block a user