Skip to content

Commit 11380f9

Browse files
nberleesmira
authored andcommitted
feat: display current CPU frequency on dashboard
Dashboard now shows the active frequency of each CPU core when cpufreq is available on non-virtualized systems, enhancing real-time accuracy. Solves the issue of displaying 0MHz on certain SBCs due to /proc/cpuinfo limitations. Signed-off-by: Nico Berlee <[email protected]> Signed-off-by: Andrey Smirnov <[email protected]>
1 parent fbce267 commit 11380f9

File tree

11 files changed

+2467
-1403
lines changed

11 files changed

+2467
-1403
lines changed

api/machine/machine.proto

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ service MachineService {
2121
rpc Bootstrap(BootstrapRequest) returns (BootstrapResponse);
2222
rpc Containers(ContainersRequest) returns (ContainersResponse);
2323
rpc Copy(CopyRequest) returns (stream common.Data);
24+
rpc CPUFreqStats(google.protobuf.Empty) returns (CPUFreqStatsResponse);
2425
rpc CPUInfo(google.protobuf.Empty) returns (CPUInfoResponse);
2526
rpc DiskStats(google.protobuf.Empty) returns (DiskStatsResponse);
2627
rpc Dmesg(DmesgRequest) returns (stream common.Data);
@@ -843,6 +844,24 @@ message SoftIRQStat {
843844
uint64 rcu = 10;
844845
}
845846

847+
// rpc CPUFreqStats
848+
849+
message CPUFreqStatsResponse {
850+
repeated CPUsFreqStats messages = 1;
851+
}
852+
853+
message CPUsFreqStats {
854+
common.Metadata metadata = 1;
855+
repeated CPUFreqStats cpu_freq_stats = 2;
856+
}
857+
858+
message CPUFreqStats {
859+
uint64 current_frequency = 1;
860+
uint64 minimum_frequency = 2;
861+
uint64 maximum_frequency = 3;
862+
string governor = 4;
863+
}
864+
846865
// rpc CPUInfo
847866

848867
message CPUInfoResponse {

internal/app/machined/internal/server/v1alpha1/v1alpha1_monitoring.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"strings"
1313

1414
"github.com/prometheus/procfs"
15+
"github.com/prometheus/procfs/sysfs"
1516
"github.com/siderolabs/gen/maps"
1617
"github.com/siderolabs/gen/xslices"
1718
"google.golang.org/protobuf/types/known/emptypb"
@@ -141,6 +142,42 @@ func (s *Server) SystemStat(ctx context.Context, in *emptypb.Empty) (*machine.Sy
141142
return reply, nil
142143
}
143144

145+
// CPUFreqStats implements the machine.MachineServer interface.
146+
func (s *Server) CPUFreqStats(ctx context.Context, in *emptypb.Empty) (*machine.CPUFreqStatsResponse, error) {
147+
fs, err := sysfs.NewDefaultFS()
148+
if err != nil {
149+
return nil, err
150+
}
151+
152+
systemCpufreqStats, err := fs.SystemCpufreq()
153+
if err != nil {
154+
return nil, err
155+
}
156+
157+
translateCPUFreqStats := func(in sysfs.SystemCPUCpufreqStats) *machine.CPUFreqStats {
158+
if in.CpuinfoCurrentFrequency == nil || in.CpuinfoMinimumFrequency == nil || in.CpuinfoMaximumFrequency == nil {
159+
return &machine.CPUFreqStats{}
160+
}
161+
162+
return &machine.CPUFreqStats{
163+
CurrentFrequency: *in.CpuinfoCurrentFrequency,
164+
MinimumFrequency: *in.CpuinfoMinimumFrequency,
165+
MaximumFrequency: *in.CpuinfoMaximumFrequency,
166+
Governor: in.Governor,
167+
}
168+
}
169+
170+
reply := &machine.CPUFreqStatsResponse{
171+
Messages: []*machine.CPUsFreqStats{
172+
{
173+
CpuFreqStats: xslices.Map(systemCpufreqStats, translateCPUFreqStats),
174+
},
175+
},
176+
}
177+
178+
return reply, nil
179+
}
180+
144181
// CPUInfo implements the machine.MachineServer interface.
145182
func (s *Server) CPUInfo(ctx context.Context, in *emptypb.Empty) (*machine.CPUInfoResponse, error) {
146183
fs, err := procfs.NewDefaultFS()

internal/app/machined/pkg/system/services/machined.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ var rules = map[string]role.Set{
4141
"/machine.MachineService/ApplyConfiguration": role.MakeSet(role.Admin),
4242
"/machine.MachineService/Bootstrap": role.MakeSet(role.Admin),
4343
"/machine.MachineService/CPUInfo": role.MakeSet(role.Admin, role.Operator, role.Reader),
44+
"/machine.MachineService/CPUFreqStats": role.MakeSet(role.Admin, role.Operator, role.Reader),
4445
"/machine.MachineService/Containers": role.MakeSet(role.Admin, role.Operator, role.Reader),
4546
"/machine.MachineService/Copy": role.MakeSet(role.Admin),
4647
"/machine.MachineService/DiskStats": role.MakeSet(role.Admin, role.Operator, role.Reader),
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
//go:build integration_api
6+
7+
package api
8+
9+
import (
10+
"context"
11+
"time"
12+
13+
"google.golang.org/protobuf/types/known/emptypb"
14+
15+
"github.com/siderolabs/talos/internal/integration/base"
16+
"github.com/siderolabs/talos/pkg/machinery/client"
17+
)
18+
19+
// MonitoringSuite ...
20+
type MonitoringSuite struct {
21+
base.APISuite
22+
23+
ctx context.Context //nolint:containedctx
24+
ctxCancel context.CancelFunc
25+
}
26+
27+
// SuiteName ...
28+
func (suite *MonitoringSuite) SuiteName() string {
29+
return "api.MonitoringSuite"
30+
}
31+
32+
// SetupTest ...
33+
func (suite *MonitoringSuite) SetupTest() {
34+
suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), 30*time.Second)
35+
}
36+
37+
// TearDownTest ...
38+
func (suite *MonitoringSuite) TearDownTest() {
39+
if suite.ctxCancel != nil {
40+
suite.ctxCancel()
41+
}
42+
}
43+
44+
// TestMonitoringAPIs tests that monitoring APIs are working.
45+
func (suite *MonitoringSuite) TestMonitoringAPIs() {
46+
node := suite.RandomDiscoveredNodeInternalIP()
47+
nodeCtx := client.WithNode(suite.ctx, node)
48+
49+
_, err := suite.Client.MachineClient.CPUFreqStats(nodeCtx, &emptypb.Empty{})
50+
suite.Require().NoError(err)
51+
52+
_, err = suite.Client.MachineClient.CPUInfo(nodeCtx, &emptypb.Empty{})
53+
suite.Require().NoError(err)
54+
55+
_, err = suite.Client.MachineClient.DiskStats(nodeCtx, &emptypb.Empty{})
56+
suite.Require().NoError(err)
57+
58+
_, err = suite.Client.MachineClient.LoadAvg(nodeCtx, &emptypb.Empty{})
59+
suite.Require().NoError(err)
60+
61+
_, err = suite.Client.MachineClient.Memory(nodeCtx, &emptypb.Empty{})
62+
suite.Require().NoError(err)
63+
64+
_, err = suite.Client.MachineClient.NetworkDeviceStats(nodeCtx, &emptypb.Empty{})
65+
suite.Require().NoError(err)
66+
67+
_, err = suite.Client.MachineClient.SystemStat(nodeCtx, &emptypb.Empty{})
68+
suite.Require().NoError(err)
69+
}
70+
71+
func init() {
72+
allSuites = append(allSuites, new(MonitoringSuite))
73+
}

internal/pkg/dashboard/apidata/node.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,17 @@ import (
1313
// Node represents data gathered from a single node.
1414
type Node struct {
1515
// These fields are directly API responses.
16-
Hostname *machine.Hostname
17-
LoadAvg *machine.LoadAvg
18-
Version *machine.Version
19-
Memory *machine.Memory
20-
SystemStat *machine.SystemStat
21-
CPUsInfo *machine.CPUsInfo
22-
NetDevStats *machine.NetworkDeviceStats
23-
DiskStats *machine.DiskStats
24-
Processes *machine.Process
25-
ServiceList *machine.ServiceList
16+
Hostname *machine.Hostname
17+
LoadAvg *machine.LoadAvg
18+
Version *machine.Version
19+
Memory *machine.Memory
20+
SystemStat *machine.SystemStat
21+
CPUsFreqStats *machine.CPUsFreqStats
22+
CPUsInfo *machine.CPUsInfo
23+
NetDevStats *machine.NetworkDeviceStats
24+
DiskStats *machine.DiskStats
25+
Processes *machine.Process
26+
ServiceList *machine.ServiceList
2627

2728
// These fields are calculated as diff with Node data from previous pol.
2829
SystemStatDiff *machine.SystemStat

internal/pkg/dashboard/apidata/source.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,27 @@ func (source *Source) gather() *Data {
179179

180180
return nil
181181
},
182+
func() error {
183+
resp, err := source.MachineClient.CPUFreqStats(source.ctx, &emptypb.Empty{})
184+
if err != nil {
185+
return err
186+
}
187+
188+
resultLock.Lock()
189+
defer resultLock.Unlock()
190+
191+
for _, msg := range resp.GetMessages() {
192+
node := source.node(msg)
193+
194+
if _, ok := result.Nodes[node]; !ok {
195+
result.Nodes[node] = &Node{}
196+
}
197+
198+
result.Nodes[node].CPUsFreqStats = msg
199+
}
200+
201+
return nil
202+
},
182203
func() error {
183204
resp, err := source.MachineClient.CPUInfo(source.ctx, &emptypb.Empty{})
184205
if err != nil {

internal/pkg/dashboard/components/header.go

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package components
77
import (
88
"fmt"
99
"math"
10+
"sort"
1011
"strconv"
1112
"time"
1213

@@ -24,7 +25,6 @@ type headerData struct {
2425
hostname string
2526
version string
2627
uptime string
27-
numCPUs string
2828
cpuFreq string
2929
totalMem string
3030
numProcesses string
@@ -107,11 +107,10 @@ func (widget *Header) redraw() {
107107
data := widget.getOrCreateNodeData(widget.selectedNode)
108108

109109
text := fmt.Sprintf(
110-
"[yellow::b]%s[-:-:-] (%s): uptime %s, %sx%s, %s RAM, PROCS %s, CPU %s, RAM %s",
110+
"[yellow::b]%s[-:-:-] (%s): uptime %s, %s, %s RAM, PROCS %s, CPU %s, RAM %s",
111111
data.hostname,
112112
data.version,
113113
data.uptime,
114-
data.numCPUs,
115114
data.cpuFreq,
116115
data.totalMem,
117116
data.numProcesses,
@@ -122,6 +121,7 @@ func (widget *Header) redraw() {
122121
widget.SetText(text)
123122
}
124123

124+
//nolint:gocyclo
125125
func (widget *Header) updateNodeAPIData(node string, data *apidata.Node) {
126126
nodeData := widget.getOrCreateNodeData(node)
127127

@@ -147,13 +147,46 @@ func (widget *Header) updateNodeAPIData(node string, data *apidata.Node) {
147147
if data.CPUsInfo != nil {
148148
numCPUs := len(data.CPUsInfo.GetCpuInfo())
149149

150-
nodeData.numCPUs = strconv.Itoa(numCPUs)
151-
152150
if numCPUs > 0 {
151+
nodeData.cpuFreq = fmt.Sprintf("%dx%s", numCPUs, widget.humanizeCPUFrequency(data.CPUsInfo.GetCpuInfo()[0].GetCpuMhz()))
152+
} else {
153153
nodeData.cpuFreq = widget.humanizeCPUFrequency(data.CPUsInfo.GetCpuInfo()[0].GetCpuMhz())
154154
}
155155
}
156156

157+
if data.CPUsFreqStats != nil && data.CPUsFreqStats.CpuFreqStats != nil {
158+
numCPUs := len(data.CPUsFreqStats.CpuFreqStats)
159+
uniqMhz := make(map[uint64]int, numCPUs)
160+
161+
for _, cpuFreqStat := range data.CPUsFreqStats.CpuFreqStats {
162+
uniqMhz[cpuFreqStat.CurrentFrequency]++
163+
}
164+
165+
keys := make([]uint64, 0, len(uniqMhz))
166+
167+
for mhz := range uniqMhz {
168+
if mhz == 0 {
169+
continue
170+
}
171+
172+
keys = append(keys, mhz)
173+
}
174+
175+
if len(keys) > 0 {
176+
sort.Slice(keys, func(i, j int) bool { return keys[i] > keys[j] })
177+
178+
nodeData.cpuFreq = ""
179+
}
180+
181+
for i, mhz := range keys {
182+
if i > 0 {
183+
nodeData.cpuFreq += " "
184+
}
185+
186+
nodeData.cpuFreq += fmt.Sprintf("%dx%s", uniqMhz[mhz], widget.humanizeCPUFrequency(float64(mhz)/1000.0))
187+
}
188+
}
189+
157190
if data.Processes != nil {
158191
nodeData.numProcesses = strconv.Itoa(len(data.Processes.GetProcesses()))
159192
}
@@ -170,7 +203,6 @@ func (widget *Header) getOrCreateNodeData(node string) *headerData {
170203
hostname: notAvailable,
171204
version: notAvailable,
172205
uptime: notAvailable,
173-
numCPUs: notAvailable,
174206
cpuFreq: notAvailable,
175207
totalMem: notAvailable,
176208
numProcesses: notAvailable,

0 commit comments

Comments
 (0)