1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 17:24:45 +00:00
Files
Mole/cmd/status/metrics.go
bsisduck 32ac98f06a Enhance CPU/GPU display for Apple Silicon
- Show all CPU cores instead of top 3 busiest
- Add P-CPU/E-CPU core grouping (Performance/Efficiency)
- Display GPU core count from system_profiler
- Add GPU usage meter via powermetrics (requires sudo)
- Show hint to run with sudo for GPU metrics

Tested on M2 Pro (6P + 4E cores, 16 GPU cores)
2025-12-07 17:37:30 +01:00

237 lines
5.1 KiB
Go

package main
import (
"context"
"fmt"
"os/exec"
"time"
"github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/host"
"github.com/shirou/gopsutil/v3/net"
)
type MetricsSnapshot struct {
CollectedAt time.Time
Host string
Platform string
Uptime string
Procs uint64
Hardware HardwareInfo
HealthScore int // 0-100 system health score
HealthScoreMsg string // Brief explanation
CPU CPUStatus
GPU []GPUStatus
Memory MemoryStatus
Disks []DiskStatus
DiskIO DiskIOStatus
Network []NetworkStatus
Proxy ProxyStatus
Batteries []BatteryStatus
Thermal ThermalStatus
Sensors []SensorReading
Bluetooth []BluetoothDevice
TopProcesses []ProcessInfo
}
type HardwareInfo struct {
Model string // MacBook Pro 14-inch, 2021
CPUModel string // Apple M1 Pro / Intel Core i7
TotalRAM string // 16GB
DiskSize string // 512GB
OSVersion string // macOS Sonoma 14.5
}
type DiskIOStatus struct {
ReadRate float64 // MB/s
WriteRate float64 // MB/s
}
type ProcessInfo struct {
Name string
CPU float64
Memory float64
}
type CPUStatus struct {
Usage float64
PerCore []float64
PerCoreEstimated bool
Load1 float64
Load5 float64
Load15 float64
CoreCount int
LogicalCPU int
PCoreCount int // Performance cores (Apple Silicon)
ECoreCount int // Efficiency cores (Apple Silicon)
}
type GPUStatus struct {
Name string
Usage float64
MemoryUsed float64
MemoryTotal float64
CoreCount int
Note string
}
type MemoryStatus struct {
Used uint64
Total uint64
UsedPercent float64
SwapUsed uint64
SwapTotal uint64
Pressure string // macOS memory pressure: normal/warn/critical
}
type DiskStatus struct {
Mount string
Device string
Used uint64
Total uint64
UsedPercent float64
Fstype string
External bool
}
type NetworkStatus struct {
Name string
RxRateMBs float64
TxRateMBs float64
IP string
}
type ProxyStatus struct {
Enabled bool
Type string // HTTP, SOCKS, System
Host string
}
type BatteryStatus struct {
Percent float64
Status string
TimeLeft string
Health string
CycleCount int
}
type ThermalStatus struct {
CPUTemp float64
GPUTemp float64
FanSpeed int
FanCount int
}
type SensorReading struct {
Label string
Value float64
Unit string
Note string
}
type BluetoothDevice struct {
Name string
Connected bool
Battery string
}
type Collector struct {
prevNet map[string]net.IOCountersStat
lastNetAt time.Time
lastBTAt time.Time
lastBT []BluetoothDevice
lastGPUAt time.Time
cachedGPU []GPUStatus
prevDiskIO disk.IOCountersStat
lastDiskAt time.Time
}
func NewCollector() *Collector {
return &Collector{
prevNet: make(map[string]net.IOCountersStat),
}
}
func (c *Collector) Collect() (MetricsSnapshot, error) {
now := time.Now()
hostInfo, _ := host.Info()
cpuStats, cpuErr := collectCPU()
memStats, memErr := collectMemory()
diskStats, diskErr := collectDisks()
hwInfo := collectHardware(memStats.Total, diskStats)
diskIO := c.collectDiskIO(now)
netStats, netErr := c.collectNetwork(now)
proxyStats := collectProxy()
batteryStats, _ := collectBatteries()
thermalStats := collectThermal()
sensorStats, _ := collectSensors()
gpuStats, gpuErr := c.collectGPU(now)
btStats := c.collectBluetooth(now)
topProcs := collectTopProcesses()
var mergeErr error
for _, e := range []error{cpuErr, memErr, diskErr, netErr, gpuErr} {
if e != nil {
if mergeErr == nil {
mergeErr = e
} else {
mergeErr = fmt.Errorf("%v; %w", mergeErr, e)
}
}
}
// Calculate health score
score, scoreMsg := calculateHealthScore(cpuStats, memStats, diskStats, diskIO, thermalStats)
return MetricsSnapshot{
CollectedAt: now,
Host: hostInfo.Hostname,
Platform: fmt.Sprintf("%s %s", hostInfo.Platform, hostInfo.PlatformVersion),
Uptime: formatUptime(hostInfo.Uptime),
Procs: hostInfo.Procs,
Hardware: hwInfo,
HealthScore: score,
HealthScoreMsg: scoreMsg,
CPU: cpuStats,
GPU: gpuStats,
Memory: memStats,
Disks: diskStats,
DiskIO: diskIO,
Network: netStats,
Proxy: proxyStats,
Batteries: batteryStats,
Thermal: thermalStats,
Sensors: sensorStats,
Bluetooth: btStats,
TopProcesses: topProcs,
}, mergeErr
}
// Utility functions
func runCmd(ctx context.Context, name string, args ...string) (string, error) {
cmd := exec.CommandContext(ctx, name, args...)
output, err := cmd.Output()
if err != nil {
return "", err
}
return string(output), nil
}
func commandExists(name string) bool {
if name == "" {
return false
}
defer func() {
if r := recover(); r != nil {
// If LookPath panics due to permissions or platform quirks, act as if the command is missing.
}
}()
_, err := exec.LookPath(name)
return err == nil
}
// humanBytes is defined in view.go to avoid duplication