- 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>
154 lines
3.2 KiB
Go
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
|
|
}
|