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"`
Power float64 `json:"power"`
PowerLimit float64 `json:"power_limit"`
PowerMax float64 `json:"power_max"`
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"`
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 {
tw = 60
}
innerW := tw - 4
gap := 2
halfW := (tw - gap) / 2
// ── 顶栏 ──
wsDot := "○"
@@ -569,23 +574,28 @@ func (m model) viewMonitor() string {
)
sep := stDim.Render(strings.Repeat("─", tw))
// ── 内容:全部垂直排列 ──
cpuBox := m.viewCPUBox(innerW)
memBox := m.viewMemBox(innerW)
histBox := m.viewHistBox(innerW)
gpuBox := m.viewGPUBox(innerW)
// ── 第一行: CPU + Memory ──
cpuBox := m.viewCPUBox(halfW - 4)
memBox := m.viewMemBox(halfW - 4)
row1 := lipgloss.JoinHorizontal(lipgloss.Top,
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,
header, sep, "",
cpuBox, "",
memBox, "",
histBox, "",
gpuBox, "",
row1, "",
row2, "",
sep,
stDim.Render(" q 退出 · r 重新配置 · 连接: "+m.cfg.Server),
)
// 截断到终端高度,防止滚动
lines := strings.Split(full, "\n")
if len(lines) > m.h {
lines = lines[:m.h]
@@ -599,40 +609,38 @@ func (m model) viewCPUBox(innerW int) string {
c := m.cpu
pct := c.UsageTotal
// 标题
modelName := c.Model
maxNameW := innerW - 10
maxNameW := innerW - 8
if len(modelName) > maxNameW && maxNameW > 3 {
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
if barW < 10 {
barW = 10
}
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)
// 信息行
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)
body := lipgloss.JoinVertical(lipgloss.Left,
titleLine, "", barLine, infoLine, "", coreLine,
titleLine, barLine, coreLine,
)
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 {
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
@@ -682,19 +696,10 @@ func (m model) viewMemBox(innerW int) string {
barW = 10
}
bar := renderBar(pct, barW)
barLine := lipgloss.JoinHorizontal(lipgloss.Center, bar, " ", pctStr)
// 详情
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),
)
barLine := lipgloss.JoinHorizontal(lipgloss.Center, bar)
body := lipgloss.JoinVertical(lipgloss.Left,
titleLine, "", pctStr, barLine, "", detailLine,
titleLine, barLine,
)
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:
default: // 满了就丢弃
}
// 每 10 轮触发一次 GC,减少内存占用
runtime.GC()
}
}
@@ -1160,28 +1168,62 @@ func collectGPUs() []GPUInfo {
if len(gpus) > 0 {
return gpus
}
return tryWMIGPU()
wmi := tryWMIGPU()
return filterPCIeGPUs(wmi)
case "linux":
gpus := tryNvidiaSMI()
if len(gpus) > 0 {
return gpus
}
return tryLinuxGPU()
lspci := tryLinuxGPU()
return filterPCIeGPUs(lspci)
case "darwin":
gpus := tryNvidiaSMI()
if len(gpus) > 0 {
return gpus
}
return tryMacGPU()
mac := tryMacGPU()
return filterPCIeGPUs(append(gpus, mac...))
}
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 {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
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",
).Output()
if err != nil {
@@ -1190,7 +1232,7 @@ func tryNvidiaSMI() []GPUInfo {
var gpus []GPUInfo
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
parts := strings.Split(line, ",")
if len(parts) < 9 {
if len(parts) < 24 {
continue
}
for i := range parts {
@@ -1198,40 +1240,34 @@ func tryNvidiaSMI() []GPUInfo {
parts[i] = strings.Trim(parts[i], "[]")
}
g := GPUInfo{
Name: parts[1],
VRAMTotalMB: parseFloat(parts[2]),
VRAMUsedMB: parseFloat(parts[3]),
Temperature: parseFloat(parts[5]),
LoadPercent: parseFloat(parts[6]),
CoreClock: parseFloat(parts[7]),
MemClock: parseFloat(parts[8]),
}
if len(parts) > 9 {
g.Power = parseFloatSafe(parts[9])
}
if len(parts) > 10 {
g.PowerLimit = parseFloatSafe(parts[10])
}
if len(parts) > 11 {
g.FanSpeed = parseFloatSafe(parts[11])
}
if len(parts) > 12 {
g.PState = cleanStr(parts[12])
}
if len(parts) > 13 {
g.Persistence = cleanStr(parts[13])
}
if len(parts) > 14 {
g.ComputeMode = cleanStr(parts[14])
}
if len(parts) > 15 {
g.DriverVersion = cleanStr(parts[15])
}
if len(parts) > 16 {
g.CudaVersion = cleanStr(parts[16])
}
gpus = append(gpus, g)
// 0:index 1:name 2:uuid 3:bus_id 4:driver 5:vbios
// 6:temp_gpu 7:temp_mem 8:fan 9:power 10:power_limit
// 11:power_default 12:power_min 13:power_max
// 14:clk_gfx 15:clk_sm 16:clk_mem 17:clk_max_gfx 18:clk_max_mem
// 19:util_gpu 20:util_mem 21:util_enc 22:util_dec
// 23:vram_total 24:vram_used 25:vram_free
// 26:ecc_corr 27:ecc_uncorr 28:compute_mode 29:pstate
gpus = append(gpus, GPUInfo{
Name: cleanStr(parts[1]),
DriverVersion: cleanStr(parts[4]),
Temperature: parseFloat(parts[6]),
FanSpeed: parseFloat(parts[8]),
Power: parseFloat(parts[9]),
PowerLimit: parseFloat(parts[10]),
PowerMax: parseFloat(parts[13]),
CoreClock: parseFloat(parts[14]),
MemClock: parseFloat(parts[16]),
ClocksMaxGfx: parseFloat(parts[17]),
ClocksMaxMem: parseFloat(parts[18]),
LoadPercent: parseFloat(parts[19]),
UtilMemory: parseFloat(parts[20]),
UtilEncoder: parseFloat(parts[21]),
UtilDecoder: parseFloat(parts[22]),
VRAMTotalMB: parseFloat(parts[23]),
VRAMUsedMB: parseFloat(parts[24]),
ComputeMode: cleanStr(parts[28]),
PState: cleanStr(parts[29]),
})
}
return gpus
}
+82 -15
View File
@@ -11,6 +11,7 @@ import (
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
@@ -374,6 +375,72 @@ func (h *Hub) saveYAML() {
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) {
var p AgentPayload
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})
}
cp := *rec
h.live[name] = raw
h.live[name] = filterGPURaw(raw)
var md *MiningData
var hist []HashratePoint
if m, ok := h.miningData[name]; ok {
@@ -416,10 +483,23 @@ func (h *Hub) UpdateDevice(raw json.RawMessage) {
h.saveYAML()
h.broadcast(BrowserMsg{
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) {
h.mu.Lock()
_, 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) {
h.mu.Lock()
h.monitors[c] = struct{}{}
File diff suppressed because it is too large Load Diff