mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 19:09:43 +00:00
Merge PR #108: Enhance CPU/GPU display for Apple Silicon
- Add P-core/E-core detection and display (8P+4E format) - Add GPU core count from system_profiler - Add real-time GPU usage via powermetrics - Keep top 3 busiest cores display for clean UI - Pre-compile regex patterns for better performance Thanks to @bsisduck for the initial implementation
This commit is contained in:
BIN
bin/status-go
BIN
bin/status-go
Binary file not shown.
@@ -63,6 +63,8 @@ type CPUStatus struct {
|
||||
Load15 float64
|
||||
CoreCount int
|
||||
LogicalCPU int
|
||||
PCoreCount int // Performance cores (Apple Silicon)
|
||||
ECoreCount int // Efficiency cores (Apple Silicon)
|
||||
}
|
||||
|
||||
type GPUStatus struct {
|
||||
@@ -70,6 +72,7 @@ type GPUStatus struct {
|
||||
Usage float64
|
||||
MemoryUsed float64
|
||||
MemoryTotal float64
|
||||
CoreCount int
|
||||
Note string
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,9 @@ func collectCPU() (CPUStatus, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Get P-core and E-core counts for Apple Silicon
|
||||
pCores, eCores := getCoreTopology()
|
||||
|
||||
return CPUStatus{
|
||||
Usage: totalPercent,
|
||||
PerCore: percents,
|
||||
@@ -72,6 +75,8 @@ func collectCPU() (CPUStatus, error) {
|
||||
Load15: loadAvg.Load15,
|
||||
CoreCount: counts,
|
||||
LogicalCPU: logical,
|
||||
PCoreCount: pCores,
|
||||
ECoreCount: eCores,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -79,6 +84,55 @@ func isZeroLoad(avg load.AvgStat) bool {
|
||||
return avg.Load1 == 0 && avg.Load5 == 0 && avg.Load15 == 0
|
||||
}
|
||||
|
||||
// getCoreTopology returns P-core and E-core counts on Apple Silicon.
|
||||
// Returns (0, 0) on non-Apple Silicon or if detection fails.
|
||||
func getCoreTopology() (pCores, eCores int) {
|
||||
if runtime.GOOS != "darwin" {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
// Get performance level info from sysctl
|
||||
out, err := runCmd(ctx, "sysctl", "-n",
|
||||
"hw.perflevel0.logicalcpu",
|
||||
"hw.perflevel0.name",
|
||||
"hw.perflevel1.logicalcpu",
|
||||
"hw.perflevel1.name")
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(out), "\n")
|
||||
if len(lines) < 4 {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// Parse perflevel0
|
||||
level0Count, _ := strconv.Atoi(strings.TrimSpace(lines[0]))
|
||||
level0Name := strings.ToLower(strings.TrimSpace(lines[1]))
|
||||
|
||||
// Parse perflevel1
|
||||
level1Count, _ := strconv.Atoi(strings.TrimSpace(lines[2]))
|
||||
level1Name := strings.ToLower(strings.TrimSpace(lines[3]))
|
||||
|
||||
// Assign based on name (Performance vs Efficiency)
|
||||
if strings.Contains(level0Name, "performance") {
|
||||
pCores = level0Count
|
||||
} else if strings.Contains(level0Name, "efficiency") {
|
||||
eCores = level0Count
|
||||
}
|
||||
|
||||
if strings.Contains(level1Name, "performance") {
|
||||
pCores = level1Count
|
||||
} else if strings.Contains(level1Name, "efficiency") {
|
||||
eCores = level1Count
|
||||
}
|
||||
|
||||
return pCores, eCores
|
||||
}
|
||||
|
||||
func fallbackLoadAvgFromUptime() (load.AvgStat, error) {
|
||||
if !commandExists("uptime") {
|
||||
return load.AvgStat{}, errors.New("uptime command unavailable")
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -13,17 +14,35 @@ import (
|
||||
const (
|
||||
systemProfilerTimeout = 4 * time.Second
|
||||
macGPUInfoTTL = 10 * time.Minute
|
||||
powermetricsTimeout = 2 * time.Second
|
||||
)
|
||||
|
||||
// Pre-compiled regex patterns for GPU usage parsing
|
||||
var (
|
||||
gpuActiveResidencyRe = regexp.MustCompile(`GPU HW active residency:\s+([\d.]+)%`)
|
||||
gpuIdleResidencyRe = regexp.MustCompile(`GPU idle residency:\s+([\d.]+)%`)
|
||||
)
|
||||
|
||||
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 +110,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 +133,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 +151,36 @@ 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%"
|
||||
matches := gpuActiveResidencyRe.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
|
||||
matchesIdle := gpuIdleResidencyRe.FindStringSubmatch(out)
|
||||
if len(matchesIdle) >= 2 {
|
||||
idle, err := strconv.ParseFloat(matchesIdle[1], 64)
|
||||
if err == nil {
|
||||
return 100.0 - idle
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -163,8 +163,8 @@ func buildCards(m MetricsSnapshot, _ int) []cardData {
|
||||
renderProcessCard(m.TopProcesses),
|
||||
renderNetworkCard(m.Network, m.Proxy),
|
||||
}
|
||||
// Only show GPU card if there are GPUs with usage data
|
||||
if len(m.GPU) > 0 && m.GPU[0].Usage >= 0 {
|
||||
// Only show GPU card if there are GPUs with usage data or core count
|
||||
if len(m.GPU) > 0 && (m.GPU[0].Usage >= 0 || m.GPU[0].CoreCount > 0) {
|
||||
cards = append(cards, renderGPUCard(m.GPU))
|
||||
}
|
||||
// Only show sensors if we have valid temperature readings
|
||||
@@ -186,12 +186,18 @@ func hasSensorData(sensors []SensorReading) bool {
|
||||
func renderCPUCard(cpu CPUStatus) cardData {
|
||||
var lines []string
|
||||
lines = append(lines, fmt.Sprintf("Total %s %5.1f%%", progressBar(cpu.Usage), cpu.Usage))
|
||||
lines = append(lines, subtleStyle.Render(fmt.Sprintf("%.2f / %.2f / %.2f (%d cores)", cpu.Load1, cpu.Load5, cpu.Load15, cpu.LogicalCPU)))
|
||||
|
||||
if cpu.PCoreCount > 0 && cpu.ECoreCount > 0 {
|
||||
lines = append(lines, subtleStyle.Render(fmt.Sprintf("Load %.2f / %.2f / %.2f (%dP+%dE)",
|
||||
cpu.Load1, cpu.Load5, cpu.Load15, cpu.PCoreCount, cpu.ECoreCount)))
|
||||
} else {
|
||||
lines = append(lines, subtleStyle.Render(fmt.Sprintf("%.2f / %.2f / %.2f (%d cores)",
|
||||
cpu.Load1, cpu.Load5, cpu.Load15, cpu.LogicalCPU)))
|
||||
}
|
||||
|
||||
if cpu.PerCoreEstimated {
|
||||
lines = append(lines, subtleStyle.Render("Per-core data unavailable (using averaged load)"))
|
||||
} else if len(cpu.PerCore) > 0 {
|
||||
// Show top 3 busiest cores
|
||||
type coreUsage struct {
|
||||
idx int
|
||||
val float64
|
||||
@@ -221,11 +227,16 @@ func renderGPUCard(gpus []GPUStatus) cardData {
|
||||
lines = append(lines, subtleStyle.Render("No GPU detected"))
|
||||
} else {
|
||||
for _, g := range gpus {
|
||||
name := shorten(g.Name, 12)
|
||||
if g.Usage >= 0 {
|
||||
lines = append(lines, fmt.Sprintf("%-12s %s %5.1f%%", name, progressBar(g.Usage), g.Usage))
|
||||
} else {
|
||||
lines = append(lines, name)
|
||||
lines = append(lines, fmt.Sprintf("Total %s %5.1f%%", progressBar(g.Usage), g.Usage))
|
||||
}
|
||||
coreInfo := ""
|
||||
if g.CoreCount > 0 {
|
||||
coreInfo = fmt.Sprintf(" (%d cores)", g.CoreCount)
|
||||
}
|
||||
lines = append(lines, subtleStyle.Render(g.Name+coreInfo))
|
||||
if g.Usage < 0 {
|
||||
lines = append(lines, subtleStyle.Render("Run with sudo for usage metrics"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user