Fix sff panic deadlock, share the evdev probe, assorted review cleanups

This commit is contained in:
flamingcow
2026-08-31 15:51:52 -07:00
parent 10d97ee82a
commit e4731c94cd
13 changed files with 205 additions and 272 deletions
+2 -2
View File
@@ -26,10 +26,10 @@ type direction struct {
// aggregate.
masked *atomic.Bool
// Guards everything the sampler touches. Collection consumes self-clearing
// sources, so a second concurrent reader would see a gap.
iface int
// Guards everything the sampler touches. Collection consumes self-clearing
// sources, so a second concurrent reader would see a gap.
mu sync.Mutex
prev sample
nic *nicCounters
+21 -3
View File
@@ -46,6 +46,7 @@ func main() {
}
restoreTerm := rawKeys()
go relayStdin()
var err error
if *fuzzN > 0 {
@@ -84,6 +85,26 @@ func rawKeys() func() {
return func() { unix.IoctlSetTermios(fd, unix.TCSETS, &saved) }
}
// The child's own process group makes the tty off limits to it (reading from
// the background raises SIGTTIN), so the harness reads and forwards to the
// run that currently owns the console. One reader for the life of the
// process: a copier per run would pile up under fuzz, each stealing
// keystrokes for a run already gone.
func relayStdin() {
buf := make([]byte, 64)
for {
n, err := os.Stdin.Read(buf)
if err != nil {
return
}
consoleMu.Lock()
if console != nil {
console.Write(buf[:n])
}
consoleMu.Unlock()
}
}
func fuzz(n int, runFor time.Duration, args []string) error {
failed := 0
for i := 1; i <= n; i++ {
@@ -162,13 +183,10 @@ func run(runFor time.Duration, at string, out io.Writer, args []string) error {
// killed when go run dies, and an orphan holding the wire poisons every
// measurement after it.
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
// The child's own process group makes the tty off limits to it (reading
// from the background raises SIGTTIN), so the harness reads and forwards.
stdin, err := cmd.StdinPipe()
if err != nil {
return err
}
go io.Copy(stdin, os.Stdin)
consoleMu.Lock()
console = stdin
consoleMu.Unlock()
+8 -2
View File
@@ -12,8 +12,10 @@ import (
"net/http"
"os"
"sync"
"time"
"g.fc.run/theater/cabletest/internal/drm"
"g.fc.run/theater/cabletest/internal/evdev"
)
const streamAddr = ":8080"
@@ -115,7 +117,7 @@ func serveStream() error {
if err != nil {
return err
}
if touchscreen, err = openTouchscreen(); err != nil {
if touchscreen, err = evdev.FindTouchscreen(os.O_RDWR); err != nil {
return err
}
ln, err := net.Listen("tcp", streamAddr)
@@ -158,7 +160,10 @@ func keys(w http.ResponseWriter, r *http.Request) {
// Between runs the crtc scans out the console framebuffer, which is not a
// dumb buffer and fails to map on every frame; each error reads once, not at
// frame rate.
// frame rate. The pause paces the retry, since a failure ahead of the vblank
// wait would otherwise spin hot.
const captureErrPause = 100 * time.Millisecond
func captureLoop(g *grabber) {
lastErr := ""
for {
@@ -168,6 +173,7 @@ func captureLoop(g *grabber) {
lastErr = err.Error()
fmt.Fprintf(os.Stderr, "capture: %v\n", err)
}
time.Sleep(captureErrPause)
continue
}
lastErr = ""
+16 -98
View File
@@ -2,115 +2,33 @@ package main
import (
"encoding/binary"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"unsafe"
"golang.org/x/sys/unix"
"g.fc.run/theater/cabletest/internal/evdev"
)
const (
evSyn = 0x00
evKey = 0x01
evAbs = 0x03
btnTouch = 0x14a
absX = 0x00
absY = 0x01
inputPropDirect = 0x01
sizeofInputEvent = 24
)
func evIoc(nr, size uintptr) uintptr {
const iocRead = 2
return iocRead<<30 | size<<16 | 'E'<<8 | nr
}
type touchDev struct {
f *os.File
minX, maxX, minY, maxY int32
}
var touchscreen *touchDev
// Picks the direct-input device that reports absolute X and Y — the same
// probe cabletest runs, so both ends name the same hardware. Events written
// to the node are fed into the input core as if the hardware produced them,
// so cabletest sees a finger without carrying any injection code.
func openTouchscreen() (*touchDev, error) {
paths, err := filepath.Glob("/dev/input/event*")
if err != nil {
return nil, err
}
for _, p := range paths {
f, err := os.OpenFile(p, os.O_RDWR, 0)
if err != nil {
continue
}
var props, absBits uint64
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
evIoc(0x09, 8), uintptr(unsafe.Pointer(&props))); errno != 0 {
f.Close()
continue
}
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
evIoc(0x20+evAbs, 8), uintptr(unsafe.Pointer(&absBits))); errno != 0 {
f.Close()
continue
}
if props&(1<<inputPropDirect) == 0 ||
absBits&(1<<absX) == 0 || absBits&(1<<absY) == 0 {
f.Close()
continue
}
t := &touchDev{f: f}
if t.minX, t.maxX, err = axisRange(f, absX); err != nil {
f.Close()
return nil, fmt.Errorf("%s x range: %w", p, err)
}
if t.minY, t.maxY, err = axisRange(f, absY); err != nil {
f.Close()
return nil, fmt.Errorf("%s y range: %w", p, err)
}
if t.maxX <= t.minX || t.maxY <= t.minY {
f.Close()
return nil, fmt.Errorf("%s: degenerate axes x %d..%d y %d..%d",
p, t.minX, t.maxX, t.minY, t.maxY)
}
return t, nil
}
return nil, fmt.Errorf("no touchscreen found among /dev/input/event*")
}
func axisRange(f *os.File, axis uintptr) (min, max int32, err error) {
var info [6]int32
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
evIoc(0x40+axis, 24), uintptr(unsafe.Pointer(&info[0]))); errno != 0 {
return 0, 0, errno
}
return info[1], info[2], nil
}
var touchscreen *evdev.Touchscreen
type inputEvent struct {
typ, code uint16
val int32
}
// Timestamps are left zero: evdev stamps injected events at delivery.
func (t *touchDev) send(evs []inputEvent) error {
buf := make([]byte, len(evs)*sizeofInputEvent)
// Events written to the node are fed into the input core as if the hardware
// produced them, so cabletest sees a finger without carrying any injection
// code. Timestamps are left zero: evdev stamps injected events at delivery.
func send(t *evdev.Touchscreen, evs []inputEvent) error {
buf := make([]byte, len(evs)*evdev.EventSize)
for i, e := range evs {
o := i * sizeofInputEvent
o := i * evdev.EventSize
binary.NativeEndian.PutUint16(buf[o+16:], e.typ)
binary.NativeEndian.PutUint16(buf[o+18:], e.code)
binary.NativeEndian.PutUint32(buf[o+20:], uint32(e.val))
}
_, err := t.f.Write(buf)
_, err := t.F.Write(buf)
return err
}
@@ -124,7 +42,7 @@ func touch(w http.ResponseWriter, r *http.Request) {
}
f := strings.Fields(string(body))
if len(f) == 1 && f[0] == "up" {
if err := touchscreen.send([]inputEvent{{evKey, btnTouch, 0}, {evSyn, 0, 0}}); err != nil {
if err := send(touchscreen, []inputEvent{{evdev.Key, evdev.BtnTouch, 0}, {evdev.Syn, 0, 0}}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
@@ -150,14 +68,14 @@ func touch(w http.ResponseWriter, r *http.Request) {
cy = min(max(cy, 0), k.pw-1)
xl, yl := cy, k.ph-1-cx
t := touchscreen
rawX := t.minX + int32(int64(xl)*int64(t.maxX-t.minX)/int64(k.pw-1))
rawY := t.minY + int32(int64(yl)*int64(t.maxY-t.minY)/int64(k.ph-1))
evs := []inputEvent{{evAbs, absX, rawX}, {evAbs, absY, rawY}}
rawX := t.MinX + int32(int64(xl)*int64(t.MaxX-t.MinX)/int64(k.pw-1))
rawY := t.MinY + int32(int64(yl)*int64(t.MaxY-t.MinY)/int64(k.ph-1))
evs := []inputEvent{{evdev.Abs, evdev.AbsX, rawX}, {evdev.Abs, evdev.AbsY, rawY}}
if f[0] == "down" {
evs = append(evs, inputEvent{evKey, btnTouch, 1})
evs = append(evs, inputEvent{evdev.Key, evdev.BtnTouch, 1})
}
evs = append(evs, inputEvent{evSyn, 0, 0})
if err := t.send(evs); err != nil {
evs = append(evs, inputEvent{evdev.Syn, 0, 0})
if err := send(touchscreen, evs); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
+1 -1
View File
@@ -85,7 +85,7 @@ func (h *history) addFaults(now time.Time, e errs) {
func (h *history) addCorrected(now time.Time, d uint64) {
h.mu.Lock()
h.slot(int64(now.Sub(h.start)/histSlotSpan)).corrected += d
h.slot(int64(now.Sub(h.start) / histSlotSpan)).corrected += d
h.mu.Unlock()
}
+4 -2
View File
@@ -7,6 +7,8 @@ import (
"time"
"golang.org/x/sys/unix"
"g.fc.run/theater/cabletest/internal/evdev"
)
// Running as PID 1 there is nothing underneath us: sysfs and devtmpfs are what
@@ -129,9 +131,9 @@ func waitForTouchscreen() checkResult {
res := checkResult{item: "touchscreen"}
start := time.Now()
for {
f, err := findTouchscreen()
ts, err := evdev.FindTouchscreen(os.O_RDONLY)
if err == nil {
f.Close()
ts.F.Close()
waited := time.Since(start)
if waited < touchPoll {
res.state = "ready"
+16 -90
View File
@@ -4,31 +4,13 @@ import (
"encoding/binary"
"fmt"
"os"
"path/filepath"
"sync"
"unsafe"
"golang.org/x/sys/unix"
"g.fc.run/theater/cabletest/internal/evdev"
)
const (
evSyn = 0x00
evKey = 0x01
evAbs = 0x03
synReport = 0x00
btnTouch = 0x14a
absX = 0x00
absY = 0x01
inputPropDirect = 0x01
sizeofInputEvent = 24
)
func evIoc(nr, size uintptr) uintptr {
const iocRead = 2
return iocRead<<30 | size<<16 | 'E'<<8 | nr
}
// Current finger position and whether it is down. Held as state rather than
// delivered as events, so a dropped release can never leave a hold armed.
type touchState struct {
@@ -49,109 +31,53 @@ func (t *touchState) get() (x, y int, down bool) {
return t.x, t.y, t.down
}
// Picks the direct-input device that reports absolute X and Y, which is the
// touchscreen; a mouse is relative and a keyboard has no absolute axes.
func findTouchscreen() (*os.File, error) {
paths, err := filepath.Glob("/dev/input/event*")
if err != nil {
return nil, err
}
for _, p := range paths {
f, err := os.OpenFile(p, os.O_RDONLY, 0)
if err != nil {
continue
}
var props, absBits uint64
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
evIoc(0x09, 8), uintptr(unsafe.Pointer(&props))); errno != 0 {
f.Close()
continue
}
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
evIoc(0x20+evAbs, 8), uintptr(unsafe.Pointer(&absBits))); errno != 0 {
f.Close()
continue
}
if props&(1<<inputPropDirect) != 0 &&
absBits&(1<<absX) != 0 && absBits&(1<<absY) != 0 {
return f, nil
}
f.Close()
}
return nil, fmt.Errorf("no touchscreen found among /dev/input/event*")
}
func axisRange(f *os.File, axis uintptr) (min, max int32, err error) {
var info [6]int32
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
evIoc(0x40+axis, 24), uintptr(unsafe.Pointer(&info[0]))); errno != 0 {
return 0, 0, errno
}
return info[1], info[2], nil
}
// Tracks the finger position, scaled to the given screen size.
func watchTouch(w, h int) (*touchState, error) {
f, err := findTouchscreen()
ts, err := evdev.FindTouchscreen(os.O_RDONLY)
if err != nil {
return nil, err
}
minX, maxX, err := axisRange(f, absX)
if err != nil {
f.Close()
return nil, fmt.Errorf("x range: %w", err)
}
minY, maxY, err := axisRange(f, absY)
if err != nil {
f.Close()
return nil, fmt.Errorf("y range: %w", err)
}
if maxX <= minX || maxY <= minY {
f.Close()
return nil, fmt.Errorf("touchscreen axes are degenerate: x %d..%d y %d..%d",
minX, maxX, minY, maxY)
}
state := &touchState{}
go func() {
defer holdPanic()
defer f.Close()
buf := make([]byte, sizeofInputEvent*32)
defer ts.F.Close()
buf := make([]byte, evdev.EventSize*32)
var rawX, rawY int32
var down bool
for {
n, err := f.Read(buf)
n, err := ts.F.Read(buf)
// Nothing else feeds this state, so returning here would freeze the
// last touch in place and leave the reset button dead while the panel
// goes on looking alive.
if err != nil {
panic(fmt.Sprintf("reading touchscreen events: %v", err))
}
for o := 0; o+sizeofInputEvent <= n; o += sizeofInputEvent {
for o := 0; o+evdev.EventSize <= n; o += evdev.EventSize {
typ := binary.NativeEndian.Uint16(buf[o+16:])
code := binary.NativeEndian.Uint16(buf[o+18:])
val := int32(binary.NativeEndian.Uint32(buf[o+20:]))
switch typ {
case evAbs:
case evdev.Abs:
switch code {
case absX:
case evdev.AbsX:
rawX = val
case absY:
case evdev.AbsY:
rawY = val
}
case evKey:
if code == btnTouch {
case evdev.Key:
if code == evdev.BtnTouch {
down = val != 0
}
case evSyn:
if code != synReport {
case evdev.Syn:
if code != evdev.SynReport {
break
}
// Scaled onto [0, w-1], so full deflection is the last pixel
// rather than one past it.
state.set(
int(int64(rawX-minX)*int64(w-1)/int64(maxX-minX)),
int(int64(rawY-minY)*int64(h-1)/int64(maxY-minY)),
int(int64(rawX-ts.MinX)*int64(w-1)/int64(ts.MaxX-ts.MinX)),
int(int64(rawY-ts.MinY)*int64(h-1)/int64(ts.MaxY-ts.MinY)),
down)
}
}
+91
View File
@@ -0,0 +1,91 @@
// Package evdev is the kernel input-event ABI.
package evdev
import (
"fmt"
"os"
"path/filepath"
"unsafe"
"golang.org/x/sys/unix"
)
const (
Syn = 0x00
Key = 0x01
Abs = 0x03
SynReport = 0x00
BtnTouch = 0x14a
AbsX = 0x00
AbsY = 0x01
propDirect = 0x01
EventSize = 24
)
func ioc(nr, size uintptr) uintptr {
const iocRead = 2
return iocRead<<30 | size<<16 | 'E'<<8 | nr
}
type Touchscreen struct {
F *os.File
MinX, MaxX, MinY, MaxY int32
}
// Picks the direct-input device that reports absolute X and Y, which is the
// touchscreen; a mouse is relative and a keyboard has no absolute axes.
func FindTouchscreen(flag int) (*Touchscreen, error) {
paths, err := filepath.Glob("/dev/input/event*")
if err != nil {
return nil, err
}
for _, p := range paths {
f, err := os.OpenFile(p, flag, 0)
if err != nil {
continue
}
var props, absBits uint64
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
ioc(0x09, 8), uintptr(unsafe.Pointer(&props))); errno != 0 {
f.Close()
continue
}
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
ioc(0x20+Abs, 8), uintptr(unsafe.Pointer(&absBits))); errno != 0 {
f.Close()
continue
}
if props&(1<<propDirect) == 0 ||
absBits&(1<<AbsX) == 0 || absBits&(1<<AbsY) == 0 {
f.Close()
continue
}
t := &Touchscreen{F: f}
if t.MinX, t.MaxX, err = axisRange(f, AbsX); err != nil {
f.Close()
return nil, fmt.Errorf("%s x range: %w", p, err)
}
if t.MinY, t.MaxY, err = axisRange(f, AbsY); err != nil {
f.Close()
return nil, fmt.Errorf("%s y range: %w", p, err)
}
if t.MaxX <= t.MinX || t.MaxY <= t.MinY {
f.Close()
return nil, fmt.Errorf("%s: degenerate axes x %d..%d y %d..%d",
p, t.MinX, t.MaxX, t.MinY, t.MaxY)
}
return t, nil
}
return nil, fmt.Errorf("no touchscreen found among /dev/input/event*")
}
func axisRange(f *os.File, axis uintptr) (min, max int32, err error) {
var info [6]int32
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
ioc(0x40+axis, 24), uintptr(unsafe.Pointer(&info[0]))); errno != 0 {
return 0, 0, errno
}
return info[1], info[2], nil
}
+2 -3
View File
@@ -23,8 +23,7 @@ type endpoint struct {
}
func (e endpoint) macString() string {
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x",
e.mac[0], e.mac[1], e.mac[2], e.mac[3], e.mac[4], e.mac[5])
return net.HardwareAddr(e.mac[:]).String()
}
func lookupEndpoint(name string) (endpoint, error) {
@@ -223,7 +222,7 @@ func run(aName, bName string) (err error) {
if err != nil {
return err
}
checks = append(checks, moduleChecks(modules, [2]string{a.name, b.name})...)
checks = append(checks, moduleChecks(modules)...)
if err := reportChecks("SETTINGS", checks); err != nil {
return err
}
+9 -9
View File
@@ -612,7 +612,7 @@ func waitCarrier(names [2]string, done *atomic.Bool) {
// carrier settles — the host checks just reset the links — before any command.
// Never waits for a link: there may be no cable, and forcing config needs
// none — the AN restart applies it whenever training next happens.
func moduleChecks(mods []*phyModule, names [2]string) []checkResult {
func moduleChecks(mods []*phyModule) []checkResult {
var out []checkResult
fail := func(item string, err error) []checkResult {
return append(out, checkResult{item: item, err: err})
@@ -628,26 +628,26 @@ func moduleChecks(mods []*phyModule, names [2]string) []checkResult {
}
}
master := !mixed
for i, m := range mods {
for _, m := range mods {
b, ok := m.dev.(*bcm)
if !ok {
out = append(out, checkResult{item: names[i] + " role", state: "auto"})
out = append(out, checkResult{item: m.dev.name() + " role", state: "auto"})
continue
}
res := checkResult{item: names[i] + " eee", state: "forced off"}
res := checkResult{item: m.dev.name() + " eee", state: "forced off"}
if err := b.forceEEEOff(); err != nil {
return fail(res.item, err)
}
out = append(out, res)
res = checkResult{item: names[i] + " jumbo", state: "forced on"}
res = checkResult{item: m.dev.name() + " jumbo", state: "forced on"}
if err := b.forceJumbo(); err != nil {
return fail(res.item, err)
}
out = append(out, res)
res = checkResult{item: names[i] + " role", state: "forced master"}
res = checkResult{item: m.dev.name() + " role", state: "forced master"}
if !master {
res.state = "forced slave"
}
@@ -672,7 +672,7 @@ func moduleChecks(mods []*phyModule, names [2]string) []checkResult {
adv[i] = v
advs[i] = fmt.Sprintf("%#04x", v)
if _, ok := m.dev.(*bcm); ok && v != 0 {
res.err = fmt.Errorf("%s still advertises EEE %#04x", names[i], v)
res.err = fmt.Errorf("%s still advertises EEE %#04x", m.dev.name(), v)
}
}
if res.err == nil && adv[0]&adv[1] != 0 {
@@ -681,9 +681,9 @@ func moduleChecks(mods []*phyModule, names [2]string) []checkResult {
res.state = advs[0] + "/" + advs[1]
out = append(out, res)
for i, m := range mods {
for _, m := range mods {
if err := devRestartAN(m.dev); err != nil {
return fail(names[i]+" retrain", err)
return fail(m.dev.name()+" retrain", err)
}
}
return out
+19 -7
View File
@@ -15,10 +15,15 @@ type sff struct {
// Every transport touch happens on the loop goroutine: requests execute
// one at a time, each admitted by the module type's own gate first.
reqs chan func()
reqs chan sffReq
admit func()
}
type sffReq struct {
fn func()
done chan struct{}
}
func openSFF(ifname string) (*sff, error) {
devLink, err := os.Readlink("/sys/class/net/" + ifname + "/device")
if err != nil {
@@ -34,7 +39,7 @@ func openSFF(ifname string) (*sff, error) {
s := &sff{
ifname: ifname,
path: "/sys/kernel/debug/ixgbe/" + filepath.Base(devLink) + "/sff_i2c",
reqs: make(chan func()),
reqs: make(chan sffReq),
admit: func() {},
}
if _, err := os.Stat(s.path); err != nil {
@@ -45,16 +50,23 @@ func openSFF(ifname string) (*sff, error) {
}
func (s *sff) loop() {
defer holdPanic()
for fn := range s.reqs {
s.admit()
fn()
for r := range s.reqs {
s.serve(r)
}
}
// done closes even when the request panics, and the recover keeps the loop
// serving, so no exec caller is ever stranded waiting on a dead loop.
func (s *sff) serve(r sffReq) {
defer close(r.done)
defer holdPanic()
s.admit()
r.fn()
}
func (s *sff) exec(fn func()) {
done := make(chan struct{})
s.reqs <- func() { fn(); close(done) }
s.reqs <- sffReq{fn: fn, done: done}
<-done
}
+15 -54
View File
@@ -100,7 +100,7 @@ func allRuleLocations(fd int, ifname string) ([]uint32, uint32, error) {
// rule_locs follows rule_cnt directly, before the struct's tail padding,
// and that is where the kernel copies it to.
locOff := unsafe.Offsetof(ethtoolRxnfc{}.ruleCnt) + 4
buf := make([]byte, int(unsafe.Sizeof(ethtoolRxnfc{}))+4*int(cnt.ruleCnt))
buf := statsBuf((int(unsafe.Sizeof(ethtoolRxnfc{})) + 4*int(cnt.ruleCnt) + 7) / 8)
all := (*ethtoolRxnfc)(unsafe.Pointer(&buf[0]))
all.cmd = ethtoolGRXCLSRLALL
all.ruleCnt = cnt.ruleCnt
@@ -113,7 +113,7 @@ func allRuleLocations(fd int, ifname string) ([]uint32, uint32, error) {
ifname, all.ruleCnt, cnt.ruleCnt)
}
raw := buf[locOff:]
raw := unsafe.Slice((*byte)(unsafe.Pointer(&buf[0])), len(buf)*8)[locOff:]
locs := make([]uint32, 0, all.ruleCnt)
for i := 0; i < int(all.ruleCnt); i++ {
locs = append(locs, binary.LittleEndian.Uint32(raw[i*4:]))
@@ -311,7 +311,10 @@ type ethtoolCoalesce struct {
}
const (
wantGovernor = "performance"
wantGovernor = "performance"
// Under the performance governor a max-frequency bound is a fixed clock, not a
// ceiling the cores rove beneath, so the line-rate margin it was qualified with
// is the margin every run gets.
wantMaxFreqKHz = "2000000"
wantCoalesceUsecs = 25
wantRxRing = 8160
@@ -522,11 +525,11 @@ func (r *statReader) sum() uint64 {
return total
}
func checkGovernor(want string) checkResult {
res := checkResult{item: "cpu governor"}
paths, err := filepath.Glob("/sys/devices/system/cpu/cpu*/cpufreq/scaling_governor")
func checkCPUAttr(item, attr, want, unit string) checkResult {
res := checkResult{item: item}
paths, err := filepath.Glob("/sys/devices/system/cpu/cpu*/cpufreq/" + attr)
if err != nil || len(paths) == 0 {
res.err = fmt.Errorf("no cpufreq governors found")
res.err = fmt.Errorf("no cpufreq %s found", attr)
return res
}
var wrong []string
@@ -544,7 +547,7 @@ func checkGovernor(want string) checkResult {
}
}
if len(wrong) == 0 {
res.state = fmt.Sprintf("%s on all %d cpus", want, len(paths))
res.state = fmt.Sprintf("%s%s on all %d cpus", want, unit, len(paths))
return res
}
var found []string
@@ -559,51 +562,7 @@ func checkGovernor(want string) checkResult {
}
}
res.fixed = true
res.state = fmt.Sprintf("was %s, now %s on all %d cpus", strings.Join(found, " "), want, len(paths))
return res
}
// Under the performance governor a max-frequency bound is a fixed clock, not a
// ceiling the cores rove beneath, so the line-rate margin it was qualified with
// is the margin every run gets.
func checkMaxFreq(wantKHz string) checkResult {
res := checkResult{item: "cpu max frequency"}
paths, err := filepath.Glob("/sys/devices/system/cpu/cpu*/cpufreq/scaling_max_freq")
if err != nil || len(paths) == 0 {
res.err = fmt.Errorf("no cpufreq max frequencies found")
return res
}
var wrong []string
counts := map[string]int{}
for _, p := range paths {
b, err := os.ReadFile(p)
if err != nil {
res.err = err
return res
}
got := strings.TrimSpace(string(b))
counts[got]++
if got != wantKHz {
wrong = append(wrong, p)
}
}
if len(wrong) == 0 {
res.state = fmt.Sprintf("%s kHz on all %d cpus", wantKHz, len(paths))
return res
}
var found []string
for k, v := range counts {
found = append(found, fmt.Sprintf("%s:%d", k, v))
}
for _, p := range wrong {
if err := os.WriteFile(p, []byte(wantKHz), 0o644); err != nil {
res.err = err
res.state = fmt.Sprintf("could not set %s", p)
return res
}
}
res.fixed = true
res.state = fmt.Sprintf("was %s, now %s kHz on all %d cpus", strings.Join(found, " "), wantKHz, len(paths))
res.state = fmt.Sprintf("was %s, now %s%s on all %d cpus", strings.Join(found, " "), want, unit, len(paths))
return res
}
@@ -774,7 +733,9 @@ func withIoctlSocket(fn func(fd int) []checkResult) []checkResult {
// under test, so with no cable there is never going to be one.
func configureSystem(ifnames []string, ethertypes []uint16) []checkResult {
return withIoctlSocket(func(fd int) []checkResult {
out := []checkResult{checkGovernor(wantGovernor), checkMaxFreq(wantMaxFreqKHz),
out := []checkResult{
checkCPUAttr("cpu governor", "scaling_governor", wantGovernor, ""),
checkCPUAttr("cpu max frequency", "scaling_max_freq", wantMaxFreqKHz, " kHz"),
checkIdleLatency(wantIdleLatency)}
for i, ifname := range ifnames {
out = append(out, checkLinkUp(fd, ifname))
+1 -1
View File
@@ -449,7 +449,7 @@ func (d *display) drawHeader(phy phyDisplay, hv histView, measuring bool,
func (d *display) drawColHeaders() {
y := d.headerH + 8
d.textRightAt(d.small, d.resetX+d.resetW, y, "RESET", uiMuted)
hdr := "LAST 90 S"
hdr := fmt.Sprintf("LAST %d S", int(histSpan/time.Second))
hw := len([]rune(hdr)) * d.small.cellW
d.small.draw(d.fb, d.cellsX+(d.cellsW-hw)/2, y-d.small.capTop, hdr, uiMuted)
d.textRightAt(d.small, d.nowX+d.nowW, y, "NOW", uiMuted)