Compare commits

...

9 Commits

3 changed files with 1800 additions and 435 deletions
+123 -87
View File
@@ -143,12 +143,16 @@ type GPUInfo struct {
MemClock float64 `json:"mem_clock_mhz"` MemClock float64 `json:"mem_clock_mhz"`
Power float64 `json:"power"` Power float64 `json:"power"`
PowerLimit float64 `json:"power_limit"` PowerLimit float64 `json:"power_limit"`
PowerMax float64 `json:"power_max"`
FanSpeed float64 `json:"fan_speed"` FanSpeed float64 `json:"fan_speed"`
PState string `json:"pstate"` PState string `json:"pstate"`
Persistence string `json:"persistence_mode"`
ComputeMode string `json:"compute_mode"` ComputeMode string `json:"compute_mode"`
DriverVersion string `json:"driver_version"` DriverVersion string `json:"driver_version"`
CudaVersion string `json:"cuda_version"` UtilMemory float64 `json:"util_memory"`
UtilEncoder float64 `json:"util_encoder"`
UtilDecoder float64 `json:"util_decoder"`
ClocksMaxGfx float64 `json:"clocks_max_graphics"`
ClocksMaxMem float64 `json:"clocks_max_memory"`
} }
// ═══════════════════════════════════════════════ // ═══════════════════════════════════════════════
@@ -549,7 +553,8 @@ func (m model) viewMonitor() string {
if tw < 60 { if tw < 60 {
tw = 60 tw = 60
} }
innerW := tw - 4 gap := 2
halfW := (tw - gap) / 2
// ── 顶栏 ── // ── 顶栏 ──
wsDot := "○" wsDot := "○"
@@ -569,23 +574,28 @@ func (m model) viewMonitor() string {
) )
sep := stDim.Render(strings.Repeat("─", tw)) sep := stDim.Render(strings.Repeat("─", tw))
// ── 内容:全部垂直排列 ── // ── 第一行: CPU + Memory ──
cpuBox := m.viewCPUBox(innerW) cpuBox := m.viewCPUBox(halfW - 4)
memBox := m.viewMemBox(innerW) memBox := m.viewMemBox(halfW - 4)
histBox := m.viewHistBox(innerW) row1 := lipgloss.JoinHorizontal(lipgloss.Top,
gpuBox := m.viewGPUBox(innerW) cpuBox, strings.Repeat(" ", gap), memBox,
)
// ── 第二行: GPU + History ──
gpuBox := m.viewGPUBox(halfW - 4)
histBox := m.viewHistBox(halfW - 4)
row2 := lipgloss.JoinHorizontal(lipgloss.Top,
gpuBox, strings.Repeat(" ", gap), histBox,
)
full := lipgloss.JoinVertical(lipgloss.Left, full := lipgloss.JoinVertical(lipgloss.Left,
header, sep, "", header, sep, "",
cpuBox, "", row1, "",
memBox, "", row2, "",
histBox, "",
gpuBox, "",
sep, sep,
stDim.Render(" q 退出 · r 重新配置 · 连接: "+m.cfg.Server), stDim.Render(" q 退出 · r 重新配置 · 连接: "+m.cfg.Server),
) )
// 截断到终端高度,防止滚动
lines := strings.Split(full, "\n") lines := strings.Split(full, "\n")
if len(lines) > m.h { if len(lines) > m.h {
lines = lines[:m.h] lines = lines[:m.h]
@@ -599,40 +609,38 @@ func (m model) viewCPUBox(innerW int) string {
c := m.cpu c := m.cpu
pct := c.UsageTotal pct := c.UsageTotal
// 标题
modelName := c.Model modelName := c.Model
maxNameW := innerW - 10 maxNameW := innerW - 8
if len(modelName) > maxNameW && maxNameW > 3 { if len(modelName) > maxNameW && maxNameW > 3 {
modelName = modelName[:maxNameW-1] + "…" modelName = modelName[:maxNameW-1] + "…"
} }
titleLine := stTitle.Render("CPU") + " " + stFg.Render(modelName)
// 百分比进度条 // 标题 + 核心/频率/温度 一行
coresStr := fmt.Sprintf("%dC/%dT", c.CoresPhysical, c.CoresLogical)
freqStr := fmt.Sprintf("%.0fMHz", c.FreqMHz)
tempStr := tempText(c.Temperature)
titleLine := lipgloss.JoinHorizontal(lipgloss.Left,
stTitle.Render("CPU"),
" ", stFg.Render(modelName),
" ", stLabel.Render(coresStr),
" ", stVal.Render(freqStr),
" ", tempStyle(c.Temperature).Render(tempStr),
)
// 进度条 + 百分比 一行
barW := innerW - 8 barW := innerW - 8
if barW < 10 { if barW < 10 {
barW = 10 barW = 10
} }
bar := renderBar(pct, barW) bar := renderBar(pct, barW)
pctStr := pctStyle(pct).Bold(true).Render(fmt.Sprintf("%5.1f%%", pct)) pctStr := pctStyle(pct).Bold(true).Render(fmt.Sprintf("%.1f%%", pct))
barLine := lipgloss.JoinHorizontal(lipgloss.Center, bar, " ", pctStr) barLine := lipgloss.JoinHorizontal(lipgloss.Center, bar, " ", pctStr)
// 信息行
coresStr := fmt.Sprintf("%dC/%dT", c.CoresPhysical, c.CoresLogical)
freqStr := fmt.Sprintf("%.0f MHz", c.FreqMHz)
tempStr := tempText(c.Temperature)
infoLine := lipgloss.JoinHorizontal(lipgloss.Left,
stLabel.Render("核心 "), stVal.Render(coresStr),
" ",
stLabel.Render("频率 "), stVal.Render(freqStr),
" ",
stLabel.Render("温度 "), tempStyle(c.Temperature).Render(tempStr),
)
// 核心热力图 // 核心热力图
coreLine := m.viewCoreHeatmap(innerW) coreLine := m.viewCoreHeatmap(innerW)
body := lipgloss.JoinVertical(lipgloss.Left, body := lipgloss.JoinVertical(lipgloss.Left,
titleLine, "", barLine, infoLine, "", coreLine, titleLine, barLine, coreLine,
) )
return stBox.Width(innerW + 4).Render(body) return stBox.Width(innerW + 4).Render(body)
} }
@@ -671,10 +679,16 @@ func (m model) viewCoreHeatmap(innerW int) string {
func (m model) viewMemBox(innerW int) string { func (m model) viewMemBox(innerW int) string {
pct := m.mem.Percent pct := m.mem.Percent
titleLine := stTitle.Render("MEMORY") usedGB := fmtBytes(m.mem.UsedBytes)
totalGB := fmtBytes(m.mem.TotalBytes)
// 百分比 // 标题 + 详情 一行
pctStr := pctStyle(pct).Bold(true).Render(fmt.Sprintf("%.1f%%", pct)) titleLine := lipgloss.JoinHorizontal(lipgloss.Left,
stTitle.Render("MEMORY"),
" ", stLabel.Render("已用 "), stVal.Render(usedGB),
" / ", stVal.Render(totalGB),
" ", pctStyle(pct).Bold(true).Render(fmt.Sprintf("%.1f%%", pct)),
)
// 进度条 // 进度条
barW := innerW - 8 barW := innerW - 8
@@ -682,19 +696,10 @@ func (m model) viewMemBox(innerW int) string {
barW = 10 barW = 10
} }
bar := renderBar(pct, barW) bar := renderBar(pct, barW)
barLine := lipgloss.JoinHorizontal(lipgloss.Center, bar, " ", pctStr) barLine := lipgloss.JoinHorizontal(lipgloss.Center, bar)
// 详情
usedGB := fmtBytes(m.mem.UsedBytes)
totalGB := fmtBytes(m.mem.TotalBytes)
detailLine := lipgloss.JoinHorizontal(lipgloss.Left,
stLabel.Render("已用 "), stVal.Render(usedGB),
" ",
stLabel.Render("总计 "), stVal.Render(totalGB),
)
body := lipgloss.JoinVertical(lipgloss.Left, body := lipgloss.JoinVertical(lipgloss.Left,
titleLine, "", pctStr, barLine, "", detailLine, titleLine, barLine,
) )
return stBox.Width(innerW + 4).Render(body) return stBox.Width(innerW + 4).Render(body)
} }
@@ -976,6 +981,9 @@ func collectLoop(done <-chan struct{}, wsURL, name string, interval time.Duratio
case dataCh <- data: case dataCh <- data:
default: // 满了就丢弃 default: // 满了就丢弃
} }
// 每 10 轮触发一次 GC,减少内存占用
runtime.GC()
} }
} }
@@ -1160,28 +1168,62 @@ func collectGPUs() []GPUInfo {
if len(gpus) > 0 { if len(gpus) > 0 {
return gpus return gpus
} }
return tryWMIGPU() wmi := tryWMIGPU()
return filterPCIeGPUs(wmi)
case "linux": case "linux":
gpus := tryNvidiaSMI() gpus := tryNvidiaSMI()
if len(gpus) > 0 { if len(gpus) > 0 {
return gpus return gpus
} }
return tryLinuxGPU() lspci := tryLinuxGPU()
return filterPCIeGPUs(lspci)
case "darwin": case "darwin":
gpus := tryNvidiaSMI() gpus := tryNvidiaSMI()
if len(gpus) > 0 { mac := tryMacGPU()
return gpus return filterPCIeGPUs(append(gpus, mac...))
}
return tryMacGPU()
} }
return tryNvidiaSMI() return tryNvidiaSMI()
} }
func filterPCIeGPUs(gpus []GPUInfo) []GPUInfo {
var result []GPUInfo
for _, g := range gpus {
if !isVirtualGPU(g.Name) {
result = append(result, g)
}
}
return result
}
func isVirtualGPU(name string) bool {
name = strings.ToLower(name)
blacklist := []string{
"microsoft", "basic render", "vmware", "virtual", "hyper-v",
"parallels", "virtualbox", "intel", "remote", "rdp", "citrix",
"teamviewer", "anydesk", "parsec", "splashtop", "oray",
"sunlogin", "todesk", "amdgpu-pro",
}
for _, v := range blacklist {
if strings.Contains(name, v) {
return true
}
}
return false
}
func tryNvidiaSMI() []GPUInfo { 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,power.limit,fan.speed,pstate,persistence_mode,compute_mode,driver_version,cuda_version", "--query-gpu=index,name,uuid,pci.bus_id,driver_version,vbios_version,"+
"temperature.gpu,temperature.memory,fan.speed,"+
"power.draw,power.limit,power.default_limit,power.min_limit,power.max_limit,"+
"clocks.current.graphics,clocks.current.sm,clocks.current.memory,"+
"clocks.max.graphics,clocks.max.memory,"+
"utilization.gpu,utilization.memory,utilization.encoder,utilization.decoder,"+
"memory.total,memory.used,memory.free,"+
"ecc.errors.corrected.volatile.total,ecc.errors.uncorrected.volatile.total,"+
"compute_mode,pstate",
"--format=csv,noheader,nounits", "--format=csv,noheader,nounits",
).Output() ).Output()
if err != nil { if err != nil {
@@ -1190,7 +1232,7 @@ func tryNvidiaSMI() []GPUInfo {
var gpus []GPUInfo var gpus []GPUInfo
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
parts := strings.Split(line, ",") parts := strings.Split(line, ",")
if len(parts) < 9 { if len(parts) < 24 {
continue continue
} }
for i := range parts { for i := range parts {
@@ -1198,40 +1240,34 @@ func tryNvidiaSMI() []GPUInfo {
parts[i] = strings.Trim(parts[i], "[]") parts[i] = strings.Trim(parts[i], "[]")
} }
g := GPUInfo{ // 0:index 1:name 2:uuid 3:bus_id 4:driver 5:vbios
Name: parts[1], // 6:temp_gpu 7:temp_mem 8:fan 9:power 10:power_limit
VRAMTotalMB: parseFloat(parts[2]), // 11:power_default 12:power_min 13:power_max
VRAMUsedMB: parseFloat(parts[3]), // 14:clk_gfx 15:clk_sm 16:clk_mem 17:clk_max_gfx 18:clk_max_mem
Temperature: parseFloat(parts[5]), // 19:util_gpu 20:util_mem 21:util_enc 22:util_dec
LoadPercent: parseFloat(parts[6]), // 23:vram_total 24:vram_used 25:vram_free
CoreClock: parseFloat(parts[7]), // 26:ecc_corr 27:ecc_uncorr 28:compute_mode 29:pstate
MemClock: parseFloat(parts[8]), gpus = append(gpus, GPUInfo{
} Name: cleanStr(parts[1]),
if len(parts) > 9 { DriverVersion: cleanStr(parts[4]),
g.Power = parseFloatSafe(parts[9]) Temperature: parseFloat(parts[6]),
} FanSpeed: parseFloat(parts[8]),
if len(parts) > 10 { Power: parseFloat(parts[9]),
g.PowerLimit = parseFloatSafe(parts[10]) PowerLimit: parseFloat(parts[10]),
} PowerMax: parseFloat(parts[13]),
if len(parts) > 11 { CoreClock: parseFloat(parts[14]),
g.FanSpeed = parseFloatSafe(parts[11]) MemClock: parseFloat(parts[16]),
} ClocksMaxGfx: parseFloat(parts[17]),
if len(parts) > 12 { ClocksMaxMem: parseFloat(parts[18]),
g.PState = cleanStr(parts[12]) LoadPercent: parseFloat(parts[19]),
} UtilMemory: parseFloat(parts[20]),
if len(parts) > 13 { UtilEncoder: parseFloat(parts[21]),
g.Persistence = cleanStr(parts[13]) UtilDecoder: parseFloat(parts[22]),
} VRAMTotalMB: parseFloat(parts[23]),
if len(parts) > 14 { VRAMUsedMB: parseFloat(parts[24]),
g.ComputeMode = cleanStr(parts[14]) ComputeMode: cleanStr(parts[28]),
} PState: cleanStr(parts[29]),
if len(parts) > 15 { })
g.DriverVersion = cleanStr(parts[15])
}
if len(parts) > 16 {
g.CudaVersion = cleanStr(parts[16])
}
gpus = append(gpus, g)
} }
return gpus return gpus
} }
+82 -15
View File
@@ -11,6 +11,7 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"
@@ -374,6 +375,72 @@ func (h *Hub) saveYAML() {
os.WriteFile(h.devicesAbs, append([]byte(hdr), out...), 0644) os.WriteFile(h.devicesAbs, append([]byte(hdr), out...), 0644)
} }
func filterGPURaw(raw json.RawMessage) json.RawMessage {
var data map[string]interface{}
if json.Unmarshal(raw, &data) != nil {
return raw
}
gpusRaw, ok := data["gpus"]
if !ok {
return raw
}
gpus, ok := gpusRaw.([]interface{})
if !ok {
return raw
}
var filtered []interface{}
for _, g := range gpus {
gpu, ok := g.(map[string]interface{})
if !ok {
continue
}
gpuName, _ := gpu["name"].(string)
if isVirtualGPU(gpuName) {
continue
}
filtered = append(filtered, g)
}
data["gpus"] = filtered
out, _ := json.Marshal(data)
return out
}
func isVirtualGPU(name string) bool {
name = strings.ToLower(name)
blacklist := []string{
"microsoft",
"basic render",
"vmware",
"virtual",
"hyper-v",
"parallels",
"virtualbox",
"intel uhd",
"intel iris",
"intel(r) uhd",
"intel(r) iris",
"uhd graphics",
"iris graphics",
"remote",
"rdp",
"citrix",
"teamviewer",
"anydesk",
"parsec",
"splashtop",
"amdgpu-pro",
"oray",
"sunlogin",
"todesk",
}
for _, v := range blacklist {
if strings.Contains(name, v) {
return true
}
}
return false
}
func (h *Hub) UpdateDevice(raw json.RawMessage) { func (h *Hub) UpdateDevice(raw json.RawMessage) {
var p AgentPayload var p AgentPayload
if json.Unmarshal(raw, &p) != nil || p.MachineName == "" { if json.Unmarshal(raw, &p) != nil || p.MachineName == "" {
@@ -398,7 +465,7 @@ func (h *Hub) UpdateDevice(raw json.RawMessage) {
rec.GPUs = append(rec.GPUs, GPURec{Name: g.Name, VRAM: g.VRAM}) rec.GPUs = append(rec.GPUs, GPURec{Name: g.Name, VRAM: g.VRAM})
} }
cp := *rec cp := *rec
h.live[name] = raw h.live[name] = filterGPURaw(raw)
var md *MiningData var md *MiningData
var hist []HashratePoint var hist []HashratePoint
if m, ok := h.miningData[name]; ok { if m, ok := h.miningData[name]; ok {
@@ -416,10 +483,23 @@ func (h *Hub) UpdateDevice(raw json.RawMessage) {
h.saveYAML() h.saveYAML()
h.broadcast(BrowserMsg{ h.broadcast(BrowserMsg{
Type: "device", Machine: name, Type: "device", Machine: name,
Device: &BrowserDevice{Info: &cp, Live: raw, Online: true, Mining: md, History: hist}, Device: &BrowserDevice{Info: &cp, Live: filterGPURaw(raw), Online: true, Mining: md, History: hist},
}) })
} }
func (h *Hub) DeleteDevice(name string) {
h.mu.Lock()
_, ok := h.records[name]
delete(h.records, name)
delete(h.live, name)
h.mu.Unlock()
if ok {
h.saveYAML()
h.broadcast(BrowserMsg{Type: "remove", Machine: name})
logInfo("delete", "%s", name)
}
}
func (h *Hub) MarkOffline(name string) { func (h *Hub) MarkOffline(name string) {
h.mu.Lock() h.mu.Lock()
_, had := h.live[name] _, had := h.live[name]
@@ -449,19 +529,6 @@ func (h *Hub) MarkOffline(name string) {
} }
} }
func (h *Hub) DeleteDevice(name string) {
h.mu.Lock()
_, ok := h.records[name]
delete(h.records, name)
delete(h.live, name)
h.mu.Unlock()
if ok {
h.saveYAML()
h.broadcast(BrowserMsg{Type: "remove", Machine: name})
logInfo("delete", "%s", name)
}
}
func (h *Hub) AddMonitor(c *websocket.Conn) { func (h *Hub) AddMonitor(c *websocket.Conn) {
h.mu.Lock() h.mu.Lock()
h.monitors[c] = struct{}{} h.monitors[c] = struct{}{}
File diff suppressed because it is too large Load Diff