mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 18:34:46 +00:00
- 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)
237 lines
5.1 KiB
Go
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
|