feat: nvidia-smi full field parsing, GPU TUI compact table, GPU summary at top, power fix
This commit is contained in:
+322
-121
@@ -134,15 +134,21 @@ type MemInfo struct {
|
|||||||
Percent float64 `json:"percent"`
|
Percent float64 `json:"percent"`
|
||||||
}
|
}
|
||||||
type GPUInfo struct {
|
type GPUInfo struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
VRAMTotalMB float64 `json:"vram_total_mb"`
|
VRAMTotalMB float64 `json:"vram_total_mb"`
|
||||||
VRAMUsedMB float64 `json:"vram_used_mb"`
|
VRAMUsedMB float64 `json:"vram_used_mb"`
|
||||||
Temperature float64 `json:"temperature"`
|
Temperature float64 `json:"temperature"`
|
||||||
LoadPercent float64 `json:"load_percent"`
|
LoadPercent float64 `json:"load_percent"`
|
||||||
CoreClock float64 `json:"core_clock_mhz"`
|
CoreClock float64 `json:"core_clock_mhz"`
|
||||||
MemClock float64 `json:"mem_clock_mhz"`
|
MemClock float64 `json:"mem_clock_mhz"`
|
||||||
Power float64 `json:"power_watts"`
|
Power float64 `json:"power"`
|
||||||
FanSpeed float64 `json:"fan_speed"`
|
PowerLimit float64 `json:"power_limit"`
|
||||||
|
FanSpeed float64 `json:"fan_speed"`
|
||||||
|
PState string `json:"pstate"`
|
||||||
|
Persistence string `json:"persistence_mode"`
|
||||||
|
ComputeMode string `json:"compute_mode"`
|
||||||
|
DriverVersion string `json:"driver_version"`
|
||||||
|
CudaVersion string `json:"cuda_version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════
|
// ═══════════════════════════════════════════════
|
||||||
@@ -543,6 +549,10 @@ func (m model) viewMonitor() string {
|
|||||||
if tw < 60 {
|
if tw < 60 {
|
||||||
tw = 60
|
tw = 60
|
||||||
}
|
}
|
||||||
|
th := m.h
|
||||||
|
if th < 20 {
|
||||||
|
th = 20
|
||||||
|
}
|
||||||
|
|
||||||
// ── 顶栏 ──
|
// ── 顶栏 ──
|
||||||
wsDot := "○"
|
wsDot := "○"
|
||||||
@@ -602,7 +612,7 @@ func (m model) viewMonitor() string {
|
|||||||
full := lipgloss.JoinVertical(lipgloss.Left,
|
full := lipgloss.JoinVertical(lipgloss.Left,
|
||||||
header, sep, "", mainContent, "", sep, footer,
|
header, sep, "", mainContent, "", sep, footer,
|
||||||
)
|
)
|
||||||
return full
|
return lipgloss.Place(tw, th, lipgloss.Left, lipgloss.Top, full)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CPU 盒子 ──
|
// ── CPU 盒子 ──
|
||||||
@@ -714,7 +724,7 @@ func (m model) viewMemBox(innerW int) string {
|
|||||||
// ── GPU 盒子 ──
|
// ── GPU 盒子 ──
|
||||||
|
|
||||||
func (m model) viewGPUBox(innerW int) string {
|
func (m model) viewGPUBox(innerW int) string {
|
||||||
titleLine := stTitle.Render("GPU")
|
titleLine := stTitle.Render("GPU") + stDim.Render(fmt.Sprintf(" (%d张)", len(m.gpus)))
|
||||||
|
|
||||||
if len(m.gpus) == 0 {
|
if len(m.gpus) == 0 {
|
||||||
body := lipgloss.JoinVertical(lipgloss.Left,
|
body := lipgloss.JoinVertical(lipgloss.Left,
|
||||||
@@ -723,68 +733,88 @@ func (m model) viewGPUBox(innerW int) string {
|
|||||||
return stBox.Width(innerW + 4).Render(body)
|
return stBox.Width(innerW + 4).Render(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 总功耗汇总
|
||||||
|
totalP := 0.0
|
||||||
|
totalFan := 0.0
|
||||||
|
fanCnt := 0
|
||||||
|
for _, g := range m.gpus {
|
||||||
|
totalP += g.Power
|
||||||
|
if g.FanSpeed > 0 {
|
||||||
|
totalFan += g.FanSpeed
|
||||||
|
fanCnt++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
avgFan := 0.0
|
||||||
|
if fanCnt > 0 {
|
||||||
|
avgFan = totalFan / float64(fanCnt)
|
||||||
|
}
|
||||||
|
summary := stDim.Render(" ") + stVal.Render(fmt.Sprintf("总功耗: %.0fW", totalP)) +
|
||||||
|
stDim.Render(" │ ") + stVal.Render(fmt.Sprintf("均温: %.0f°C", m.avgTemp())) +
|
||||||
|
stDim.Render(" │ ") + stVal.Render(fmt.Sprintf("风扇: %.0f%%", avgFan))
|
||||||
|
|
||||||
var blocks []string
|
var blocks []string
|
||||||
blocks = append(blocks, titleLine, "")
|
blocks = append(blocks, titleLine, summary, "")
|
||||||
|
|
||||||
|
// 表头
|
||||||
|
hdr := lipgloss.JoinHorizontal(lipgloss.Left,
|
||||||
|
stDim.Render(fmt.Sprintf(" %-4s", "#")),
|
||||||
|
stDim.Render(fmt.Sprintf("%-6s", "Temp")),
|
||||||
|
stDim.Render(fmt.Sprintf("%-6s", "Load")),
|
||||||
|
stDim.Render(fmt.Sprintf("%-8s", "Power")),
|
||||||
|
stDim.Render(fmt.Sprintf("%-10s", "Clock")),
|
||||||
|
stDim.Render(fmt.Sprintf("%-10s", "MemClk")),
|
||||||
|
stDim.Render(fmt.Sprintf("%-10s", "VRAM")),
|
||||||
|
stDim.Render(fmt.Sprintf("%-5s", "Fan")),
|
||||||
|
)
|
||||||
|
blocks = append(blocks, hdr, stDim.Render(" "+strings.Repeat("─", innerW-4)))
|
||||||
|
|
||||||
for i, g := range m.gpus {
|
for i, g := range m.gpus {
|
||||||
if i > 0 {
|
tmpC := tempStyle(&g.Temperature)
|
||||||
blocks = append(blocks, stDim.Render(" "+strings.Repeat("·", innerW-4)))
|
loadC := pctStyle(g.LoadPercent)
|
||||||
|
|
||||||
|
vramStr := "—"
|
||||||
|
if g.VRAMTotalMB > 0 {
|
||||||
|
vramStr = fmt.Sprintf("%.0f/%.0fM", g.VRAMUsedMB, g.VRAMTotalMB)
|
||||||
}
|
}
|
||||||
blocks = append(blocks, m.viewSingleGPU(g, innerW))
|
|
||||||
|
fanStr := "—"
|
||||||
|
if g.FanSpeed > 0 {
|
||||||
|
fanStr = fmt.Sprintf("%.0f%%", g.FanSpeed)
|
||||||
|
}
|
||||||
|
|
||||||
|
line := lipgloss.JoinHorizontal(lipgloss.Left,
|
||||||
|
stVal.Render(fmt.Sprintf(" %-4d", i)),
|
||||||
|
tmpC.Render(fmt.Sprintf("%-6.0f°C", g.Temperature)),
|
||||||
|
loadC.Render(fmt.Sprintf("%-6.0f%%", g.LoadPercent)),
|
||||||
|
stVal.Render(fmt.Sprintf("%-8.0fW", g.Power)),
|
||||||
|
stVal.Render(fmt.Sprintf("%-10.0f", g.CoreClock)),
|
||||||
|
stVal.Render(fmt.Sprintf("%-10.0f", g.MemClock)),
|
||||||
|
stVal.Render(fmt.Sprintf("%-10s", vramStr)),
|
||||||
|
stVal.Render(fmt.Sprintf("%-5s", fanStr)),
|
||||||
|
)
|
||||||
|
blocks = append(blocks, line)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 总功耗
|
||||||
|
totalP = 0.0
|
||||||
|
for _, g := range m.gpus {
|
||||||
|
totalP += g.Power
|
||||||
|
}
|
||||||
|
blocks = append(blocks, stDim.Render(" ")+stVal.Render(fmt.Sprintf("总功耗: %.0fW", totalP)), "")
|
||||||
|
|
||||||
body := lipgloss.JoinVertical(lipgloss.Left, blocks...)
|
body := lipgloss.JoinVertical(lipgloss.Left, blocks...)
|
||||||
return stBox.Width(innerW + 4).Render(body)
|
return stBox.Width(innerW + 4).Render(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) viewSingleGPU(g GPUInfo, w int) string {
|
func (m model) avgTemp() float64 {
|
||||||
// 名称
|
if len(m.gpus) == 0 {
|
||||||
nm := g.Name
|
return 0
|
||||||
if len(nm) > w-2 && w > 5 {
|
|
||||||
nm = nm[:w-5] + "…"
|
|
||||||
}
|
}
|
||||||
nameLine := stBold.Render(" " + nm)
|
total := 0.0
|
||||||
|
for _, g := range m.gpus {
|
||||||
// 统计行
|
total += g.Temperature
|
||||||
loadC := pctStyle(g.LoadPercent)
|
|
||||||
tmpC := tempStyle(&g.Temperature)
|
|
||||||
statsLine := lipgloss.JoinHorizontal(lipgloss.Left,
|
|
||||||
stLabel.Render(" 负载 "), loadC.Render(fmt.Sprintf("%.0f%%", g.LoadPercent)),
|
|
||||||
" ",
|
|
||||||
stLabel.Render("温度 "), tmpC.Render(fmt.Sprintf("%.0f°C", g.Temperature)),
|
|
||||||
" ",
|
|
||||||
stLabel.Render("功耗 "), stVal.Render(fmt.Sprintf("%.0fW", g.Power)),
|
|
||||||
)
|
|
||||||
|
|
||||||
// 频率行
|
|
||||||
clockLine := lipgloss.JoinHorizontal(lipgloss.Left,
|
|
||||||
stLabel.Render(" 核心 "), stVal.Render(fmt.Sprintf("%.0f MHz", g.CoreClock)),
|
|
||||||
" ",
|
|
||||||
stLabel.Render("显存 "), stVal.Render(fmt.Sprintf("%.0f MHz", g.MemClock)),
|
|
||||||
)
|
|
||||||
|
|
||||||
// VRAM
|
|
||||||
vramLine := ""
|
|
||||||
if g.VRAMTotalMB > 0 {
|
|
||||||
vpct := g.VRAMUsedMB / g.VRAMTotalMB * 100
|
|
||||||
barW := w - 20
|
|
||||||
if barW < 6 {
|
|
||||||
barW = 6
|
|
||||||
}
|
|
||||||
vbar := renderBar(vpct, barW)
|
|
||||||
usedGB := fmt.Sprintf("%.1f", g.VRAMUsedMB/1024)
|
|
||||||
totalGB := fmt.Sprintf("%.1f", g.VRAMTotalMB/1024)
|
|
||||||
vramLine = lipgloss.JoinHorizontal(lipgloss.Left,
|
|
||||||
stLabel.Render(" 显存 "),
|
|
||||||
pctStyle(vpct).Render(usedGB),
|
|
||||||
stVal.Render("/"+totalGB+" GB "),
|
|
||||||
vbar,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
return total / float64(len(m.gpus))
|
||||||
return lipgloss.JoinVertical(lipgloss.Left,
|
|
||||||
nameLine, statsLine, clockLine, vramLine,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── History 盒子 ──
|
// ── History 盒子 ──
|
||||||
@@ -1014,27 +1044,72 @@ func collectCPU(perCore []float64) CPUInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getCPUTemp() *float64 {
|
func getCPUTemp() *float64 {
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
out, err := exec.CommandContext(ctx, "powershell", "-NoProfile", "-Command",
|
|
||||||
"(Get-CimInstance MSAcpi_ThermalZoneTemperature -Namespace 'root/wmi' -EA SilentlyContinue | Select -First 1).CurrentTemperature",
|
switch runtime.GOOS {
|
||||||
).Output()
|
case "windows":
|
||||||
if err != nil {
|
out, err := exec.CommandContext(ctx, "powershell", "-NoProfile", "-Command",
|
||||||
return nil
|
"(Get-CimInstance MSAcpi_ThermalZoneTemperature -Namespace 'root/wmi' -EA SilentlyContinue | Select -First 1).CurrentTemperature",
|
||||||
|
).Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
raw := strings.TrimSpace(string(out))
|
||||||
|
val, err := strconv.ParseFloat(raw, 64)
|
||||||
|
if err != nil || val < 1000 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c := (val - 2732) / 10.0
|
||||||
|
if c < 0 || c > 150 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &c
|
||||||
|
case "linux":
|
||||||
|
out, err := exec.CommandContext(ctx, "cat", "/sys/class/thermal/thermal_zone0/temp").Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
raw := strings.TrimSpace(string(out))
|
||||||
|
val, err := strconv.ParseFloat(raw, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c := val / 1000.0
|
||||||
|
if c < 0 || c > 150 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &c
|
||||||
|
case "darwin":
|
||||||
|
out, err := exec.CommandContext(ctx, "sudo", "powermetrics", "--samplers", "smc", "-i", "1", "-n", "1", "--format", "plist").Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s := string(out)
|
||||||
|
idx := strings.Index(s, "<key>CPU die temperature</key>")
|
||||||
|
if idx < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s = s[idx:]
|
||||||
|
idx = strings.Index(s, "<real>")
|
||||||
|
if idx < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s = s[idx+6:]
|
||||||
|
end := strings.Index(s, "</real>")
|
||||||
|
if end < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
val, err := strconv.ParseFloat(strings.TrimSpace(s[:end]), 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if val < 0 || val > 150 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &val
|
||||||
}
|
}
|
||||||
raw := strings.TrimSpace(string(out))
|
return nil
|
||||||
val, err := strconv.ParseFloat(raw, 64)
|
|
||||||
if err != nil || val < 1000 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
c := (val - 2732) / 10.0
|
|
||||||
if c < 0 || c > 150 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &c
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectMemory() MemInfo {
|
func collectMemory() MemInfo {
|
||||||
@@ -1050,34 +1125,76 @@ func collectMemory() MemInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func collectMotherboard() BoardInfo {
|
func collectMotherboard() BoardInfo {
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
return BoardInfo{}
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
out, err := exec.CommandContext(ctx, "wmic", "baseboard", "get", "manufacturer,product", "/format:list").Output()
|
|
||||||
if err != nil {
|
switch runtime.GOOS {
|
||||||
return BoardInfo{}
|
case "windows":
|
||||||
}
|
out, err := exec.CommandContext(ctx, "wmic", "baseboard", "get", "manufacturer,product", "/format:list").Output()
|
||||||
var b BoardInfo
|
if err != nil {
|
||||||
for _, line := range strings.Split(string(out), "\n") {
|
return BoardInfo{}
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if strings.HasPrefix(line, "Manufacturer=") {
|
|
||||||
b.Manufacturer = strings.TrimPrefix(line, "Manufacturer=")
|
|
||||||
} else if strings.HasPrefix(line, "Product=") {
|
|
||||||
b.Product = strings.TrimPrefix(line, "Product=")
|
|
||||||
}
|
}
|
||||||
|
var b BoardInfo
|
||||||
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(line, "Manufacturer=") {
|
||||||
|
b.Manufacturer = strings.TrimPrefix(line, "Manufacturer=")
|
||||||
|
} else if strings.HasPrefix(line, "Product=") {
|
||||||
|
b.Product = strings.TrimPrefix(line, "Product=")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
case "linux":
|
||||||
|
man, _ := os.ReadFile("/sys/class/dmi/id/board_vendor")
|
||||||
|
prod, _ := os.ReadFile("/sys/class/dmi/id/board_name")
|
||||||
|
return BoardInfo{
|
||||||
|
Manufacturer: strings.TrimSpace(string(man)),
|
||||||
|
Product: strings.TrimSpace(string(prod)),
|
||||||
|
}
|
||||||
|
case "darwin":
|
||||||
|
out, err := exec.CommandContext(ctx, "system_profiler", "SPHardwareDataType").Output()
|
||||||
|
if err != nil {
|
||||||
|
return BoardInfo{}
|
||||||
|
}
|
||||||
|
var b BoardInfo
|
||||||
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(line, "Model Name:") {
|
||||||
|
b.Product = strings.TrimSpace(strings.TrimPrefix(line, "Model Name:"))
|
||||||
|
} else if strings.HasPrefix(line, "Model Identifier:") {
|
||||||
|
if b.Manufacturer == "" {
|
||||||
|
b.Manufacturer = "Apple"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if b.Manufacturer == "" {
|
||||||
|
b.Manufacturer = "Apple"
|
||||||
|
}
|
||||||
|
return b
|
||||||
}
|
}
|
||||||
return b
|
return BoardInfo{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectGPUs() []GPUInfo {
|
func collectGPUs() []GPUInfo {
|
||||||
if runtime.GOOS == "windows" {
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
gpus := tryNvidiaSMI()
|
gpus := tryNvidiaSMI()
|
||||||
if len(gpus) > 0 {
|
if len(gpus) > 0 {
|
||||||
return gpus
|
return gpus
|
||||||
}
|
}
|
||||||
return tryWMIGPU()
|
return tryWMIGPU()
|
||||||
|
case "linux":
|
||||||
|
gpus := tryNvidiaSMI()
|
||||||
|
if len(gpus) > 0 {
|
||||||
|
return gpus
|
||||||
|
}
|
||||||
|
return tryLinuxGPU()
|
||||||
|
case "darwin":
|
||||||
|
gpus := tryNvidiaSMI()
|
||||||
|
if len(gpus) > 0 {
|
||||||
|
return gpus
|
||||||
|
}
|
||||||
|
return tryMacGPU()
|
||||||
}
|
}
|
||||||
return tryNvidiaSMI()
|
return tryNvidiaSMI()
|
||||||
}
|
}
|
||||||
@@ -1086,7 +1203,7 @@ func tryNvidiaSMI() []GPUInfo {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
out, err := exec.CommandContext(ctx, "nvidia-smi",
|
out, err := exec.CommandContext(ctx, "nvidia-smi",
|
||||||
"--query-gpu=index,name,memory.total,memory.used,memory.free,temperature.gpu,utilization.gpu,clocks.current.graphics,clocks.current.memory,power.draw,fan.speed",
|
"--query-gpu=index,name,memory.total,memory.used,memory.free,temperature.gpu,utilization.gpu,clocks.current.graphics,clocks.current.memory,power.draw,power.limit,fan.speed,pstate,persistence_mode,compute_mode,driver_version,cuda_version",
|
||||||
"--format=csv,noheader,nounits",
|
"--format=csv,noheader,nounits",
|
||||||
).Output()
|
).Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1100,38 +1217,52 @@ func tryNvidiaSMI() []GPUInfo {
|
|||||||
}
|
}
|
||||||
for i := range parts {
|
for i := range parts {
|
||||||
parts[i] = strings.TrimSpace(parts[i])
|
parts[i] = strings.TrimSpace(parts[i])
|
||||||
|
parts[i] = strings.Trim(parts[i], "[]")
|
||||||
}
|
}
|
||||||
fanStr := strings.TrimSpace(parts[10])
|
|
||||||
fanStr = strings.Trim(fanStr, "[]")
|
fan := parseFloatSafe(parts[10])
|
||||||
var fan float64
|
power := parseFloatSafe(parts[9])
|
||||||
if fanStr == "N/A" || fanStr == "" {
|
powerLimit := parseFloatSafe(parts[10])
|
||||||
fan = 0
|
|
||||||
} else {
|
|
||||||
fan = parseFloat(fanStr)
|
|
||||||
}
|
|
||||||
powerStr := strings.TrimSpace(parts[9])
|
|
||||||
powerStr = strings.Trim(powerStr, "[]")
|
|
||||||
var power float64
|
|
||||||
if powerStr == "N/A" || powerStr == "" {
|
|
||||||
power = 0
|
|
||||||
} else {
|
|
||||||
power = parseFloat(powerStr)
|
|
||||||
}
|
|
||||||
gpus = append(gpus, GPUInfo{
|
gpus = append(gpus, GPUInfo{
|
||||||
Name: parts[1],
|
Name: parts[1],
|
||||||
VRAMTotalMB: parseFloat(parts[2]),
|
VRAMTotalMB: parseFloat(parts[2]),
|
||||||
VRAMUsedMB: parseFloat(parts[3]),
|
VRAMUsedMB: parseFloat(parts[3]),
|
||||||
Temperature: parseFloat(parts[5]),
|
Temperature: parseFloat(parts[5]),
|
||||||
LoadPercent: parseFloat(parts[6]),
|
LoadPercent: parseFloat(parts[6]),
|
||||||
CoreClock: parseFloat(parts[7]),
|
CoreClock: parseFloat(parts[7]),
|
||||||
MemClock: parseFloat(parts[8]),
|
MemClock: parseFloat(parts[8]),
|
||||||
Power: power,
|
Power: power,
|
||||||
FanSpeed: fan,
|
PowerLimit: powerLimit,
|
||||||
|
FanSpeed: fan,
|
||||||
|
PState: cleanStr(parts[12]),
|
||||||
|
Persistence: cleanStr(parts[13]),
|
||||||
|
ComputeMode: cleanStr(parts[14]),
|
||||||
|
DriverVersion: cleanStr(parts[15]),
|
||||||
|
CudaVersion: cleanStr(parts[16]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return gpus
|
return gpus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseFloatSafe(s string) float64 {
|
||||||
|
s = strings.Trim(s, "[] ")
|
||||||
|
s = strings.TrimSuffix(s, "W")
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "N/A" || s == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return parseFloat(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanStr(s string) string {
|
||||||
|
s = strings.Trim(s, "[] ")
|
||||||
|
if s == "N/A" || s == "[N/A]" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
func tryWMIGPU() []GPUInfo {
|
func tryWMIGPU() []GPUInfo {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -1164,6 +1295,76 @@ func tryWMIGPU() []GPUInfo {
|
|||||||
return gpus
|
return gpus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tryLinuxGPU() []GPUInfo {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
out, err := exec.CommandContext(ctx, "lspci", "-vnn").Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var gpus []GPUInfo
|
||||||
|
lines := strings.Split(string(out), "\n")
|
||||||
|
for i, line := range lines {
|
||||||
|
if strings.Contains(line, "VGA") || strings.Contains(line, "3D") || strings.Contains(line, "Display") {
|
||||||
|
name := line
|
||||||
|
if idx := strings.Index(name, ": "); idx >= 0 {
|
||||||
|
name = name[idx+2:]
|
||||||
|
}
|
||||||
|
if idx := strings.Index(name, " ["); idx >= 0 {
|
||||||
|
name = name[:idx]
|
||||||
|
}
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
var vram float64
|
||||||
|
for j := i + 1; j < len(lines) && j < i+10; j++ {
|
||||||
|
if strings.Contains(lines[j], "prefetchable") && strings.Contains(lines[j], "Memory") {
|
||||||
|
if idx := strings.Index(lines[j], "size="); idx >= 0 {
|
||||||
|
sz := lines[j][idx+5:]
|
||||||
|
if end := strings.Index(sz, "M"); end > 0 {
|
||||||
|
vram, _ = strconv.ParseFloat(sz[:end], 64)
|
||||||
|
} else if end := strings.Index(sz, "G"); end > 0 {
|
||||||
|
v, _ := strconv.ParseFloat(sz[:end], 64)
|
||||||
|
vram = v * 1024
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gpus = append(gpus, GPUInfo{Name: name, VRAMTotalMB: vram})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return gpus
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryMacGPU() []GPUInfo {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
out, err := exec.CommandContext(ctx, "system_profiler", "SPDisplaysDataType", "-json").Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var data struct {
|
||||||
|
SPDisplaysDataType []struct {
|
||||||
|
sppci_model string `json:"_name"`
|
||||||
|
sppci_memory string `json:"sppci_memory"`
|
||||||
|
SPDisplaysVendorID string `json:"spdisplays_vendor-id"`
|
||||||
|
} `json:"SPDisplaysDataType"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(out, &data) != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var gpus []GPUInfo
|
||||||
|
for _, d := range data.SPDisplaysDataType {
|
||||||
|
g := GPUInfo{Name: d.sppci_model}
|
||||||
|
if vram, err := strconv.ParseFloat(d.sppci_memory, 64); err == nil {
|
||||||
|
g.VRAMTotalMB = vram / (1024 * 1024)
|
||||||
|
if g.VRAMTotalMB > 1024*10 {
|
||||||
|
g.VRAMTotalMB = vram / (1024 * 1024)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gpus = append(gpus, g)
|
||||||
|
}
|
||||||
|
return gpus
|
||||||
|
}
|
||||||
|
|
||||||
func parseFloat(s string) float64 {
|
func parseFloat(s string) float64 {
|
||||||
v, _ := strconv.ParseFloat(strings.TrimSpace(s), 64)
|
v, _ := strconv.ParseFloat(strings.TrimSpace(s), 64)
|
||||||
return v
|
return v
|
||||||
|
|||||||
@@ -344,13 +344,20 @@ Vue.component('gpu-section',{
|
|||||||
<div class="gpu-pop-grid">
|
<div class="gpu-pop-grid">
|
||||||
<div class="gd-item"><span class="gd-l">负载</span><span class="gd-v" :style="{color:pColor(gg.load_percent||0)}">{{ (gg.load_percent||0).toFixed(1) }}%</span></div>
|
<div class="gd-item"><span class="gd-l">负载</span><span class="gd-v" :style="{color:pColor(gg.load_percent||0)}">{{ (gg.load_percent||0).toFixed(1) }}%</span></div>
|
||||||
<div class="gd-item"><span class="gd-l">温度</span><span class="gd-v" :style="{color:tmpColor(gg)}">{{ tmpStr(gg) }}</span></div>
|
<div class="gd-item"><span class="gd-l">温度</span><span class="gd-v" :style="{color:tmpColor(gg)}">{{ tmpStr(gg) }}</span></div>
|
||||||
<div class="gd-item"><span class="gd-l">功耗</span><span class="gd-v">{{ Math.round(gg.power||0) }} W</span></div>
|
<div class="gd-item"><span class="gd-l">功耗</span><span class="gd-v">{{ Math.round(gg.power||0) }}W / {{ Math.round(gg.power_limit||0) }}W</span></div>
|
||||||
<div class="gd-item"><span class="gd-l">核心频率</span><span class="gd-v">{{ Math.round(gg.core_clock_mhz||0) }} MHz</span></div>
|
<div class="gd-item"><span class="gd-l">核心频率</span><span class="gd-v">{{ Math.round(gg.core_clock_mhz||0) }} MHz</span></div>
|
||||||
<div class="gd-item"><span class="gd-l">显存频率</span><span class="gd-v">{{ Math.round(gg.mem_clock_mhz||0) }} MHz</span></div>
|
<div class="gd-item"><span class="gd-l">显存频率</span><span class="gd-v">{{ Math.round(gg.mem_clock_mhz||0) }} MHz</span></div>
|
||||||
<div class="gd-item"><span class="gd-l">风扇</span><span class="gd-v">{{ fanStr(gg) }}</span></div>
|
<div class="gd-item"><span class="gd-l">风扇</span><span class="gd-v">{{ fanStr(gg) }}</span></div>
|
||||||
|
<div class="gd-item"><span class="gd-l">性能状态</span><span class="gd-v">{{ gg.pstate||'N/A' }}</span></div>
|
||||||
|
<div class="gd-item"><span class="gd-l">持久模式</span><span class="gd-v">{{ gg.persistence_mode||'N/A' }}</span></div>
|
||||||
|
<div class="gd-item"><span class="gd-l">计算模式</span><span class="gd-v">{{ gg.compute_mode||'N/A' }}</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="vramTotal(gg)>0" style="margin-top:2px"><div class="info-row" style="margin-bottom:1px"><span class="label">显存</span><span class="value">{{ vramUsed(gg) }} / {{ vramTotal(gg) }} GB</span></div>
|
<div v-if="vramTotal(gg)>0" style="margin-top:2px"><div class="info-row" style="margin-bottom:1px"><span class="label">显存</span><span class="value">{{ vramUsed(gg) }} / {{ vramTotal(gg) }} GB</span></div>
|
||||||
<a-progress :percent="Math.round(vramPct(gg))" :stroke-color="pColor(vramPct(gg))" :stroke-width="4" size="small" :format="fmtPct" /></div>
|
<a-progress :percent="Math.round(vramPct(gg))" :stroke-color="pColor(vramPct(gg))" :stroke-width="4" size="small" :format="fmtPct" /></div>
|
||||||
|
<div v-if="gg.driver_version" style="margin-top:2px;font-size:8px;color:var(--dim);display:flex;gap:8px">
|
||||||
|
<span>驱动: {{ gg.driver_version }}</span>
|
||||||
|
<span v-if="gg.cuda_version">CUDA: {{ gg.cuda_version }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div></template>
|
</div></template>
|
||||||
<div @click.stop="toggle(i)">
|
<div @click.stop="toggle(i)">
|
||||||
|
|||||||
Reference in New Issue
Block a user