Restructure stats into delta, mask, and aggregate layers

This commit is contained in:
flamingcow
2026-08-31 09:35:32 -07:00
parent ea272e489c
commit 06fa759b1a
11 changed files with 316 additions and 364 deletions
+25 -54
View File
@@ -5,8 +5,6 @@ import (
"path/filepath"
"strconv"
"strings"
"sync/atomic"
"time"
)
// Split by direction of travel so each counter belongs to exactly one
@@ -79,72 +77,45 @@ func sumFields(base string, fields []string) uint64 {
return total
}
// A poll is an ioctl and a handful of sysfs attributes, each costing
// 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 it can
// match the sampler's cadence, so a link error lands on the panel as quickly
// as a lost frame rather than stepping five times a second.
const nicInterval = sampleInterval
type nicPoller struct {
txBase string
rx *statReader
total *atomic.Uint64
measuring *atomic.Bool
raw uint64
// 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
}
func newNICPoller(fd int, txName, rxName string, total *atomic.Uint64, measuring *atomic.Bool) (*nicPoller, error) {
func newNICCounters(fd int, txName, rxName string) (*nicCounters, error) {
rx, err := newStatReader(fd, rxName, nicRxFields)
if err != nil {
return nil, err
}
return &nicPoller{
txBase: "/sys/class/net/" + txName,
rx: rx,
total: total,
measuring: measuring,
return &nicCounters{
txBase: "/sys/class/net/" + txName,
rx: rx,
}, 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 {
return sumFields(p.txBase, nicTxFields) + p.rx.sum() +
mustReadUint(p.txBase+"/carrier_down_count")
func (c *nicCounters) read() uint64 {
return sumFields(c.txBase, nicTxFields) + c.rx.sum() +
mustReadUint(c.txBase+"/carrier_down_count")
}
// 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. The hardware cannot pause its
// counters for a cable measure, so its blip is suppressed here at the read:
// the baseline tracks the raw values without accumulating until it is over.
func (p *nicPoller) poll() {
raw := p.read()
if raw > p.raw && !p.measuring.Load() {
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()
// 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
}
c.raw = raw
return d
}