package tui import ( "errors" "fmt" "get-container/cmd/hytop/tchart" "slices" "strings" "sync" "time" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" linkedlist "github.com/emirpasic/gods/v2/lists/doublylinkedlist" ) /* 所有dcu的平均功耗表常态维护 一旦选中了dcu,选中dcu的平均功耗表常态维护 ModelPowerDetail.Charts中key为非负数的值,如果存在就更新数据 */ // ModelPowerDetail 展示DCU功率消耗的Model type ModelPowerDetail struct { PowerCap map[int]float32 // dcu功耗墙,即最大功耗,单位为瓦 DataCache map[int]*linkedlist.List[tchart.TimePoint] // 功率数据,key为dcu index,value为dcu功率 DCUDataCache map[int]*[4]float32 // 记录dcu与功耗相关的其他信息,key为dcu index,value为dcu的温度、使用率、内存使用率和功率 height, width int // 屏幕高度、宽度 Charts map[int]*ChartAndArea // 图表,key为dcu index,key为-1表示平均值图表,-2表示选中的dcu的平均值图表 Pids map[int]int // 记录哪些dcu在被使用 selectedDCU map[int]bool // 记录哪些dcu被选中 cursor *int // 光标指向的dcu index dcuIndex []int // dcu index DisplaySummary bool // 显示概要信息 DisplayAvg bool // 显示功率平均值 lock sync.RWMutex // 保护DataCache和Charts的锁 ScreenSplit map[int][][]ScreenArea // 记录屏幕尺寸 title string // 标题字符串 } type ChartAndArea struct { chart *MyTimeChart area ScreenArea h, l int } func NewModelPower(height, width int) *ModelPowerDetail { result := ModelPowerDetail{ PowerCap: make(map[int]float32), DataCache: make(map[int]*linkedlist.List[tchart.TimePoint]), DCUDataCache: make(map[int]*[4]float32), height: height, width: width, Pids: make(map[int]int), Charts: make(map[int]*ChartAndArea), selectedDCU: make(map[int]bool), dcuIndex: make([]int, 0, 16), cursor: nil, ScreenSplit: make(map[int][][]ScreenArea), DisplaySummary: false, DisplayAvg: false, lock: sync.RWMutex{}, } return &result } const ( ModelPowerTitle = "DCU Pids Temp(°C) HCU(%) Mem(%) PwrCap(W) AvgPwr(W)\n" ) var ( MPStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder(), false, false, false, true) ) func (m *ModelPowerDetail) Init() tea.Cmd { m.height-- for i := 1; i < 10; i++ { m.ScreenSplit[i] = m.SplitScreen(i) } m.height++ m.title = "DCU Power Monitor" s := "Press

and to select dcu, Parse to show average" extLen := m.width - len(m.title) - len(s) if extLen > 0 { m.title += strings.Repeat(" ", extLen) + s } m.title = HeightLightStyle.Render(m.title) + "\n" return nil } func (m *ModelPowerDetail) Update(imsg tea.Msg) (tea.Model, tea.Cmd) { switch msg := imsg.(type) { case *ModelMsg: // 更新数据 var powerTotal float32 = 0 threshold := msg.t.Add(time.Second * (time.Duration(m.width * -2))) m.dcuIndex = m.dcuIndex[:0] var pwrCap float32 = 0 info, lock := msg.DCUInfo.GetQuitInfo() extAvg := m.DisplayAvg && len(m.selectedDCU) > 0 var selectedTotal float32 = 0 m.lock.Lock() for k, v := range info { tmp := v.PwrCap.OrElse(0) m.PowerCap[k] = tmp if tmp > 0 { pwrCap = tmp } l, have := m.DataCache[k] if !have { l = linkedlist.New[tchart.TimePoint]() } tmp = v.PwrAvg.OrElse(0) tp := tchart.TimePoint{Time: msg.t, Value: float64(tmp)} l.Append(tp) chart, have := m.Charts[k] if have { chart.chart.PutPoint([]tchart.TimePoint{tp}) } powerTotal += tmp if extAvg && m.selectedDCU[k] { selectedTotal += tmp } if !have { m.DataCache[k] = l } // 剔除多余的点 for { point, have := l.Get(0) if !have { break } if point.Time.Before(threshold) { l.Remove(0) } else { break } } d, have := m.DCUDataCache[k] if !have { d := &[4]float32{v.Temp.OrElse(0), v.DCUUTil.OrElse(0), v.MemUsedPerent.OrElse(0), v.PwrAvg.OrElse(0)} m.DCUDataCache[k] = d } else { d[0] = v.Temp.OrElse(0) d[1] = v.DCUUTil.OrElse(0) d[2] = v.MemUsedPerent.OrElse(0) d[3] = v.PwrAvg.OrElse(0) } m.dcuIndex = append(m.dcuIndex, k) } lock.Unlock() slices.Sort(m.dcuIndex) powerTotal /= float32(len(m.dcuIndex)) selectedTotal /= float32(len(m.selectedDCU)) l, have := m.DataCache[-1] if !have { l = linkedlist.New[tchart.TimePoint]() m.DataCache[-1] = l } l.Append(tchart.TimePoint{Time: msg.t, Value: float64(powerTotal)}) for { point, have := l.Get(0) if !have { break } if point.Time.Before(threshold) { l.Remove(0) } else { break } } l, have = m.DataCache[-2] if !have { l = linkedlist.New[tchart.TimePoint]() m.DataCache[-2] = l } for { point, have := l.Get(0) if !have { break } if point.Time.Before(threshold) { l.Remove(0) } else { break } } chart, have := m.Charts[-1] if !have { c := NewTimeChart(m.width-1, m.height-2, 0, float64(pwrCap), []lipgloss.Color{lipgloss.Color("#ff0000"), lipgloss.Color("#00ff00ff")}) chart = &ChartAndArea{chart: c, area: m.ScreenSplit[1][0][0], h: 0, l: 0} m.Charts[-1] = chart } chart.chart.PutPoint([]tchart.TimePoint{{Time: msg.t, Value: float64(powerTotal)}}) if extAvg { chart, have = m.Charts[-2] if have { chart.chart.PutPoint([]tchart.TimePoint{{Time: msg.t, Value: float64(selectedTotal)}}) m.Charts[-2] = chart } } m.lock.Unlock() m.PowerCap[-1] = pwrCap for k := range m.Pids { m.Pids[k] = 0 } for k, v := range msg.DCUPidInfo { m.Pids[k] = len(v) } case tea.KeyType: m.HandleKey(msg.String()) case tea.KeyMsg: m.HandleKey(msg.String()) } return m, nil } // HandleKey 处理按键事件 func (m *ModelPowerDetail) HandleKey(key string) { switch key { case "p": // 显示概要信息 m.DisplaySummary = !m.DisplaySummary case "a": // 显示平均功率 m.DisplayAvg = !m.DisplayAvg case KeySpace: // 选中/取消dcu if !m.DisplaySummary { break } if m.cursor == nil { break } a, b := m.selectedDCU[*m.cursor] selected := a && b if selected { // 已经选中了,现在取消 delete(m.selectedDCU, *m.cursor) m.UpdateCharts(*m.cursor, false) } else { // 判断 if len(m.selectedDCU) >= 8 { break } m.selectedDCU[*m.cursor] = true m.UpdateCharts(*m.cursor, true) } case KeyUp: if !m.DisplaySummary { break } if m.cursor == nil { index := m.dcuIndex[len(m.dcuIndex)-1] m.cursor = &index } else { index := slices.Index(m.dcuIndex, *m.cursor) if index == -1 { *m.cursor = m.dcuIndex[len(m.dcuIndex)-1] } else { if index > 0 { *m.cursor = m.dcuIndex[index-1] } } } case KeyDown: if !m.DisplaySummary { break } if m.cursor == nil { index := m.dcuIndex[0] m.cursor = &index } else { index := slices.Index(m.dcuIndex, *m.cursor) if index == -1 { *m.cursor = m.dcuIndex[0] } else { if index < (len(m.dcuIndex) - 1) { *m.cursor = m.dcuIndex[index+1] } } } } } // SummaryInfo 生成概要信息 func (m *ModelPowerDetail) SummaryInfo() string { sb := strings.Builder{} sb.WriteString(ModelPowerTitle) tmp := len(m.dcuIndex) - 1 for k, v := range m.dcuIndex { d := m.DCUDataCache[v] a, b := m.selectedDCU[v] selected := a && b cursorOn := (m.cursor != nil && *m.cursor == v) if !selected && !cursorOn { fmt.Fprintf(&sb, "%3d %4d %8.2f %6.2f %6.2f %9.2f %9.2f", v, m.Pids[v], d[0], d[1], d[2], m.PowerCap[v], d[3]) } else { var style lipgloss.Style if selected && cursorOn { style = HeightSelectedStyle } else if selected { style = SelectedStyle } else { style = HeightLightStyle } s := fmt.Sprintf("%3d %4d %8.2f %6.2f %6.2f %9.2f %9.2f", v, m.Pids[v], d[0], d[1], d[2], m.PowerCap[v], d[3]) sb.WriteString(style.Render(s)) } if k < tmp { sb.WriteByte('\n') } } style := lipgloss.NewStyle().Border(lipgloss.NormalBorder()) return style.Render(sb.String()) } // UpdateCharts 解析数据,生成图表结构信息 func (m *ModelPowerDetail) UpdateCharts(index int, isAdd bool) { dcuNum := len(m.selectedDCU) dcus := make([]int, 0, dcuNum) for k, v := range m.selectedDCU { if v { dcus = append(dcus, k) } } dcuNum = len(dcus) slices.Sort(dcus) m.lock.Lock() if isAdd { // 添加dcu for k, v := range dcus { area, h, l, err := m.getScreenArea(dcuNum, k) if err != nil { panic(fmt.Sprintf("%v", err)) } chart, have := m.Charts[v] if !have { // 需要新建图表 c := createChart(area, float64(m.PowerCap[v]), 0) chart = &ChartAndArea{ chart: c, area: area, h: h, l: l, } m.Charts[v] = chart chart.chart.PutPoint(m.DataCache[v].Values()) continue } // 判断图表尺寸是否一致 if chart.area.Equal(&area) { // 一致,仅修改位置即可 chart.h = h chart.l = l } else { // 尺寸不一致,需要重新生成图表 chart.chart = createChart(area, float64(m.PowerCap[v]), 0) chart.h = h chart.l = l chart.area = area // 新图表添加数据点 chart.chart.PutPoint(m.DataCache[v].Values()) } } } else { // 删除dcu delete(m.Charts, index) for k, v := range dcus { area, h, l, err := m.getScreenArea(dcuNum, k) if err != nil { panic(fmt.Sprintf("%v", err)) } chart := m.Charts[v] if chart.area.Equal(&area) { // 尺寸一致,无需重新生成,仅修改位置即可 chart.h = h chart.l = l continue } // 尺寸不一致,需要重新生成图表 chart.chart = createChart(area, float64(m.PowerCap[v]), 0) chart.h = h chart.l = l chart.area = area // 新图表添加数据点 chart.chart.PutPoint(m.DataCache[v].Values()) } } // 更新选中dcu的平均值图表 if dcuNum != 0 { l := linkedlist.New[tchart.TimePoint]() var points []tchart.TimePoint for _, v := range dcus { p := m.DataCache[v].Values() if points == nil { points = p continue } for k := range p { v := p[k].Value + points[k].Value points[k].Value = v } } for k := range points { points[k].Value /= float64(dcuNum) } l.Append(points...) m.DataCache[-2] = l chart, have := m.Charts[-2] if !have { c := NewTimeChart(m.width-1, m.height-2, 0, float64(m.PowerCap[-1]), []lipgloss.Color{lipgloss.Color("#ff0000"), lipgloss.Color("#00ff00ff")}) chart = &ChartAndArea{chart: c, area: m.ScreenSplit[1][0][0], h: 0, l: 0} m.Charts[-2] = chart } chart.chart.ResetPutPoint(points) } else { delete(m.Charts, -2) } m.lock.Unlock() } // RenderCharts 渲染图表 func (m *ModelPowerDetail) RenderCharts() string { dcuSelected := make([]int, 0, len(m.selectedDCU)) for k, v := range m.selectedDCU { if v { dcuSelected = append(dcuSelected, k) } } slices.Sort(dcuSelected) selectNum := len(dcuSelected) mstr := LowLeightStyle.Render(fmt.Sprintf("%.0fW", m.PowerCap[-1]/2)) // 显示全部平均功率 if selectNum == 0 || (selectNum == len(m.dcuIndex) && m.DisplayAvg) { chart := m.Charts[-1].chart x := genXAxis(chart.width, B) str := StackPosition(mstr, chart.ViewWithMiddleLine(), lipgloss.Center, lipgloss.Left) + "\n" + x str = StackPosition("DCU: All Avg", str, lipgloss.Top, lipgloss.Left) str = MPStyle.Render(str) return str } else if m.DisplayAvg { // 显示选中dcu的平均功率图表 chart := m.Charts[-2].chart x := genXAxis(chart.width, B) str := StackPosition(mstr, chart.ViewWithMiddleLine(), lipgloss.Center, lipgloss.Left) + "\n" + x strDCU := strings.ReplaceAll(strings.Trim(fmt.Sprintf("%v", dcuSelected), "[]"), " ", ",") str = StackPosition(fmt.Sprintf("DCU: %s Avg", strDCU), str, lipgloss.Top, lipgloss.Left) return MPStyle.Render(str) } else { // 显示选中dcu的图表 areas := m.ScreenSplit[selectNum] pa, err := parseAreas(areas) if err != nil { panic(err.Error()) } strs := make([]string, 0, selectNum) for _, v := range dcuSelected { chart := m.Charts[v].chart str := StackPosition(mstr, chart.ViewWithMiddleLine(), lipgloss.Center, lipgloss.Left) str = StackPosition(fmt.Sprintf("DCU: %d", v), str, lipgloss.Top, lipgloss.Left) str += "\n" str += genXAxis(chart.width, B) strs = append(strs, MPStyle.Render(str)) } lines := make(map[int]string) for k, v := range strs { po := pa[k] l, have := lines[po[0]] if !have { l = v lines[po[0]] = l } else { lines[po[0]] = lipgloss.JoinHorizontal(lipgloss.Top, l, v) } } switch len(lines) { case 1: return lines[0] case 2: return lipgloss.JoinVertical(lipgloss.Left, lines[0], lines[1]) case 3: return lipgloss.JoinVertical(lipgloss.Left, lines[0], lines[1], lines[2]) default: panic("error line number") } } } func (m *ModelPowerDetail) View() string { rl := m.lock.RLocker() rl.Lock() defer rl.Unlock() if m.DisplaySummary { summary := m.SummaryInfo() charts := m.RenderCharts() return m.title + StackPosition(summary, charts, lipgloss.Center, lipgloss.Center) } else { return m.title + m.RenderCharts() } } // getScreenArea 获取指定索引的屏幕区域尺寸、所在的行、列 func (m *ModelPowerDetail) getScreenArea(num, index int) (ScreenArea, int, int, error) { if num > 9 || num <= 0 { return ScreenArea{}, 0, 0, errors.New("num out of range") } if index < 0 || index >= num { return ScreenArea{}, 0, 0, errors.New("index out of range") } if m.ScreenSplit == nil { return ScreenArea{}, 0, 0, errors.New("Model PowerDetail not init") } target := m.ScreenSplit[num] switch num { case 1: return target[0][0], 0, 0, nil case 2: switch index { case 0: return target[0][0], 0, 0, nil case 1: return target[1][0], 1, 0, nil } case 3, 4: h := index / 2 l := index % 2 return target[h][l], h, l, nil case 5, 6, 8, 9: h := index / 3 l := index % 3 return target[h][l], h, l, nil case 7: switch index { case 0, 1, 2: return target[0][index], 0, index, nil case 3, 4: return target[1][index-3], 1, index - 3, nil case 5, 6: return target[2][index-5], 2, index - 5, nil } } return ScreenArea{}, 0, 0, errors.New("Model PowerDetail unknown error") } // SplitScreen 分割屏幕,得到每个区域的尺寸,返回结果为二维切片,第一维是行,第二维是列 func (m *ModelPowerDetail) SplitScreen(num int) [][]ScreenArea { if num <= 0 { return nil } result := make([][]ScreenArea, 0, 3) result = append(result, make([]ScreenArea, 0, 3)) switch num { case 1: result[0] = append(result[0], NewScreenArea(m.width, m.height)) case 2: result = append(result, make([]ScreenArea, 0, 1)) result[0] = append(result[0], NewScreenArea(m.width, m.height/2)) result[1] = append(result[1], NewScreenArea(m.width, m.height/2)) if m.height%2 != 0 { result[0][0].h++ } case 3: result = append(result, make([]ScreenArea, 0, 1)) result[0] = append(result[0], NewScreenArea(m.width/2, m.height/2), NewScreenArea(m.width/2, m.height/2)) result[1] = append(result[1], NewScreenArea(m.width, m.height/2)) if m.width%2 != 0 { result[0][0].w++ } if m.height%2 != 0 { result[0][0].h++ result[0][1].h++ } case 4: result = append(result, make([]ScreenArea, 0, 1)) result[0] = append(result[0], NewScreenArea(m.width/2, m.height/2), NewScreenArea(m.width/2, m.height/2)) result[1] = append(result[1], NewScreenArea(m.width/2, m.height/2), NewScreenArea(m.width/2, m.height/2)) if m.width%2 != 0 { result[0][0].w++ result[1][0].w++ } if m.height%2 != 0 { result[0][0].h++ result[0][1].h++ } case 5: // 两行,3,2 result = append(result, make([]ScreenArea, 0, 2)) result[0] = append(result[0], NewScreenArea(m.width/3, m.height/2), NewScreenArea(m.width/3, m.height/2), NewScreenArea(m.width/3, m.height/2)) result[1] = append(result[1], NewScreenArea(m.width/2, m.height/2), NewScreenArea(m.width/2, m.height/2)) if m.width%2 != 0 { result[1][0].w++ } for l := 0; l < m.width%3; l++ { result[0][l].w++ } if m.height%2 != 0 { for l := range result[0] { result[0][l].h++ } } case 6: // 两行,3,3 result = append(result, make([]ScreenArea, 0, 3)) result[0] = append(result[0], NewScreenArea(m.width/3, m.height/2), NewScreenArea(m.width/3, m.height/2), NewScreenArea(m.width/3, m.height/2)) result[1] = append(result[1], NewScreenArea(m.width/3, m.height/2), NewScreenArea(m.width/3, m.height/2), NewScreenArea(m.width/3, m.height/2)) for h := range result { for l := 0; l < m.width%3; l++ { result[h][l].w++ } } if m.height%2 != 0 { for l := range result[0] { result[0][l].h++ } } case 7: // 三行,3,2,2 result = append(result, make([]ScreenArea, 0, 2), make([]ScreenArea, 0, 2)) result[0] = append(result[0], NewScreenArea(m.width/3, m.height/3), NewScreenArea(m.width/3, m.height/3), NewScreenArea(m.width/3, m.height/3)) result[1] = append(result[1], NewScreenArea(m.width/2, m.height/3), NewScreenArea(m.width/2, m.height/3)) result[2] = append(result[2], NewScreenArea(m.width/2, m.height/3), NewScreenArea(m.width/2, m.height/3)) for h := 0; h < m.height%3; h++ { for l := range result[h] { result[h][l].h++ } } for l := 0; l < m.width%3; l++ { result[0][l].w++ } if m.width%2 != 0 { result[1][0].w++ result[1][1].w++ result[2][0].w++ result[2][1].w++ } case 8: // 三行,3,3,2 result = append(result, make([]ScreenArea, 0, 3), make([]ScreenArea, 0, 2)) result[0] = append(result[0], NewScreenArea(m.width/3, m.height/3), NewScreenArea(m.width/3, m.height/3), NewScreenArea(m.width/3, m.height/3)) result[1] = append(result[1], NewScreenArea(m.width/3, m.height/3), NewScreenArea(m.width/3, m.height/3), NewScreenArea(m.width/3, m.height/3)) result[2] = append(result[2], NewScreenArea(m.width/2, m.height/3), NewScreenArea(m.width/2, m.height/3)) for h := 0; h < m.height%3; h++ { for l := range result[h] { result[h][l].h++ } } for h := range result[:2] { for l := 0; l < m.width%3; l++ { result[h][l].w++ } } if m.width%2 != 0 { result[2][0].w++ } case 9: result = append(result, make([]ScreenArea, 0, 3), make([]ScreenArea, 0, 3)) result[0] = append(result[0], NewScreenArea(m.width/3, m.height/3), NewScreenArea(m.width/3, m.height/3), NewScreenArea(m.width/3, m.height/3)) result[1] = append(result[1], NewScreenArea(m.width/3, m.height/3), NewScreenArea(m.width/3, m.height/3), NewScreenArea(m.width/3, m.height/3)) result[2] = append(result[2], NewScreenArea(m.width/3, m.height/3), NewScreenArea(m.width/3, m.height/3), NewScreenArea(m.width/3, m.height/3)) for h := 0; h < m.height%3; h++ { for l := range result[h] { result[h][l].h++ } } for h := range result { for l := 0; l < m.width%3; l++ { result[h][l].w++ } } default: return nil } return result } type ScreenArea struct { w, h int } func (sa *ScreenArea) Equal(other *ScreenArea) bool { return sa.h == other.h && sa.w == other.w } func NewScreenArea(w, h int) ScreenArea { return ScreenArea{ w: w, h: h, } } func createChart(area ScreenArea, max, min float64) *MyTimeChart { return NewTimeChart(area.w-1, area.h-1, min, max, []lipgloss.Color{lipgloss.Color("#ff0000"), lipgloss.Color("#00ff00ff")}) } func parseAreas(areas [][]ScreenArea) (map[int][2]int, error) { if areas == nil { return nil, errors.New("error args is null") } index := 0 result := make(map[int][2]int) for kout, vout := range areas { for kin := range vout { result[index] = [2]int{kout, kin} index++ } } return result, nil }