mirror of
https://github.com/tw93/Mole.git
synced 2026-02-15 18:05:05 +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
|
Load15 float64
|
||||||
CoreCount int
|
CoreCount int
|
||||||
LogicalCPU int
|
LogicalCPU int
|
||||||
|
PCoreCount int // Performance cores (Apple Silicon)
|
||||||
|
ECoreCount int // Efficiency cores (Apple Silicon)
|
||||||
}
|
}
|
||||||
|
|
||||||
type GPUStatus struct {
|
type GPUStatus struct {
|
||||||
@@ -70,6 +72,7 @@ type GPUStatus struct {
|
|||||||
Usage float64
|
Usage float64
|
||||||
MemoryUsed float64
|
MemoryUsed float64
|
||||||
MemoryTotal float64
|
MemoryTotal float64
|
||||||
|
CoreCount int
|
||||||
Note string
|
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{
|
return CPUStatus{
|
||||||
Usage: totalPercent,
|
Usage: totalPercent,
|
||||||
PerCore: percents,
|
PerCore: percents,
|
||||||
@@ -72,6 +75,8 @@ func collectCPU() (CPUStatus, error) {
|
|||||||
Load15: loadAvg.Load15,
|
Load15: loadAvg.Load15,
|
||||||
CoreCount: counts,
|
CoreCount: counts,
|
||||||
LogicalCPU: logical,
|
LogicalCPU: logical,
|
||||||
|
PCoreCount: pCores,
|
||||||
|
ECoreCount: eCores,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +84,55 @@ func isZeroLoad(avg load.AvgStat) bool {
|
|||||||
return avg.Load1 == 0 && avg.Load5 == 0 && avg.Load15 == 0
|
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) {
|
func fallbackLoadAvgFromUptime() (load.AvgStat, error) {
|
||||||
if !commandExists("uptime") {
|
if !commandExists("uptime") {
|
||||||
return load.AvgStat{}, errors.New("uptime command unavailable")
|
return load.AvgStat{}, errors.New("uptime command unavailable")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -13,17 +14,35 @@ import (
|
|||||||
const (
|
const (
|
||||||
systemProfilerTimeout = 4 * time.Second
|
systemProfilerTimeout = 4 * time.Second
|
||||||
macGPUInfoTTL = 10 * time.Minute
|
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) {
|
func (c *Collector) collectGPU(now time.Time) ([]GPUStatus, error) {
|
||||||
if runtime.GOOS == "darwin" {
|
if runtime.GOOS == "darwin" {
|
||||||
if len(c.cachedGPU) > 0 && !c.lastGPUAt.IsZero() && now.Sub(c.lastGPUAt) < macGPUInfoTTL {
|
// Get static GPU info (cached for 10 min)
|
||||||
return c.cachedGPU, nil
|
if len(c.cachedGPU) == 0 || c.lastGPUAt.IsZero() || now.Sub(c.lastGPUAt) >= macGPUInfoTTL {
|
||||||
}
|
|
||||||
if gpus, err := readMacGPUInfo(); err == nil && len(gpus) > 0 {
|
if gpus, err := readMacGPUInfo(); err == nil && len(gpus) > 0 {
|
||||||
c.cachedGPU = gpus
|
c.cachedGPU = gpus
|
||||||
c.lastGPUAt = now
|
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"`
|
VRAM string `json:"spdisplays_vram"`
|
||||||
Vendor string `json:"spdisplays_vendor"`
|
Vendor string `json:"spdisplays_vendor"`
|
||||||
Metal string `json:"spdisplays_metal"`
|
Metal string `json:"spdisplays_metal"`
|
||||||
|
Cores string `json:"sppci_cores"`
|
||||||
} `json:"SPDisplaysDataType"`
|
} `json:"SPDisplaysDataType"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal([]byte(out), &data); err != nil {
|
if err := json.Unmarshal([]byte(out), &data); err != nil {
|
||||||
@@ -113,9 +133,11 @@ func readMacGPUInfo() ([]GPUStatus, error) {
|
|||||||
noteParts = append(noteParts, d.Vendor)
|
noteParts = append(noteParts, d.Vendor)
|
||||||
}
|
}
|
||||||
note := strings.Join(noteParts, " · ")
|
note := strings.Join(noteParts, " · ")
|
||||||
|
coreCount, _ := strconv.Atoi(d.Cores)
|
||||||
gpus = append(gpus, GPUStatus{
|
gpus = append(gpus, GPUStatus{
|
||||||
Name: d.Name,
|
Name: d.Name,
|
||||||
Usage: -1,
|
Usage: -1, // Will be updated with real-time data
|
||||||
|
CoreCount: coreCount,
|
||||||
Note: note,
|
Note: note,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -129,3 +151,36 @@ func readMacGPUInfo() ([]GPUStatus, error) {
|
|||||||
|
|
||||||
return gpus, nil
|
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),
|
renderProcessCard(m.TopProcesses),
|
||||||
renderNetworkCard(m.Network, m.Proxy),
|
renderNetworkCard(m.Network, m.Proxy),
|
||||||
}
|
}
|
||||||
// Only show GPU card if there are GPUs with usage data
|
// Only show GPU card if there are GPUs with usage data or core count
|
||||||
if len(m.GPU) > 0 && m.GPU[0].Usage >= 0 {
|
if len(m.GPU) > 0 && (m.GPU[0].Usage >= 0 || m.GPU[0].CoreCount > 0) {
|
||||||
cards = append(cards, renderGPUCard(m.GPU))
|
cards = append(cards, renderGPUCard(m.GPU))
|
||||||
}
|
}
|
||||||
// Only show sensors if we have valid temperature readings
|
// Only show sensors if we have valid temperature readings
|
||||||
@@ -186,12 +186,18 @@ func hasSensorData(sensors []SensorReading) bool {
|
|||||||
func renderCPUCard(cpu CPUStatus) cardData {
|
func renderCPUCard(cpu CPUStatus) cardData {
|
||||||
var lines []string
|
var lines []string
|
||||||
lines = append(lines, fmt.Sprintf("Total %s %5.1f%%", progressBar(cpu.Usage), cpu.Usage))
|
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 {
|
if cpu.PerCoreEstimated {
|
||||||
lines = append(lines, subtleStyle.Render("Per-core data unavailable (using averaged load)"))
|
lines = append(lines, subtleStyle.Render("Per-core data unavailable (using averaged load)"))
|
||||||
} else if len(cpu.PerCore) > 0 {
|
} else if len(cpu.PerCore) > 0 {
|
||||||
// Show top 3 busiest cores
|
|
||||||
type coreUsage struct {
|
type coreUsage struct {
|
||||||
idx int
|
idx int
|
||||||
val float64
|
val float64
|
||||||
@@ -221,11 +227,16 @@ func renderGPUCard(gpus []GPUStatus) cardData {
|
|||||||
lines = append(lines, subtleStyle.Render("No GPU detected"))
|
lines = append(lines, subtleStyle.Render("No GPU detected"))
|
||||||
} else {
|
} else {
|
||||||
for _, g := range gpus {
|
for _, g := range gpus {
|
||||||
name := shorten(g.Name, 12)
|
|
||||||
if g.Usage >= 0 {
|
if g.Usage >= 0 {
|
||||||
lines = append(lines, fmt.Sprintf("%-12s %s %5.1f%%", name, progressBar(g.Usage), g.Usage))
|
lines = append(lines, fmt.Sprintf("Total %s %5.1f%%", progressBar(g.Usage), g.Usage))
|
||||||
} else {
|
}
|
||||||
lines = append(lines, name)
|
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