Compare commits
9 Commits
2377530fab
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 095f2845f6 | |||
| 2f6bea41d5 | |||
| 5ef7937d3c | |||
| b412b8f9d1 | |||
| 9bdc92252e | |||
| 62143a5a32 | |||
| 2c103af9d8 | |||
| d39713a196 | |||
| 8f1325800c |
+123
-87
@@ -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
@@ -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{}{}
|
||||
|
||||
+1595
-333
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user