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
}