1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-12 19:15:13 +00:00

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)
This commit is contained in:
bsisduck
2025-12-07 17:12:48 +01:00
parent e7fd73302d
commit 32ac98f06a
5 changed files with 157 additions and 35 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"regexp"
"runtime"
"strconv"
"strings"
@@ -13,17 +14,29 @@ import (
const (
systemProfilerTimeout = 4 * time.Second
macGPUInfoTTL = 10 * time.Minute
powermetricsTimeout = 2 * time.Second
)
func (c *Collector) collectGPU(now time.Time) ([]GPUStatus, error) {
if runtime.GOOS == "darwin" {
if len(c.cachedGPU) > 0 && !c.lastGPUAt.IsZero() && now.Sub(c.lastGPUAt) < macGPUInfoTTL {
return c.cachedGPU, nil
// Get static GPU info (cached for 10 min)
if len(c.cachedGPU) == 0 || c.lastGPUAt.IsZero() || now.Sub(c.lastGPUAt) >= macGPUInfoTTL {
if gpus, err := readMacGPUInfo(); err == nil && len(gpus) > 0 {
c.cachedGPU = gpus
c.lastGPUAt = now
}
}
if gpus, err := readMacGPUInfo(); err == nil && len(gpus) > 0 {
c.cachedGPU = gpus
c.lastGPUAt = now
return gpus, nil
// Get real-time GPU usage
if len(c.cachedGPU) > 0 {
usage := getMacGPUUsage()
result := make([]GPUStatus, len(c.cachedGPU))
copy(result, c.cachedGPU)
// Apply usage to first GPU (Apple Silicon has one integrated GPU)
if len(result) > 0 {
result[0].Usage = usage
}
return result, nil
}
}
@@ -91,6 +104,7 @@ func readMacGPUInfo() ([]GPUStatus, error) {
VRAM string `json:"spdisplays_vram"`
Vendor string `json:"spdisplays_vendor"`
Metal string `json:"spdisplays_metal"`
Cores string `json:"sppci_cores"`
} `json:"SPDisplaysDataType"`
}
if err := json.Unmarshal([]byte(out), &data); err != nil {
@@ -113,10 +127,12 @@ func readMacGPUInfo() ([]GPUStatus, error) {
noteParts = append(noteParts, d.Vendor)
}
note := strings.Join(noteParts, " · ")
coreCount, _ := strconv.Atoi(d.Cores)
gpus = append(gpus, GPUStatus{
Name: d.Name,
Usage: -1,
Note: note,
Name: d.Name,
Usage: -1, // Will be updated with real-time data
CoreCount: coreCount,
Note: note,
})
}
@@ -129,3 +145,38 @@ func readMacGPUInfo() ([]GPUStatus, error) {
return gpus, nil
}
// getMacGPUUsage gets GPU active residency from powermetrics.
// Returns -1 if unavailable (e.g., not running as root).
func getMacGPUUsage() float64 {
ctx, cancel := context.WithTimeout(context.Background(), powermetricsTimeout)
defer cancel()
// powermetrics requires root, but we try anyway - some systems may have it enabled
out, err := runCmd(ctx, "powermetrics", "--samplers", "gpu_power", "-i", "500", "-n", "1")
if err != nil {
return -1
}
// Parse "GPU HW active residency: X.XX%"
re := regexp.MustCompile(`GPU HW active residency:\s+([\d.]+)%`)
matches := re.FindStringSubmatch(out)
if len(matches) >= 2 {
usage, err := strconv.ParseFloat(matches[1], 64)
if err == nil {
return usage
}
}
// Fallback: parse "GPU idle residency: X.XX%" and calculate active
reIdle := regexp.MustCompile(`GPU idle residency:\s+([\d.]+)%`)
matchesIdle := reIdle.FindStringSubmatch(out)
if len(matchesIdle) >= 2 {
idle, err := strconv.ParseFloat(matchesIdle[1], 64)
if err == nil {
return 100.0 - idle
}
}
return -1
}