package tui import ( "fmt" "get-container/cmd/hytop/tchart" "get-container/utils" "strings" "sync" "sync/atomic" "time" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" linkedlist "github.com/emirpasic/gods/v2/lists/doublylinkedlist" "github.com/lucasb-eyer/go-colorful" "github.com/muesli/gamut" ) const ( SysLoadHeight = 5 // 固定图表高度 SysLoadWidth = 77 // 固定图表宽度,不包含左右的边框 SysLoadCap = 600 // 记录 ) var ( SysLoadBorder = LowLeightStyle.Render(`│ │ │ │ │`) ) // ModelSysLoad 系统负载组件 type ModelSysLoad struct { SysMem *MyTimeChart SysCPU *MyTimeChart DCU *MyTimeChart DCUMem *MyTimeChart dataMaplock sync.RWMutex // 保护下面两个map的锁,防止并发读写 dcuMemData map[int]*linkedlist.List[tchart.TimePoint] // 记录dcu的内存使用信息点 dcuUsgData map[int]*linkedlist.List[tchart.TimePoint] // 记录dcu的使用率信息点 current *SysLoadInfo // 当前的全量信息 topLine, topL string bottomLine, bottomL string style lipgloss.Style width int // 组件总宽度 colors []lipgloss.Color // 时序图的颜色表 colorsLow []lipgloss.Color // 低亮度模式的颜色表 actionMsg *ActionMsg // 动作消息 modelMsg *ModelMsg // 模型周期性消息 DCUToShow atomic.Int32 // 要显示的DCU信息的索引,如果为-1,表示显示dcu的平均数据 dataNumToStore int // 维持DCU和DCUMem图表需要的最多数据点的数量 } type SysLoadInfo struct { T time.Time DCUUsage map[int]float32 DCUMemUsage map[int]float32 Load1, Load5, Load15 float64 MemTotal, SwapTotal uint64 MemUsed, SwapUsed uint64 MemUsedPercent, SwapUsedPercent float64 CPUPercent float64 DCUUsageAvg float32 DCUMemUsageAvg float32 } func NewModelSysLoad(width int) *ModelSysLoad { result := ModelSysLoad{} result.width = width result.dcuMemData = make(map[int]*linkedlist.List[tchart.TimePoint]) result.dcuUsgData = make(map[int]*linkedlist.List[tchart.TimePoint]) result.colors = ConvertColor(gamut.Blends(lipgloss.Color("#ff0000"), lipgloss.Color("#00ff00ff"), SysLoadHeight)) result.colorsLow = ConvertColor(gamut.Blends(gamut.Darker(lipgloss.Color("#ff0000"), 0.8), gamut.Darker(lipgloss.Color("#00ff00ff"), 0.8), SysLoadHeight)) subLine := width - StaticWidth - 1 result.dataNumToStore = subLine*2 + 1 result.SysMem = NewTimeChart(SysLoadWidth, SysLoadHeight, 0, 100, result.colors) result.SysCPU = NewTimeChart(SysLoadWidth, SysLoadHeight, 0, 100, result.colors) result.DCU = NewTimeChart(subLine, SysLoadHeight, 0, 100, result.colors) result.DCUMem = NewTimeChart(subLine, SysLoadHeight, 0, 100, result.colors) result.topLine = myBorder.MiddleLeft + genXAxis(SysLoadWidth) + myBorder.Middle + genXAxis(subLine) + myBorder.MiddleRight result.bottomLine = "╞" + strings.Repeat(myBorder.Bottom, SysLoadWidth) + "╧" + strings.Repeat(myBorder.Bottom, subLine) + "╡" result.topL = LowLeightStyle.Render("├" + genXAxisNoStyle(SysLoadWidth) + "┼" + genXAxisNoStyle(subLine) + "┤") result.bottomL = LowLeightStyle.Render(result.bottomLine) result.style = lipgloss.NewStyle() result.DCUToShow.Store(-1) return &result } func (m *ModelSysLoad) Init() tea.Cmd { result := make([]tea.Cmd, 0) if c := m.DCU.Init(); c != nil { result = append(result, c) } if c := m.DCUMem.Init(); c != nil { result = append(result, c) } if c := m.SysMem.Init(); c != nil { result = append(result, c) } if c := m.SysCPU.Init(); c != nil { result = append(result, c) } sysInfo, _ := utils.GetSysInfo() s := SysLoadInfo{} s.Load1 = sysInfo.LoadAverage1 s.Load5 = sysInfo.LoadAverage5 s.Load15 = sysInfo.LoadAverage15 s.MemTotal = sysInfo.MemTotal s.MemUsed = sysInfo.MemUsage s.SwapTotal = sysInfo.SwapTotal s.SwapUsed = sysInfo.SwapUsage s.MemUsedPercent = sysInfo.MemUsagePercent s.SwapUsedPercent = sysInfo.SwapUsagePercent s.T = time.Now() s.CPUPercent = sysInfo.CPUPercent s.DCUUsageAvg = 0 s.DCUMemUsageAvg = 0 m.current = &s return tea.Batch(result...) } func (m *ModelSysLoad) init() [6]time.Time { var result [6]time.Time result[0] = time.Now() m.DCU.Init() result[1] = time.Now() m.DCUMem.Init() result[2] = time.Now() m.SysMem.Init() result[3] = time.Now() m.SysCPU.Init() result[4] = time.Now() sysInfo, _ := utils.GetSysInfo() result[5] = time.Now() s := SysLoadInfo{} s.Load1 = sysInfo.LoadAverage1 s.Load5 = sysInfo.LoadAverage5 s.Load15 = sysInfo.LoadAverage15 s.MemTotal = sysInfo.MemTotal s.MemUsed = sysInfo.MemUsage s.SwapTotal = sysInfo.SwapTotal s.SwapUsed = sysInfo.SwapUsage s.MemUsedPercent = sysInfo.MemUsagePercent s.SwapUsedPercent = sysInfo.SwapUsagePercent s.T = time.Now() s.CPUPercent = sysInfo.CPUPercent s.DCUUsageAvg = 0 s.DCUMemUsageAvg = 0 m.current = &s return result } func (m *ModelSysLoad) Update(inputMsg tea.Msg) (tea.Model, tea.Cmd) { switch msg := inputMsg.(type) { case *ModelMsg: m.modelMsg = msg m.updateInfo(msg) return m, nil case *ActionMsg: m.actionMsg = msg m.handleAction() return m, nil } return m, nil } // handleAction 处理ActionMsg func (m *ModelSysLoad) handleAction() { if m.actionMsg == nil || m.actionMsg.VM != VMMain { return } targetIndex := int32(m.actionMsg.TargetDCUIndex) oldIndex := m.DCUToShow.Swap(targetIndex) if oldIndex == targetIndex { // 没有变 return } wg := sync.WaitGroup{} wg.Add(1) index := int(targetIndex) rl := m.dataMaplock.RLocker() rl.Lock() mem := m.dcuMemData[index].Values() usage := m.dcuUsgData[index].Values() rl.Unlock() go func() { defer wg.Done() m3, _ := m.DCU.Update(NewTimeCharMsg(usage, true)) m.DCU = m3.(*MyTimeChart) }() m4, _ := m.DCUMem.Update(NewTimeCharMsg(mem, true)) m.DCUMem = m4.(*MyTimeChart) wg.Wait() } func (m *ModelSysLoad) View() string { darkMode := false if m.actionMsg != nil && m.actionMsg.Action != nil && m.actionMsg.VM == VMMain { darkMode = true } memUsed := utils.MemorySize{Num: m.current.MemUsed, Unit: utils.Byte} load := fmt.Sprintf(" Load Average: %.2f %.2f %.2f\n CPU: %.1f%%", m.current.Load1, m.current.Load5, m.current.Load15, m.current.CPUPercent) mem := fmt.Sprintf(" MEM: %s (%.1f%%)", memUsed.HumanReadStr(1), m.current.MemUsedPercent) if darkMode { load = LowLeightStyle.Render(load) mem = LowLeightStyle.Render(mem) } var dcuMem, dcu string if m.actionMsg == nil { dcuMem = fmt.Sprintf(" DCU AVG MEM: %.1f%%", m.current.DCUMemUsageAvg) dcu = fmt.Sprintf(" DCU AVG UTL: %.1f%%", m.current.DCUUsageAvg) } else { index := m.actionMsg.TargetDCUIndex if index == -1 { dcuMem = fmt.Sprintf(" DCU AVG MEM: %.1f%%", m.current.DCUMemUsageAvg) dcu = fmt.Sprintf(" DCU AVG UTL: %.1f%%", m.current.DCUUsageAvg) } else { dcuMem = fmt.Sprintf(" DCU %2d MEM: %.1f%%", index, m.current.DCUMemUsage[index]) dcu = fmt.Sprintf(" DCU %2d UTL: %.1f%%", index, m.current.DCUUsage[index]) } } if darkMode { dcuMem = LowLeightStyle.Render(dcuMem) dcu = LowLeightStyle.Render(dcu) } if darkMode { load = StackPosition(load, m.SysCPU.ViewWithColor(m.colorsLow), lipgloss.Top, lipgloss.Left) mem = StackPosition(mem, m.SysMem.ViewWithColor(m.colorsLow), lipgloss.Bottom, lipgloss.Left) dcuMem = StackPosition(dcuMem, m.DCUMem.ViewWithColor(m.colorsLow), lipgloss.Top, lipgloss.Left) dcu = StackPosition(dcu, m.DCU.ViewWithColor(m.colorsLow), lipgloss.Bottom, lipgloss.Left) load = lipgloss.JoinHorizontal(lipgloss.Top, SysLoadBorder, load, SysLoadBorder) mem = lipgloss.JoinHorizontal(lipgloss.Top, SysLoadBorder, mem, SysLoadBorder) dcuMem = lipgloss.JoinHorizontal(lipgloss.Top, dcuMem, SysLoadBorder) dcu = lipgloss.JoinHorizontal(lipgloss.Top, dcu, SysLoadBorder) } else { load = StackPosition(load, m.SysCPU.View(), lipgloss.Top, lipgloss.Left) mem = StackPosition(mem, m.SysMem.View(), lipgloss.Bottom, lipgloss.Left) dcuMem = StackPosition(dcuMem, m.DCUMem.View(), lipgloss.Top, lipgloss.Left) dcu = StackPosition(dcu, m.DCU.View(), lipgloss.Bottom, lipgloss.Left) load = m.style.Border(myBorder, false, true, false).Render(load) mem = m.style.Border(myBorder, false, true, false).Render(mem) dcuMem = m.style.Border(myBorder, false, true, false, false).Render(dcuMem) dcu = m.style.Border(myBorder, false, true, false, false).Render(dcu) } up := lipgloss.JoinHorizontal(lipgloss.Top, load, dcuMem) down := lipgloss.JoinHorizontal(lipgloss.Top, mem, dcu) if darkMode { return lipgloss.JoinVertical(lipgloss.Left, up, m.topL, down, m.bottomL) + "\n" } return lipgloss.JoinVertical(lipgloss.Left, up, m.topLine, down, m.bottomLine) + "\n" } // updateInfo 向ModelSysLoad中添加数据 func (m *ModelSysLoad) updateInfo(t *ModelMsg) { var sysInfo *utils.SysInfo if t.systemInfo != nil { sysInfo = t.systemInfo } else { sysInfo, _ = utils.GetSysInfo() } s := SysLoadInfo{} s.Load1 = sysInfo.LoadAverage1 s.Load5 = sysInfo.LoadAverage5 s.Load15 = sysInfo.LoadAverage15 s.MemTotal = sysInfo.MemTotal s.MemUsed = sysInfo.MemUsage s.SwapTotal = sysInfo.SwapTotal s.SwapUsed = sysInfo.SwapUsage s.MemUsedPercent = sysInfo.MemUsagePercent s.SwapUsedPercent = sysInfo.SwapUsagePercent s.T = t.t s.CPUPercent = sysInfo.CPUPercent s.DCUUsage = make(map[int]float32) s.DCUMemUsage = make(map[int]float32) s.DCUMemUsageAvg, s.DCUUsageAvg = 0, 0 qinfo, lock := t.DCUInfo.GetQuitInfo() m.dataMaplock.Lock() for k, v := range qinfo { s.DCUMemUsageAvg += v.MemUsedPerent s.DCUMemUsage[k] = v.MemUsedPerent s.DCUUsageAvg += v.DCUUTil s.DCUUsage[k] = v.DCUUTil l1, have := m.dcuMemData[k] if !have { l1 = linkedlist.New[tchart.TimePoint]() m.dcuMemData[k] = l1 } l1.Add(tchart.TimePoint{Time: t.t, Value: float64(v.MemUsedPerent)}) l2, have := m.dcuUsgData[k] if !have { l2 = linkedlist.New[tchart.TimePoint]() m.dcuUsgData[k] = l2 } l2.Add(tchart.TimePoint{Time: t.t, Value: float64(v.DCUUTil)}) } l := len(qinfo) lock.Unlock() s.DCUMemUsageAvg /= float32(l) s.DCUUsageAvg /= float32(l) s.DCUMemUsage[-1] = s.DCUMemUsageAvg s.DCUUsage[-1] = s.DCUUsageAvg dcu, have := m.dcuUsgData[-1] if !have { dcu = linkedlist.New[tchart.TimePoint]() m.dcuUsgData[-1] = dcu } dcu.Add(tchart.TimePoint{Time: t.t, Value: float64(s.DCUUsageAvg)}) mem, have := m.dcuMemData[-1] if !have { mem = linkedlist.New[tchart.TimePoint]() m.dcuMemData[-1] = mem } mem.Add(tchart.TimePoint{Time: t.t, Value: float64(s.DCUMemUsageAvg)}) // 判断是否要丢弃多余的点 if dcu.Size() > m.dataNumToStore { for _, v := range m.dcuMemData { v.Remove(0) } for _, v := range m.dcuUsgData { v.Remove(0) } } m.dataMaplock.Unlock() m.current = &s needUpdate, index := false, -1 if m.actionMsg != nil { index = m.actionMsg.TargetDCUIndex // 需要进入特定DCU的图表 oldIndex := m.DCUToShow.Swap(int32(index)) if oldIndex != int32(m.actionMsg.TargetDCUIndex) { // 需要更新 needUpdate = true } } else { oldIndex := m.DCUToShow.Swap(-1) if oldIndex != -1 { // 需要更新 needUpdate = true } } wg := sync.WaitGroup{} wg.Add(4) go func() { defer wg.Done() m1, _ := m.SysMem.Update(NewTimeCharMsg([]tchart.TimePoint{{Time: t.t, Value: s.MemUsedPercent}}, false)) m.SysMem = m1.(*MyTimeChart) }() go func() { defer wg.Done() m2, _ := m.SysCPU.Update(NewTimeCharMsg([]tchart.TimePoint{{Time: t.t, Value: s.CPUPercent}}, false)) m.SysCPU = m2.(*MyTimeChart) }() if needUpdate { // 准备全量数据 rl := m.dataMaplock.RLocker() rl.Lock() mem := m.dcuMemData[index].Values() usage := m.dcuUsgData[index].Values() rl.Unlock() go func() { defer wg.Done() m3, _ := m.DCU.Update(NewTimeCharMsg(usage, true)) m.DCU = m3.(*MyTimeChart) }() go func() { defer wg.Done() m4, _ := m.DCUMem.Update(NewTimeCharMsg(mem, true)) m.DCUMem = m4.(*MyTimeChart) }() } else { // 无需更新,仅追加数据 var valM, valU float32 vU, haveU := s.DCUUsage[index] vM, haveM := s.DCUMemUsage[index] if haveU { valU = vU } else { valU = 0 } if haveM { valM = vM } else { valM = 0 } go func() { defer wg.Done() m3, _ := m.DCU.Update(NewTimeCharMsg([]tchart.TimePoint{{Time: t.t, Value: float64(valU)}}, false)) m.DCU = m3.(*MyTimeChart) }() go func() { defer wg.Done() m4, _ := m.DCUMem.Update(NewTimeCharMsg([]tchart.TimePoint{{Time: t.t, Value: float64(valM)}}, false)) m.DCUMem = m4.(*MyTimeChart) }() } wg.Wait() } func (m *ModelSysLoad) RanderColor(str string) string { lines := strings.Split(strings.Trim(str, "\n"), "\n") result := "" for k, line := range lines { c, _ := colorful.MakeColor(m.colors[k]) result += lipgloss.NewStyle().Foreground(lipgloss.Color(c.Hex())).Render(line) + "\n" } return result }