From 12d729aea3ef5d089622ec92f255c84d9209f0f8 Mon Sep 17 00:00:00 2001 From: HynoR <20227709+HynoR@users.noreply.github.com> Date: Tue, 2 Dec 2025 17:55:37 +0800 Subject: [PATCH 1/3] fix: Update CPU usage retrieval to include detailed percentage information --- agent/app/service/dashboard.go | 4 +- agent/utils/psutil/cpu.go | 312 ++++++++++++++++++++------------- 2 files changed, 194 insertions(+), 122 deletions(-) diff --git a/agent/app/service/dashboard.go b/agent/app/service/dashboard.go index 312364fa5e18..09fc09047570 100644 --- a/agent/app/service/dashboard.go +++ b/agent/app/service/dashboard.go @@ -108,7 +108,7 @@ func (u *DashboardService) LoadCurrentInfoForNode() *dto.NodeCurrent { currentInfo.CPUTotal, _ = psutil.CPUInfo.GetLogicalCores(false) - cpuUsedPercent, perCore := psutil.CPU.GetCPUUsage() + cpuUsedPercent, perCore, _ := psutil.CPU.GetCPUUsage() if len(perCore) == 0 { currentInfo.CPUTotal = psutil.CPU.NumCPU() } else { @@ -184,7 +184,7 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d currentInfo.Procs = hostInfo.Procs currentInfo.CPUTotal, _ = psutil.CPUInfo.GetLogicalCores(false) - cpuUsedPercent, perCore := psutil.CPU.GetCPUUsage() + cpuUsedPercent, perCore, _ := psutil.CPU.GetCPUUsage() if len(perCore) == 0 { currentInfo.CPUTotal = psutil.CPU.NumCPU() } else { diff --git a/agent/utils/psutil/cpu.go b/agent/utils/psutil/cpu.go index f10f22fa2629..d652f1b30bef 100644 --- a/agent/utils/psutil/cpu.go +++ b/agent/utils/psutil/cpu.go @@ -15,126 +15,70 @@ const ( fastInterval = 3 * time.Second ) +// ============================================================================ +// 结构体定义 +// ============================================================================ + type CPUStat struct { Idle uint64 Total uint64 } +// CPUDetailedStat 存储 /proc/stat 中的详细 CPU 时间数据 +// 字段顺序: user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice +type CPUDetailedStat struct { + User uint64 // 用户态时间 + Nice uint64 // 低优先级用户态时间 + System uint64 // 内核态时间 + Idle uint64 // 空闲时间 + Iowait uint64 // I/O 等待时间 + Irq uint64 // 硬中断时间 + Softirq uint64 // 软中断时间 + Steal uint64 // 虚拟化环境中被其他 OS 占用的时间 + Guest uint64 // 运行虚拟 CPU 的时间 + GuestNice uint64 // 运行低优先级虚拟 CPU 的时间 + Total uint64 // 总时间 +} + +// CPUDetailedPercent 存储类似 top 命令的 CPU 百分比信息 +type CPUDetailedPercent struct { + User float64 `json:"user"` // %us - 用户空间占用 + System float64 `json:"system"` // %sy - 内核空间占用 + Nice float64 `json:"nice"` // %ni - 改变过优先级的进程占用 + Idle float64 `json:"idle"` // %id - 空闲 + Iowait float64 `json:"iowait"` // %wa - I/O 等待 + Irq float64 `json:"irq"` // %hi - 硬中断 + Softirq float64 `json:"softirq"` // %si - 软中断 + Steal float64 `json:"steal"` // %st - 虚拟机偷取 +} + type CPUUsageState struct { mu sync.Mutex lastTotalStat *CPUStat lastPerCPUStat []CPUStat + lastDetailStat *CPUDetailedStat lastSampleTime time.Time - cachedTotalUsage float64 - cachedPerCore []float64 -} - -func readCPUStat() (CPUStat, error) { - data, err := os.ReadFile("/proc/stat") - if err != nil { - return CPUStat{}, err - } - - fields := strings.Fields(strings.Split(string(data), "\n")[0])[1:] - nums := make([]uint64, len(fields)) - - for i, f := range fields { - v, _ := strconv.ParseUint(f, 10, 64) - nums[i] = v - } - - idle := nums[3] + nums[4] - var total uint64 - for _, v := range nums { - total += v - } - - return CPUStat{Idle: idle, Total: total}, nil -} - -func (c *CPUUsageState) readPerCPUStat() ([]CPUStat, error) { - data, err := os.ReadFile("/proc/stat") - if err != nil { - return nil, err - } - - lines := strings.Split(string(data), "\n") - stats := c.lastPerCPUStat[:0] - - for _, l := range lines[1:] { - if !strings.HasPrefix(l, "cpu") { - continue - } - if len(l) < 4 || l[3] < '0' || l[3] > '9' { - continue - } - - fields := strings.Fields(l)[1:] - nums := make([]uint64, len(fields)) - for i, f := range fields { - v, _ := strconv.ParseUint(f, 10, 64) - nums[i] = v - } - - idle := nums[3] + nums[4] - var total uint64 - for _, v := range nums { - total += v - } - - stats = append(stats, CPUStat{Idle: idle, Total: total}) - } - - return stats, nil + cachedTotalUsage float64 + cachedPerCore []float64 + cachedDetailedPercent CPUDetailedPercent } -func readPerCPUStatCopy() []CPUStat { - data, err := os.ReadFile("/proc/stat") - if err != nil { - return nil - } - - lines := strings.Split(string(data), "\n") - var stats []CPUStat - - for _, l := range lines[1:] { - if !strings.HasPrefix(l, "cpu") { - continue - } - if len(l) < 4 || l[3] < '0' || l[3] > '9' { - continue - } - - fields := strings.Fields(l)[1:] - nums := make([]uint64, len(fields)) - for i, f := range fields { - v, _ := strconv.ParseUint(f, 10, 64) - nums[i] = v - } - - idle := nums[3] + nums[4] - var total uint64 - for _, v := range nums { - total += v - } - - stats = append(stats, CPUStat{Idle: idle, Total: total}) - } - - return stats +type CPUInfoState struct { + mu sync.RWMutex + initialized bool + cachedInfo []cpu.InfoStat + cachedPhysCores int + cachedLogicCores int } -func calcCPUPercent(prev, cur CPUStat) float64 { - deltaIdle := float64(cur.Idle - prev.Idle) - deltaTotal := float64(cur.Total - prev.Total) - if deltaTotal <= 0 { - return 0 - } - return (1 - deltaIdle/deltaTotal) * 100 -} +// ============================================================================ +// CPUUsageState 公有方法 +// ============================================================================ -func (c *CPUUsageState) GetCPUUsage() (float64, []float64) { +// GetCPUUsage 返回 CPU 使用率、每核使用率和详细百分比信息 +// 返回: totalUsage, perCoreUsage, detailedPercent (类似 top 命令的 %us, %sy, %ni, %id, %wa, %hi, %si, %st) +func (c *CPUUsageState) GetCPUUsage() (float64, []float64, CPUDetailedPercent) { c.mu.Lock() now := time.Now() @@ -142,21 +86,21 @@ func (c *CPUUsageState) GetCPUUsage() (float64, []float64) { if !c.lastSampleTime.IsZero() && now.Sub(c.lastSampleTime) < fastInterval { result := c.cachedTotalUsage perCore := c.cachedPerCore + detailed := c.cachedDetailedPercent c.mu.Unlock() - return result, perCore + return result, perCore, detailed } needReset := c.lastSampleTime.IsZero() || now.Sub(c.lastSampleTime) >= resetInterval c.mu.Unlock() if needReset { - firstTotal, _ := readCPUStat() - firstPer := readPerCPUStatCopy() + firstTotal, firstDetail, firstPer := readAllCPUStat() time.Sleep(100 * time.Millisecond) - secondTotal, _ := readCPUStat() - secondPer := readPerCPUStatCopy() + secondTotal, secondDetail, secondPer := readAllCPUStat() totalUsage := calcCPUPercent(firstTotal, secondTotal) + detailedPercent := calcCPUDetailedPercent(firstDetail, secondDetail) perCore := make([]float64, len(secondPer)) for i := range secondPer { @@ -166,21 +110,23 @@ func (c *CPUUsageState) GetCPUUsage() (float64, []float64) { c.mu.Lock() c.cachedTotalUsage = totalUsage c.cachedPerCore = perCore + c.cachedDetailedPercent = detailedPercent c.lastTotalStat = &secondTotal + c.lastDetailStat = &secondDetail c.lastPerCPUStat = secondPer c.lastSampleTime = time.Now() c.mu.Unlock() - return totalUsage, perCore + return totalUsage, perCore, detailedPercent } - curTotal, _ := readCPUStat() - curPer := readPerCPUStatCopy() + curTotal, curDetail, curPer := readAllCPUStat() c.mu.Lock() defer c.mu.Unlock() totalUsage := calcCPUPercent(*c.lastTotalStat, curTotal) + detailedPercent := calcCPUDetailedPercent(*c.lastDetailStat, curDetail) if len(c.cachedPerCore) != len(curPer) { c.cachedPerCore = make([]float64, len(curPer)) @@ -190,11 +136,14 @@ func (c *CPUUsageState) GetCPUUsage() (float64, []float64) { } c.cachedTotalUsage = totalUsage + c.cachedPerCore = c.cachedPerCore + c.cachedDetailedPercent = detailedPercent c.lastTotalStat = &curTotal + c.lastDetailStat = &curDetail c.lastPerCPUStat = curPer c.lastSampleTime = time.Now() - return totalUsage, c.cachedPerCore + return totalUsage, c.cachedPerCore, detailedPercent } func (c *CPUUsageState) NumCPU() int { @@ -204,13 +153,9 @@ func (c *CPUUsageState) NumCPU() int { return len(c.cachedPerCore) } -type CPUInfoState struct { - mu sync.RWMutex - initialized bool - cachedInfo []cpu.InfoStat - cachedPhysCores int - cachedLogicCores int -} +// ============================================================================ +// CPUInfoState 公有方法 +// ============================================================================ func (c *CPUInfoState) GetCPUInfo(forceRefresh bool) ([]cpu.InfoStat, error) { c.mu.RLock() @@ -274,3 +219,130 @@ func (c *CPUInfoState) GetLogicalCores(forceRefresh bool) (int, error) { return cores, nil } + +// ============================================================================ +// 私有函数 +// ============================================================================ + +// readProcStat 读取 /proc/stat 文件内容 +func readProcStat() ([]byte, error) { + return os.ReadFile("/proc/stat") +} + +// parseCPUFields 解析 CPU 行的数值字段 +func parseCPUFields(line string) []uint64 { + fields := strings.Fields(line) + if len(fields) <= 1 { + return nil + } + fields = fields[1:] // 跳过 "cpu" 或 "cpuN" 前缀 + + nums := make([]uint64, len(fields)) + for i, f := range fields { + v, _ := strconv.ParseUint(f, 10, 64) + nums[i] = v + } + return nums +} + +// calcIdleAndTotal 计算空闲时间和总时间 +func calcIdleAndTotal(nums []uint64) (idle, total uint64) { + if len(nums) < 5 { + return 0, 0 + } + idle = nums[3] + nums[4] + for _, v := range nums { + total += v + } + return +} + +// readAllCPUStat 一次性读取所有 CPU 统计数据,避免多次读取 /proc/stat +// 返回: 总CPU统计、详细CPU统计、每核CPU统计 +func readAllCPUStat() (CPUStat, CPUDetailedStat, []CPUStat) { + data, err := readProcStat() + if err != nil { + return CPUStat{}, CPUDetailedStat{}, nil + } + + lines := strings.Split(string(data), "\n") + if len(lines) == 0 { + return CPUStat{}, CPUDetailedStat{}, nil + } + + // 解析第一行 (总 CPU) + firstLine := lines[0] + nums := parseCPUFields(firstLine) + + // CPUStat + idle, total := calcIdleAndTotal(nums) + cpuStat := CPUStat{Idle: idle, Total: total} + + // CPUDetailedStat - 确保至少有 10 个元素 + if len(nums) < 10 { + padded := make([]uint64, 10) + copy(padded, nums) + nums = padded + } + detailedStat := CPUDetailedStat{ + User: nums[0], + Nice: nums[1], + System: nums[2], + Idle: nums[3], + Iowait: nums[4], + Irq: nums[5], + Softirq: nums[6], + Steal: nums[7], + Guest: nums[8], + GuestNice: nums[9], + } + // 计算总时间 (不包括 guest 和 guest_nice,因为它们已经包含在 user 和 nice 中) + detailedStat.Total = detailedStat.User + detailedStat.Nice + detailedStat.System + + detailedStat.Idle + detailedStat.Iowait + detailedStat.Irq + detailedStat.Softirq + detailedStat.Steal + + // 解析每核 CPU + var perCPUStats []CPUStat + for _, line := range lines[1:] { + if !strings.HasPrefix(line, "cpu") { + continue + } + if len(line) < 4 || line[3] < '0' || line[3] > '9' { + continue + } + + perNums := parseCPUFields(line) + perIdle, perTotal := calcIdleAndTotal(perNums) + perCPUStats = append(perCPUStats, CPUStat{Idle: perIdle, Total: perTotal}) + } + + return cpuStat, detailedStat, perCPUStats +} + +// calcCPUPercent 计算两次采样之间的 CPU 使用率百分比 +func calcCPUPercent(prev, cur CPUStat) float64 { + deltaIdle := float64(cur.Idle - prev.Idle) + deltaTotal := float64(cur.Total - prev.Total) + if deltaTotal <= 0 { + return 0 + } + return (1 - deltaIdle/deltaTotal) * 100 +} + +// calcCPUDetailedPercent 根据两次采样计算各项 CPU 百分比 +func calcCPUDetailedPercent(prev, cur CPUDetailedStat) CPUDetailedPercent { + deltaTotal := float64(cur.Total - prev.Total) + if deltaTotal <= 0 { + return CPUDetailedPercent{Idle: 100} + } + + return CPUDetailedPercent{ + User: float64(cur.User-prev.User) / deltaTotal * 100, + System: float64(cur.System-prev.System) / deltaTotal * 100, + Nice: float64(cur.Nice-prev.Nice) / deltaTotal * 100, + Idle: float64(cur.Idle-prev.Idle) / deltaTotal * 100, + Iowait: float64(cur.Iowait-prev.Iowait) / deltaTotal * 100, + Irq: float64(cur.Irq-prev.Irq) / deltaTotal * 100, + Softirq: float64(cur.Softirq-prev.Softirq) / deltaTotal * 100, + Steal: float64(cur.Steal-prev.Steal) / deltaTotal * 100, + } +} From 5eb7d287ab8db4907b9923831eb1e052b81451e4 Mon Sep 17 00:00:00 2001 From: HynoR <20227709+HynoR@users.noreply.github.com> Date: Wed, 3 Dec 2025 11:46:33 +0800 Subject: [PATCH 2/3] feat: Enhance CPU metrics by adding CPU frequency and detailed usage percentages --- agent/app/dto/dashboard.go | 23 ++++--- agent/app/service/dashboard.go | 7 +- agent/utils/psutil/cpu.go | 85 ++++++++---------------- frontend/src/api/interface/dashboard.ts | 2 + frontend/src/lang/modules/en.ts | 8 +++ frontend/src/lang/modules/es-es.ts | 8 +++ frontend/src/lang/modules/ja.ts | 8 +++ frontend/src/lang/modules/ko.ts | 8 +++ frontend/src/lang/modules/ms.ts | 8 +++ frontend/src/lang/modules/pt-br.ts | 8 +++ frontend/src/lang/modules/ru.ts | 8 +++ frontend/src/lang/modules/tr.ts | 8 +++ frontend/src/lang/modules/zh-Hant.ts | 8 +++ frontend/src/lang/modules/zh.ts | 8 +++ frontend/src/views/home/index.vue | 1 + frontend/src/views/home/status/index.vue | 45 ++++++++++++- 16 files changed, 172 insertions(+), 71 deletions(-) diff --git a/agent/app/dto/dashboard.go b/agent/app/dto/dashboard.go index 44a3045e0204..fd419fba567b 100644 --- a/agent/app/dto/dashboard.go +++ b/agent/app/dto/dashboard.go @@ -19,9 +19,10 @@ type DashboardBase struct { IpV4Addr string `json:"ipV4Addr"` SystemProxy string `json:"systemProxy"` - CPUCores int `json:"cpuCores"` - CPULogicalCores int `json:"cpuLogicalCores"` - CPUModelName string `json:"cpuModelName"` + CPUCores int `json:"cpuCores"` + CPULogicalCores int `json:"cpuLogicalCores"` + CPUModelName string `json:"cpuModelName"` + CPUMhz float64 `json:"cpuMhz"` QuickJumps []QuickJump `json:"quickJump"` CurrentInfo DashboardCurrent `json:"currentInfo"` @@ -58,9 +59,10 @@ type NodeCurrent struct { Load15 float64 `json:"load15"` LoadUsagePercent float64 `json:"loadUsagePercent"` - CPUUsedPercent float64 `json:"cpuUsedPercent"` - CPUUsed float64 `json:"cpuUsed"` - CPUTotal int `json:"cpuTotal"` + CPUUsedPercent float64 `json:"cpuUsedPercent"` + CPUUsed float64 `json:"cpuUsed"` + CPUTotal int `json:"cpuTotal"` + CPUDetailedPercent []float64 `json:"cpuDetailedPercent"` MemoryTotal uint64 `json:"memoryTotal"` MemoryAvailable uint64 `json:"memoryAvailable"` @@ -84,10 +86,11 @@ type DashboardCurrent struct { Load15 float64 `json:"load15"` LoadUsagePercent float64 `json:"loadUsagePercent"` - CPUPercent []float64 `json:"cpuPercent"` - CPUUsedPercent float64 `json:"cpuUsedPercent"` - CPUUsed float64 `json:"cpuUsed"` - CPUTotal int `json:"cpuTotal"` + CPUPercent []float64 `json:"cpuPercent"` + CPUUsedPercent float64 `json:"cpuUsedPercent"` + CPUUsed float64 `json:"cpuUsed"` + CPUTotal int `json:"cpuTotal"` + CPUDetailedPercent []float64 `json:"cpuDetailedPercent"` MemoryTotal uint64 `json:"memoryTotal"` MemoryUsed uint64 `json:"memoryUsed"` diff --git a/agent/app/service/dashboard.go b/agent/app/service/dashboard.go index 09fc09047570..1e67e7147843 100644 --- a/agent/app/service/dashboard.go +++ b/agent/app/service/dashboard.go @@ -108,7 +108,7 @@ func (u *DashboardService) LoadCurrentInfoForNode() *dto.NodeCurrent { currentInfo.CPUTotal, _ = psutil.CPUInfo.GetLogicalCores(false) - cpuUsedPercent, perCore, _ := psutil.CPU.GetCPUUsage() + cpuUsedPercent, perCore, cpuDetailedPercent := psutil.CPU.GetCPUUsage() if len(perCore) == 0 { currentInfo.CPUTotal = psutil.CPU.NumCPU() } else { @@ -116,6 +116,7 @@ func (u *DashboardService) LoadCurrentInfoForNode() *dto.NodeCurrent { } currentInfo.CPUUsedPercent = cpuUsedPercent currentInfo.CPUUsed = cpuUsedPercent * 0.01 * float64(currentInfo.CPUTotal) + currentInfo.CPUDetailedPercent = cpuDetailedPercent loadInfo, _ := load.Avg() currentInfo.Load1 = loadInfo.Load1 @@ -171,6 +172,7 @@ func (u *DashboardService) LoadBaseInfo(ioOption string, netOption string) (*dto baseInfo.CPUCores, _ = psutil.CPUInfo.GetPhysicalCores(false) baseInfo.CPULogicalCores, _ = psutil.CPUInfo.GetLogicalCores(false) + baseInfo.CPUMhz = cpuInfo[0].Mhz baseInfo.CurrentInfo = *u.LoadCurrentInfo(ioOption, netOption) return &baseInfo, nil @@ -184,7 +186,7 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d currentInfo.Procs = hostInfo.Procs currentInfo.CPUTotal, _ = psutil.CPUInfo.GetLogicalCores(false) - cpuUsedPercent, perCore, _ := psutil.CPU.GetCPUUsage() + cpuUsedPercent, perCore, cpuDetailedPercent := psutil.CPU.GetCPUUsage() if len(perCore) == 0 { currentInfo.CPUTotal = psutil.CPU.NumCPU() } else { @@ -193,6 +195,7 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d currentInfo.CPUPercent = perCore currentInfo.CPUUsedPercent = cpuUsedPercent currentInfo.CPUUsed = cpuUsedPercent * 0.01 * float64(currentInfo.CPUTotal) + currentInfo.CPUDetailedPercent = cpuDetailedPercent loadInfo, _ := load.Avg() currentInfo.Load1 = loadInfo.Load1 diff --git a/agent/utils/psutil/cpu.go b/agent/utils/psutil/cpu.go index d652f1b30bef..3f6a26aeb639 100644 --- a/agent/utils/psutil/cpu.go +++ b/agent/utils/psutil/cpu.go @@ -15,41 +15,38 @@ const ( fastInterval = 3 * time.Second ) -// ============================================================================ -// 结构体定义 -// ============================================================================ - type CPUStat struct { Idle uint64 Total uint64 } -// CPUDetailedStat 存储 /proc/stat 中的详细 CPU 时间数据 -// 字段顺序: user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice type CPUDetailedStat struct { - User uint64 // 用户态时间 - Nice uint64 // 低优先级用户态时间 - System uint64 // 内核态时间 - Idle uint64 // 空闲时间 - Iowait uint64 // I/O 等待时间 - Irq uint64 // 硬中断时间 - Softirq uint64 // 软中断时间 - Steal uint64 // 虚拟化环境中被其他 OS 占用的时间 - Guest uint64 // 运行虚拟 CPU 的时间 - GuestNice uint64 // 运行低优先级虚拟 CPU 的时间 - Total uint64 // 总时间 + User uint64 + Nice uint64 + System uint64 + Idle uint64 + Iowait uint64 + Irq uint64 + Softirq uint64 + Steal uint64 + Guest uint64 + GuestNice uint64 + Total uint64 } -// CPUDetailedPercent 存储类似 top 命令的 CPU 百分比信息 type CPUDetailedPercent struct { - User float64 `json:"user"` // %us - 用户空间占用 - System float64 `json:"system"` // %sy - 内核空间占用 - Nice float64 `json:"nice"` // %ni - 改变过优先级的进程占用 - Idle float64 `json:"idle"` // %id - 空闲 - Iowait float64 `json:"iowait"` // %wa - I/O 等待 - Irq float64 `json:"irq"` // %hi - 硬中断 - Softirq float64 `json:"softirq"` // %si - 软中断 - Steal float64 `json:"steal"` // %st - 虚拟机偷取 + User float64 `json:"user"` + System float64 `json:"system"` + Nice float64 `json:"nice"` + Idle float64 `json:"idle"` + Iowait float64 `json:"iowait"` + Irq float64 `json:"irq"` + Softirq float64 `json:"softirq"` + Steal float64 `json:"steal"` +} + +func (c *CPUDetailedPercent) GetCPUDetailedPercent() []float64 { + return []float64{c.User, c.System, c.Nice, c.Idle, c.Iowait, c.Irq, c.Softirq, c.Steal} } type CPUUsageState struct { @@ -72,13 +69,7 @@ type CPUInfoState struct { cachedLogicCores int } -// ============================================================================ -// CPUUsageState 公有方法 -// ============================================================================ - -// GetCPUUsage 返回 CPU 使用率、每核使用率和详细百分比信息 -// 返回: totalUsage, perCoreUsage, detailedPercent (类似 top 命令的 %us, %sy, %ni, %id, %wa, %hi, %si, %st) -func (c *CPUUsageState) GetCPUUsage() (float64, []float64, CPUDetailedPercent) { +func (c *CPUUsageState) GetCPUUsage() (float64, []float64, []float64) { c.mu.Lock() now := time.Now() @@ -88,7 +79,7 @@ func (c *CPUUsageState) GetCPUUsage() (float64, []float64, CPUDetailedPercent) { perCore := c.cachedPerCore detailed := c.cachedDetailedPercent c.mu.Unlock() - return result, perCore, detailed + return result, perCore, detailed.GetCPUDetailedPercent() } needReset := c.lastSampleTime.IsZero() || now.Sub(c.lastSampleTime) >= resetInterval @@ -117,7 +108,7 @@ func (c *CPUUsageState) GetCPUUsage() (float64, []float64, CPUDetailedPercent) { c.lastSampleTime = time.Now() c.mu.Unlock() - return totalUsage, perCore, detailedPercent + return totalUsage, perCore, detailedPercent.GetCPUDetailedPercent() } curTotal, curDetail, curPer := readAllCPUStat() @@ -143,7 +134,7 @@ func (c *CPUUsageState) GetCPUUsage() (float64, []float64, CPUDetailedPercent) { c.lastPerCPUStat = curPer c.lastSampleTime = time.Now() - return totalUsage, c.cachedPerCore, detailedPercent + return totalUsage, c.cachedPerCore, detailedPercent.GetCPUDetailedPercent() } func (c *CPUUsageState) NumCPU() int { @@ -153,10 +144,6 @@ func (c *CPUUsageState) NumCPU() int { return len(c.cachedPerCore) } -// ============================================================================ -// CPUInfoState 公有方法 -// ============================================================================ - func (c *CPUInfoState) GetCPUInfo(forceRefresh bool) ([]cpu.InfoStat, error) { c.mu.RLock() if c.initialized && c.cachedInfo != nil && !forceRefresh { @@ -220,22 +207,16 @@ func (c *CPUInfoState) GetLogicalCores(forceRefresh bool) (int, error) { return cores, nil } -// ============================================================================ -// 私有函数 -// ============================================================================ - -// readProcStat 读取 /proc/stat 文件内容 func readProcStat() ([]byte, error) { return os.ReadFile("/proc/stat") } -// parseCPUFields 解析 CPU 行的数值字段 func parseCPUFields(line string) []uint64 { fields := strings.Fields(line) if len(fields) <= 1 { return nil } - fields = fields[1:] // 跳过 "cpu" 或 "cpuN" 前缀 + fields = fields[1:] nums := make([]uint64, len(fields)) for i, f := range fields { @@ -245,7 +226,6 @@ func parseCPUFields(line string) []uint64 { return nums } -// calcIdleAndTotal 计算空闲时间和总时间 func calcIdleAndTotal(nums []uint64) (idle, total uint64) { if len(nums) < 5 { return 0, 0 @@ -257,8 +237,6 @@ func calcIdleAndTotal(nums []uint64) (idle, total uint64) { return } -// readAllCPUStat 一次性读取所有 CPU 统计数据,避免多次读取 /proc/stat -// 返回: 总CPU统计、详细CPU统计、每核CPU统计 func readAllCPUStat() (CPUStat, CPUDetailedStat, []CPUStat) { data, err := readProcStat() if err != nil { @@ -270,15 +248,12 @@ func readAllCPUStat() (CPUStat, CPUDetailedStat, []CPUStat) { return CPUStat{}, CPUDetailedStat{}, nil } - // 解析第一行 (总 CPU) firstLine := lines[0] nums := parseCPUFields(firstLine) - // CPUStat idle, total := calcIdleAndTotal(nums) cpuStat := CPUStat{Idle: idle, Total: total} - // CPUDetailedStat - 确保至少有 10 个元素 if len(nums) < 10 { padded := make([]uint64, 10) copy(padded, nums) @@ -296,11 +271,9 @@ func readAllCPUStat() (CPUStat, CPUDetailedStat, []CPUStat) { Guest: nums[8], GuestNice: nums[9], } - // 计算总时间 (不包括 guest 和 guest_nice,因为它们已经包含在 user 和 nice 中) detailedStat.Total = detailedStat.User + detailedStat.Nice + detailedStat.System + detailedStat.Idle + detailedStat.Iowait + detailedStat.Irq + detailedStat.Softirq + detailedStat.Steal - // 解析每核 CPU var perCPUStats []CPUStat for _, line := range lines[1:] { if !strings.HasPrefix(line, "cpu") { @@ -318,7 +291,6 @@ func readAllCPUStat() (CPUStat, CPUDetailedStat, []CPUStat) { return cpuStat, detailedStat, perCPUStats } -// calcCPUPercent 计算两次采样之间的 CPU 使用率百分比 func calcCPUPercent(prev, cur CPUStat) float64 { deltaIdle := float64(cur.Idle - prev.Idle) deltaTotal := float64(cur.Total - prev.Total) @@ -328,7 +300,6 @@ func calcCPUPercent(prev, cur CPUStat) float64 { return (1 - deltaIdle/deltaTotal) * 100 } -// calcCPUDetailedPercent 根据两次采样计算各项 CPU 百分比 func calcCPUDetailedPercent(prev, cur CPUDetailedStat) CPUDetailedPercent { deltaTotal := float64(cur.Total - prev.Total) if deltaTotal <= 0 { diff --git a/frontend/src/api/interface/dashboard.ts b/frontend/src/api/interface/dashboard.ts index ee5c1a9f3977..e8f41f20a69f 100644 --- a/frontend/src/api/interface/dashboard.ts +++ b/frontend/src/api/interface/dashboard.ts @@ -61,6 +61,7 @@ export namespace Dashboard { cpuCores: number; cpuLogicalCores: number; cpuModelName: string; + cpuMhz: number; currentInfo: CurrentInfo; quickJump: Array; @@ -79,6 +80,7 @@ export namespace Dashboard { cpuUsedPercent: number; cpuUsed: number; cpuTotal: number; + cpuDetailedPercent: Array; // [user, system, nice, idle, iowait, irq, softirq, steal] memoryTotal: number; memoryAvailable: number; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index acea6e869d00..f88db0046f0c 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -439,6 +439,14 @@ const message = { core: 'Physical core', logicCore: 'Logical core', corePercent: 'Core Usage', + cpuFrequency: 'CPU Frequency', + cpuDetailedPercent: 'CPU Usage Detail', + cpuUser: 'User', + cpuSystem: 'System', + cpuIdle: 'Idle', + cpuIrq: 'IRQ', + cpuSoftirq: 'Soft IRQ', + cpuSteal: 'Steal', cpuTop: 'Top 5 Processes by CPU Usage', memTop: 'Top 5 Processes by Memory Usage', loadAverage: 'Load average in the last 1 minute | Load average in the last {n} minutes', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 48cac0e4fca4..17feca9638b6 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -444,6 +444,14 @@ const message = { core: 'Núcleo físico', logicCore: 'Núcleo lógico', corePercent: 'Uso del Núcleo', + cpuFrequency: 'Frecuencia CPU', + cpuDetailedPercent: 'Distribución del Tiempo de CPU', + cpuUser: 'Usuario', + cpuSystem: 'Sistema', + cpuIdle: 'Inactivo', + cpuIrq: 'IRQ', + cpuSoftirq: 'Soft IRQ', + cpuSteal: 'Robado', cpuTop: 'Top 5 de Procesos por Uso de CPU', memTop: 'Top 5 de Procesos por Uso de Memoria', loadAverage: 'Promedio de carga en el último minuto | Promedio de carga en los últimos {n} minutos', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 80dc20fdbe8d..d95600c563dd 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -427,6 +427,14 @@ const message = { core: '物理コア', logicCore: '論理コア', corePercent: 'コア使用率', + cpuFrequency: 'CPU 周波数', + cpuDetailedPercent: 'CPU 占有', + cpuUser: 'ユーザー', + cpuSystem: 'システム', + cpuIdle: 'アイドル', + cpuIrq: 'IRQ', + cpuSoftirq: 'Soft IRQ', + cpuSteal: 'Steal', cpuTop: 'CPU使用率トップ5のプロセス情報', memTop: 'メモリ使用率トップ5のプロセス情報', loadAverage: '最後の1分で平均を積み込みます|最後の{n}分で平均を読み込みます', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 4a495a8e493a..d60c2599dac0 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -430,6 +430,14 @@ const message = { core: '물리적 코어', logicCore: '논리 코어', corePercent: '코어 사용률', + cpuFrequency: 'CPU 주파수', + cpuDetailedPercent: 'CPU 사용률 상세', + cpuUser: '사용자', + cpuSystem: '시스템', + cpuIdle: '유휴', + cpuIrq: 'IRQ', + cpuSoftirq: 'Soft IRQ', + cpuSteal: 'Steal', cpuTop: 'CPU 사용률 상위 5개 프로세스 정보', memTop: '메모리 사용률 상위 5개 프로세스 정보', loadAverage: '지난 1분의 평균 부하 | 지난 {n} 분의 평균 부하', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 117c685b9fa0..eb70acabdda1 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -435,6 +435,14 @@ const message = { core: 'Teras Fizikal', corePercent: 'Penggunaan Teras', + cpuFrequency: 'Frekuensi CPU', + cpuDetailedPercent: 'Pengagihan Masa CPU', + cpuUser: 'Pengguna', + cpuSystem: 'Sistem', + cpuIdle: 'Tidak Aktif', + cpuIrq: 'IRQ', + cpuSoftirq: 'Soft IRQ', + cpuSteal: 'Dicuri', cpuTop: 'Maklumat Proses 5 Teratas Mengikut Penggunaan CPU', memTop: 'Maklumat Proses 5 Teratas Mengikut Penggunaan Memori', logicCore: 'Teras Logik', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index b2a81a3a6eb9..4bd49d85f5cf 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -434,6 +434,14 @@ const message = { core: 'Núcleo físico', logicCore: 'Núcleo lógico', corePercent: 'Uso do Núcleo', + cpuFrequency: 'Frequência CPU', + cpuDetailedPercent: 'Distribuição do Tempo de CPU', + cpuUser: 'Usuário', + cpuSystem: 'Sistema', + cpuIdle: 'Ocioso', + cpuIrq: 'IRQ', + cpuSoftirq: 'Soft IRQ', + cpuSteal: 'Roubado', cpuTop: 'Top 5 Processos por Uso de CPU', memTop: 'Top 5 Processos por Uso de Memória', loadAverage: 'Média de carga nos últimos 1 minuto | Média de carga nos últimos {n} minutos', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 220a16f008ae..45167d7ee0c1 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -431,6 +431,14 @@ const message = { core: 'Физических ядер', logicCore: 'Логических ядер', corePercent: 'Использование Ядра', + cpuFrequency: 'Частота CPU', + cpuDetailedPercent: 'Распределение Времени CPU', + cpuUser: 'Пользователь', + cpuSystem: 'Система', + cpuIdle: 'Простой', + cpuIrq: 'IRQ', + cpuSoftirq: 'Soft IRQ', + cpuSteal: 'Steal', cpuTop: 'Топ 5 Процессов по Использованию ЦПУ', memTop: 'Топ 5 Процессов по Использованию Памяти', loadAverage: 'Средняя нагрузка за последнюю минуту | Средняя нагрузка за последние {n} минут', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 6bcc8dc3c658..97fa9fd0f6ec 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -442,6 +442,14 @@ const message = { core: 'Fiziksel çekirdek', logicCore: 'Mantıksal çekirdek', corePercent: 'Çekirdek Kullanımı', + cpuFrequency: 'CPU Frekansı', + cpuDetailedPercent: 'CPU Zaman Dağılımı', + cpuUser: 'Kullanıcı', + cpuSystem: 'Sistem', + cpuIdle: 'Boşta', + cpuIrq: 'IRQ', + cpuSoftirq: 'Soft IRQ', + cpuSteal: 'Çalıntı', cpuTop: 'CPU Kullanımına Göre İlk 5 İşlem', memTop: 'Bellek Kullanımına Göre İlk 5 İşlem', loadAverage: 'Son 1 dakikadaki yük ortalaması | Son {n} dakikadaki yük ortalaması', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index ea2e61c175cf..507b4d1c464c 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -425,6 +425,14 @@ const message = { core: '物理核心', logicCore: '邏輯核心', corePercent: '核心使用率', + cpuFrequency: 'CPU 頻率', + cpuDetailedPercent: 'CPU 佔用', + cpuUser: '用戶態', + cpuSystem: '內核態', + cpuIdle: '空閒', + cpuIrq: '硬中斷', + cpuSoftirq: '軟中斷', + cpuSteal: '被VM搶佔', cpuTop: 'CPU 佔用率 Top5 的行程資訊', memTop: '記憶體佔用率 Top5 的行程資訊', loadAverage: '最近 {0} 分鐘平均負載', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index dcb8caa8f24c..f3394fe8626e 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -426,6 +426,14 @@ const message = { core: '物理核心', logicCore: '逻辑核心', corePercent: '核心使用率', + cpuFrequency: 'CPU 频率', + cpuDetailedPercent: 'CPU 占用', + cpuUser: '用户态', + cpuSystem: '内核态', + cpuIdle: '空闲', + cpuIrq: '硬中断', + cpuSoftirq: '软中断', + cpuSteal: '被VM抢占', cpuTop: 'CPU 占用率 Top5 的进程信息', memTop: '内存占用率 Top5 的进程信息', loadAverage: '最近 {0} 分钟平均负载', diff --git a/frontend/src/views/home/index.vue b/frontend/src/views/home/index.vue index c953eb75dd5b..94510ed71ffa 100644 --- a/frontend/src/views/home/index.vue +++ b/frontend/src/views/home/index.vue @@ -420,6 +420,7 @@ const currentInfo = ref({ cpuUsedPercent: 0, cpuUsed: 0, cpuTotal: 0, + cpuDetailedPercent: [] as Array, memoryTotal: 0, memoryAvailable: 0, diff --git a/frontend/src/views/home/status/index.vue b/frontend/src/views/home/status/index.vue index 5a94470a83d0..fa2d69f53b53 100644 --- a/frontend/src/views/home/status/index.vue +++ b/frontend/src/views/home/status/index.vue @@ -58,13 +58,16 @@ v-if="chartsOption['cpu']" @hide="onCpuPopoverHide" > - + {{ baseInfo.cpuCores }} {{ baseInfo.cpuLogicalCores }} + + {{ formatNumber(baseInfo.cpuMhz) }} MHz + @@ -81,7 +84,42 @@ -
+ +
+ + {{ $t('home.cpuDetailedPercent') }} + + + + +
+ {{ $t('home.cpuUser') }}: {{ formatNumber(currentInfo.cpuDetailedPercent[0]) }}% +
+
+ {{ $t('home.cpuSystem') }}: {{ formatNumber(currentInfo.cpuDetailedPercent[1]) }}% +
+
Nice: {{ formatNumber(currentInfo.cpuDetailedPercent[2]) }}%
+
+ {{ $t('home.cpuIdle') }}: {{ formatNumber(currentInfo.cpuDetailedPercent[3]) }}% +
+
I/O: {{ formatNumber(currentInfo.cpuDetailedPercent[4]) }}%
+
+ {{ $t('home.cpuIrq') }}: {{ formatNumber(currentInfo.cpuDetailedPercent[5]) }}% +
+
+ {{ $t('home.cpuSoftirq') }}: {{ formatNumber(currentInfo.cpuDetailedPercent[6]) }}% +
+
+ {{ $t('home.cpuSteal') }}: {{ formatNumber(currentInfo.cpuDetailedPercent[7]) }}% +
+
+
{{ $t('home.cpuTop') }} @@ -377,6 +415,7 @@ const baseInfo = ref({ cpuCores: 0, cpuLogicalCores: 0, cpuModelName: '', + cpuMhz: 0, currentInfo: null, quickJump: [], }); @@ -394,6 +433,7 @@ const currentInfo = ref({ cpuUsedPercent: 0, cpuUsed: 0, cpuTotal: 0, + cpuDetailedPercent: [] as Array, memoryTotal: 0, memoryAvailable: 0, @@ -426,6 +466,7 @@ const currentInfo = ref({ }); const cpuShowAll = ref(); +const showCpuDetailedPercent = ref(false); const showCpuTop = ref(false); const showMemTop = ref(false); const killProcessID = ref(); From 6a9d34f08022d2c183108f259c48f56fee48d73f Mon Sep 17 00:00:00 2001 From: HynoR <20227709+HynoR@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:30:36 +0800 Subject: [PATCH 3/3] feat: Add CPU frequency metric to dashboard base information --- frontend/src/views/home/index.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/views/home/index.vue b/frontend/src/views/home/index.vue index 94510ed71ffa..8cae0134981b 100644 --- a/frontend/src/views/home/index.vue +++ b/frontend/src/views/home/index.vue @@ -402,6 +402,7 @@ const baseInfo = ref({ cpuCores: 0, cpuLogicalCores: 0, cpuModelName: '', + cpuMhz: 0, currentInfo: null, quickJump: [],