TUISTREAM/internal/health/cpu.go
28allday 959e507a83 Initial commit: TUISTREAM — headless Jellyfin TUI for Omarchy/Arch
Three-tab Bubble Tea TUI (Setup / Manage / Monitor) for setting up and
running a headless Jellyfin server over SSH:

- Setup: install/uninstall Jellyfin, add media drives (single or btrfs
  RAID pool), import an existing detached btrfs pool non-destructively,
  firewall, move Jellyfin storage onto a media drive
- Boot-drive-safe drive classifier (walks LUKS/LVM/RAID to the physical
  disk; never offers a disk hosting /, /boot or swap)
- Keep-existing-filesystem path previews on-disk content, skips starter
  folders when content exists, and grants Jellyfin recursive read access
- Manage: copy from external drives, mount/eject, rename/delete
- Monitor: per-core CPU, load/mem/uptime, btrfs error counters, SMART

Single static binary; installs to /usr/local/bin so sudo can find it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 16:47:18 +01:00

139 lines
3.1 KiB
Go

// CPU sampling for the Monitor tab. /proc/stat reports cumulative ticks
// per CPU since boot; usage % is the delta of (total - idle) over the
// delta of total between two snapshots.
//
// The Monitor tab samples once per second, holds the previous reading on
// the model, and renders the delta — first sample after startup shows 0
// because there's nothing to subtract against yet.
package health
import (
"bufio"
"os"
"strconv"
"strings"
)
// ProcStatLine is one row of /proc/stat (either the "cpu" aggregate or a
// single "cpuN" core). All values are cumulative kernel ticks since boot.
type ProcStatLine struct {
Index int // -1 for the aggregate line
User uint64
Nice uint64
System uint64
Idle uint64
IOWait uint64
IRQ uint64
SoftIRQ uint64
Steal uint64
Total uint64 // sum of all of the above
IdleAll uint64 // Idle + IOWait
}
// CPUUsage is the per-tick result: an aggregate % and per-core %s in
// CPU-index order. Empty when we don't yet have a previous sample.
type CPUUsage struct {
Aggregate float64
Cores []float64
}
// ReadProcStat parses /proc/stat. Stops at the first non-cpu line for
// speed (we don't need intr/ctxt/btime/etc).
func ReadProcStat() ([]ProcStatLine, error) {
f, err := os.Open("/proc/stat")
if err != nil {
return nil, err
}
defer f.Close()
var out []ProcStatLine
sc := bufio.NewScanner(f)
for sc.Scan() {
line := sc.Text()
if !strings.HasPrefix(line, "cpu") {
break
}
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
idx := -1
if fields[0] != "cpu" {
if n, err := strconv.Atoi(strings.TrimPrefix(fields[0], "cpu")); err == nil {
idx = n
}
}
var ps ProcStatLine
ps.Index = idx
vals := fields[1:]
readU64 := func(i int) uint64 {
if i >= len(vals) {
return 0
}
v, _ := strconv.ParseUint(vals[i], 10, 64)
return v
}
ps.User = readU64(0)
ps.Nice = readU64(1)
ps.System = readU64(2)
ps.Idle = readU64(3)
ps.IOWait = readU64(4)
ps.IRQ = readU64(5)
ps.SoftIRQ = readU64(6)
ps.Steal = readU64(7)
for _, v := range vals {
n, _ := strconv.ParseUint(v, 10, 64)
ps.Total += n
}
ps.IdleAll = ps.Idle + ps.IOWait
out = append(out, ps)
}
return out, sc.Err()
}
// DeltaCPU computes usage between two /proc/stat snapshots. Returns an
// empty CPUUsage if `prev` is empty (first sample after startup).
func DeltaCPU(prev, now []ProcStatLine) CPUUsage {
if len(prev) == 0 {
return CPUUsage{}
}
prevByIdx := map[int]ProcStatLine{}
for _, p := range prev {
prevByIdx[p.Index] = p
}
var u CPUUsage
for _, n := range now {
p, ok := prevByIdx[n.Index]
if !ok {
continue
}
pct := cpuPct(p, n)
if n.Index == -1 {
u.Aggregate = pct
} else {
u.Cores = append(u.Cores, pct)
}
}
return u
}
func cpuPct(prev, now ProcStatLine) float64 {
if now.Total <= prev.Total {
return 0
}
dTotal := now.Total - prev.Total
var dIdle uint64
if now.IdleAll > prev.IdleAll {
dIdle = now.IdleAll - prev.IdleAll
}
if dTotal == 0 {
return 0
}
pct := float64(dTotal-dIdle) / float64(dTotal) * 100
if pct < 0 {
return 0
}
if pct > 100 {
return 100
}
return pct
}