Files
tyto/backend/internal/collectors/amdgpu.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

170 lines
3.9 KiB
Go

package collectors
import (
"os"
"path/filepath"
"strconv"
"strings"
"tyto/internal/models"
)
type AMDGPUCollector struct {
sysPath string
cardPath string
hwmonPath string
available bool
name string
}
func NewAMDGPUCollector(sysPath string) *AMDGPUCollector {
c := &AMDGPUCollector{sysPath: sysPath}
c.detectCard()
return c
}
func (c *AMDGPUCollector) detectCard() {
drmPath := filepath.Join(c.sysPath, "class/drm")
entries, err := os.ReadDir(drmPath)
if err != nil {
return
}
for _, entry := range entries {
name := entry.Name()
// Look for card directories (card0, card1, ...) but not render nodes
if !strings.HasPrefix(name, "card") || strings.Contains(name, "-") {
continue
}
devicePath := filepath.Join(drmPath, name, "device")
// Check if this is an AMD GPU by looking at the driver
driverLink, err := os.Readlink(filepath.Join(devicePath, "driver"))
if err != nil {
continue
}
if !strings.Contains(driverLink, "amdgpu") {
continue
}
c.cardPath = devicePath
c.available = true
// Find hwmon path
hwmonDir := filepath.Join(devicePath, "hwmon")
hwmonEntries, err := os.ReadDir(hwmonDir)
if err == nil && len(hwmonEntries) > 0 {
c.hwmonPath = filepath.Join(hwmonDir, hwmonEntries[0].Name())
}
// Try to get GPU name from uevent
ueventData, err := os.ReadFile(filepath.Join(devicePath, "uevent"))
if err == nil {
for _, line := range strings.Split(string(ueventData), "\n") {
if strings.HasPrefix(line, "PCI_ID=") {
c.name = strings.TrimPrefix(line, "PCI_ID=")
}
}
}
return
}
}
func (c *AMDGPUCollector) Collect() (models.AMDGPUStats, error) {
stats := models.AMDGPUStats{
Available: c.available,
Name: c.name,
}
if !c.available {
return stats, nil
}
// GPU utilization
if val, err := c.readInt(filepath.Join(c.cardPath, "gpu_busy_percent")); err == nil {
stats.Utilization = val
}
// VRAM usage
if val, err := c.readUint64(filepath.Join(c.cardPath, "mem_info_vram_used")); err == nil {
stats.VRAMUsed = val
}
if val, err := c.readUint64(filepath.Join(c.cardPath, "mem_info_vram_total")); err == nil {
stats.VRAMTotal = val
}
// Temperature from hwmon (millidegrees Celsius)
if c.hwmonPath != "" {
if val, err := c.readInt(filepath.Join(c.hwmonPath, "temp1_input")); err == nil {
stats.Temperature = float64(val) / 1000.0
}
// Fan speed (RPM)
if val, err := c.readInt(filepath.Join(c.hwmonPath, "fan1_input")); err == nil {
stats.FanRPM = val
}
// Power usage (microwatts to watts)
if val, err := c.readInt(filepath.Join(c.hwmonPath, "power1_average")); err == nil {
stats.PowerWatts = float64(val) / 1000000.0
}
}
// Clock speeds from pp_dpm_sclk and pp_dpm_mclk
stats.ClockGPU = c.parseCurrentClock(filepath.Join(c.cardPath, "pp_dpm_sclk"))
stats.ClockMemory = c.parseCurrentClock(filepath.Join(c.cardPath, "pp_dpm_mclk"))
return stats, nil
}
func (c *AMDGPUCollector) parseCurrentClock(path string) int {
data, err := os.ReadFile(path)
if err != nil {
return 0
}
// Parse lines like "1: 1311Mhz *" where * indicates current
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if !strings.HasSuffix(line, "*") {
continue
}
// Remove the * and parse
line = strings.TrimSuffix(line, "*")
parts := strings.Fields(line)
if len(parts) < 2 {
continue
}
freqStr := parts[1]
freqStr = strings.TrimSuffix(freqStr, "Mhz")
freqStr = strings.TrimSuffix(freqStr, "MHz")
if freq, err := strconv.Atoi(freqStr); err == nil {
return freq
}
}
return 0
}
func (c *AMDGPUCollector) readInt(path string) (int, error) {
data, err := os.ReadFile(path)
if err != nil {
return 0, err
}
return strconv.Atoi(strings.TrimSpace(string(data)))
}
func (c *AMDGPUCollector) readUint64(path string) (uint64, error) {
data, err := os.ReadFile(path)
if err != nil {
return 0, err
}
return strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64)
}