Files
tyto/backend/internal/collectors/cpu.go
vikingowl a2504c1327 feat: rename project to Tyto with owl branding
- Rename project from system-monitor to Tyto (barn owl themed)
- Update Go module name and all import paths
- Update Docker container names (tyto-backend, tyto-frontend)
- Update localStorage keys (tyto-settings, tyto-hosts)
- Create barn owl SVG favicon and PWA icons (192, 512)
- Update header with owl logo icon
- Update manifest.json and app.html with Tyto branding

Named after Tyto alba, the barn owl — nature's silent, watchful guardian

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 06:36:01 +01:00

154 lines
3.2 KiB
Go

package collectors
import (
"bufio"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"tyto/internal/models"
)
type CPUCollector struct {
procPath string
sysPath string
prevStats map[string]cpuTime
mu sync.Mutex
}
type cpuTime struct {
user, nice, system, idle, iowait, irq, softirq uint64
}
func NewCPUCollector(procPath, sysPath string) *CPUCollector {
return &CPUCollector{
procPath: procPath,
sysPath: sysPath,
prevStats: make(map[string]cpuTime),
}
}
func (c *CPUCollector) Collect() (models.CPUStats, error) {
stats := models.CPUStats{
Cores: []models.CPUCore{}, // Initialize to empty slice, not nil
}
// Read /proc/stat for usage
file, err := os.Open(filepath.Join(c.procPath, "stat"))
if err != nil {
return stats, err
}
defer file.Close()
c.mu.Lock()
defer c.mu.Unlock()
scanner := bufio.NewScanner(file)
coreID := 0
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "cpu") {
continue
}
fields := strings.Fields(line)
if len(fields) < 8 {
continue
}
name := fields[0]
current := cpuTime{
user: parseUint(fields[1]),
nice: parseUint(fields[2]),
system: parseUint(fields[3]),
idle: parseUint(fields[4]),
iowait: parseUint(fields[5]),
irq: parseUint(fields[6]),
softirq: parseUint(fields[7]),
}
usage := c.calculateUsage(name, current)
if name == "cpu" {
stats.TotalUsage = usage
} else {
freq := c.getCoreFrequency(coreID)
stats.Cores = append(stats.Cores, models.CPUCore{
ID: coreID,
Usage: usage,
Frequency: freq,
})
coreID++
}
c.prevStats[name] = current
}
// Load average from /proc/loadavg
stats.LoadAverage = c.getLoadAverage()
return stats, nil
}
func (c *CPUCollector) calculateUsage(name string, current cpuTime) float64 {
prev, exists := c.prevStats[name]
if !exists {
return 0
}
prevTotal := prev.user + prev.nice + prev.system + prev.idle + prev.iowait + prev.irq + prev.softirq
currTotal := current.user + current.nice + current.system + current.idle + current.iowait + current.irq + current.softirq
totalDiff := float64(currTotal - prevTotal)
if totalDiff == 0 {
return 0
}
idleDiff := float64((current.idle + current.iowait) - (prev.idle + prev.iowait))
return (1 - idleDiff/totalDiff) * 100
}
func (c *CPUCollector) getCoreFrequency(coreID int) int64 {
// Try scaling_cur_freq first (more accurate)
path := filepath.Join(c.sysPath, "devices/system/cpu",
"cpu"+strconv.Itoa(coreID), "cpufreq/scaling_cur_freq")
data, err := os.ReadFile(path)
if err != nil {
return 0
}
freq, _ := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
return freq / 1000 // Convert kHz to MHz
}
func (c *CPUCollector) getLoadAverage() models.LoadAverage {
data, err := os.ReadFile(filepath.Join(c.procPath, "loadavg"))
if err != nil {
return models.LoadAverage{}
}
fields := strings.Fields(string(data))
if len(fields) < 3 {
return models.LoadAverage{}
}
load1, _ := strconv.ParseFloat(fields[0], 64)
load5, _ := strconv.ParseFloat(fields[1], 64)
load15, _ := strconv.ParseFloat(fields[2], 64)
return models.LoadAverage{
Load1: load1,
Load5: load5,
Load15: load15,
}
}
func parseUint(s string) uint64 {
v, _ := strconv.ParseUint(s, 10, 64)
return v
}