Skip to content

Commit 71a13b8

Browse files
authored
Merge pull request #2011 from sthaha/feat-fake-cpu-meter
chore(device): add fake CPU power meter for testing
2 parents 2bde02f + 55cbc56 commit 71a13b8

File tree

5 files changed

+425
-4
lines changed

5 files changed

+425
-4
lines changed

cmd/kepler/main.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,8 @@ func createServices(logger *slog.Logger, cfg *config.Config) ([]service.Service,
167167

168168
func createPowerMonitor(logger *slog.Logger, cfg *config.Config) (*monitor.PowerMonitor, error) {
169169
logger.Debug("Creating PowerMonitor")
170-
cpuPowerMeter, err := device.NewCPUPowerMeter(cfg.Host.SysFS)
170+
171+
cpuPowerMeter, err := createCPUMeter(logger, cfg)
171172
if err != nil {
172173
return nil, fmt.Errorf("failed to create CPU power meter: %w", err)
173174
}
@@ -179,3 +180,10 @@ func createPowerMonitor(logger *slog.Logger, cfg *config.Config) (*monitor.Power
179180

180181
return pm, nil
181182
}
183+
184+
func createCPUMeter(logger *slog.Logger, cfg *config.Config) (device.CPUPowerMeter, error) {
185+
if fake := cfg.Dev.FakeCpuMeter; fake.Enabled {
186+
return device.NewFakeCPUMeter(fake.Zones, device.WithFakeLogger(logger))
187+
}
188+
return device.NewCPUPowerMeter(cfg.Host.SysFS)
189+
}

config/config.go

+11
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,18 @@ type (
2424
ProcFS string `yaml:"procfs"`
2525
}
2626

27+
// Development mode settings; disabled by default
28+
Dev struct {
29+
FakeCpuMeter struct {
30+
Enabled bool `yaml:"enabled"`
31+
Zones []string `yaml:"zones"`
32+
} `yaml:"fake-cpu-meter"`
33+
}
34+
2735
Config struct {
2836
Log Log `yaml:"log"`
2937
Host Host `yaml:"host"`
38+
Dev Dev `yaml:"dev"` // WARN: do not expose dev settings as flags
3039
}
3140
)
3241

@@ -36,6 +45,8 @@ const (
3645
LogFormatFlag = "log.format"
3746
HostSysFSFlag = "host.sysfs"
3847
HostProcFSFlag = "host.procfs"
48+
49+
// WARN: dev settings shouldn't be exposed as flags as flags are intended for end users
3950
)
4051

4152
// DefaultConfig returns a Config with default values

hack/config.yaml

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
log:
2-
level: debug # debug, info, warn, error (default: info)
3-
format: text # text or json (default: text)
2+
level: debug # debug, info, warn, error (default: info)
3+
format: text # text or json (default: text)
44

55
host:
6-
sysfs: /sys # Path to sysfs filesystem (default: /sys)
6+
sysfs: /sys # Path to sysfs filesystem (default: /sys)
77
procfs: /proc # Path to procfs filesystem (default: /proc)
8+
9+
# WARN DO NOT ENABLE THIS IN PRODUCTION - for development / testing only
10+
dev:
11+
fake-cpu-meter:
12+
enabled: false
13+
zones: [] # zones to be enabled, empty enables all default zones
+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// SPDX-FileCopyrightText: 2025 The Kepler Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package device
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"log/slog"
10+
"math/rand"
11+
"path/filepath"
12+
"sync"
13+
)
14+
15+
// NOTE: This fake meter is not intended to be used in production and is for testing only
16+
17+
type Zone = string
18+
19+
const (
20+
ZonePackage Zone = "package"
21+
ZoneCore Zone = "core"
22+
ZoneDRAM Zone = "dram"
23+
ZoneUncore Zone = "uncore"
24+
)
25+
26+
var defaultFakeZones = []Zone{ZonePackage, ZoneCore, ZoneDRAM}
27+
28+
const defaultRaplPath = "/sys/class/powercap/intel-rapl"
29+
30+
// fakeEnergyZone implements the EnergyZone interface
31+
type fakeEnergyZone struct {
32+
name string
33+
index int
34+
path string
35+
energy Energy
36+
maxEnergy Energy
37+
mu sync.Mutex
38+
39+
// For generating fake values
40+
increment Energy
41+
randomFactor float64
42+
}
43+
44+
var _ EnergyZone = (*fakeEnergyZone)(nil)
45+
46+
// Name returns the zone name
47+
func (z *fakeEnergyZone) Name() string {
48+
return z.name
49+
}
50+
51+
// Index returns the index of the zone
52+
func (z *fakeEnergyZone) Index() int {
53+
return z.index
54+
}
55+
56+
// Path returns the path from which the energy usage value ie being read
57+
func (z *fakeEnergyZone) Path() string {
58+
return z.path
59+
}
60+
61+
// Energy returns energy consumed by the zone.
62+
func (z *fakeEnergyZone) Energy() (Energy, error) {
63+
z.mu.Lock()
64+
defer z.mu.Unlock()
65+
66+
randomComponent := Energy(rand.Float64() * float64(z.increment) * z.randomFactor)
67+
z.energy = (z.energy + z.increment + randomComponent) % z.maxEnergy
68+
69+
return z.energy, nil
70+
}
71+
72+
// MaxEnergy returns the maximum value of energy usage that can be read.
73+
func (z *fakeEnergyZone) MaxEnergy() Energy {
74+
return z.maxEnergy
75+
}
76+
77+
// fakeRaplMeter implements the CPUPowerMeter interface
78+
type fakeRaplMeter struct {
79+
logger *slog.Logger
80+
zones []EnergyZone
81+
devicePath string
82+
}
83+
84+
var _ CPUPowerMeter = (*fakeRaplMeter)(nil)
85+
86+
// FakeOptFn is a functional option for configuring FakeRaplMeter
87+
type FakeOptFn func(*fakeRaplMeter)
88+
89+
// WithFakePath sets the base device path for the fake meter
90+
func WithFakePath(path string) FakeOptFn {
91+
return func(m *fakeRaplMeter) {
92+
m.devicePath = path
93+
for _, z := range m.zones {
94+
if fz, ok := z.(*fakeEnergyZone); ok {
95+
fz.path = filepath.Join(path, fmt.Sprintf("energy_%s", fz.name))
96+
}
97+
}
98+
}
99+
}
100+
101+
// WithFakeMaxEnergy sets the maximum energy value before wrap-around
102+
func WithFakeMaxEnergy(e Energy) FakeOptFn {
103+
return func(m *fakeRaplMeter) {
104+
for _, z := range m.zones {
105+
if fz, ok := z.(*fakeEnergyZone); ok {
106+
fz.maxEnergy = e
107+
}
108+
}
109+
}
110+
}
111+
112+
// WithFakeMaxEnergy sets the maximum energy value before wrap-around
113+
func WithFakeLogger(l *slog.Logger) FakeOptFn {
114+
return func(m *fakeRaplMeter) {
115+
m.logger = l.With("meter", m.Name())
116+
}
117+
}
118+
119+
// NewFakeCPUMeter creates a new fake CPU power meter
120+
func NewFakeCPUMeter(zones []string, opts ...FakeOptFn) (CPUPowerMeter, error) {
121+
meter := &fakeRaplMeter{
122+
devicePath: defaultRaplPath,
123+
logger: slog.Default().With("meter", "fake-cpu-meter"),
124+
}
125+
126+
// nil and empty slices are equivalent
127+
if len(zones) == 0 {
128+
zones = defaultFakeZones
129+
}
130+
131+
zoneIncrementFactor := map[Zone]int{
132+
ZonePackage: 12,
133+
ZoneCore: 8,
134+
ZoneDRAM: 5,
135+
ZoneUncore: 2,
136+
}
137+
138+
meter.zones = make([]EnergyZone, 0, len(zones))
139+
140+
for i, zoneName := range zones {
141+
meter.zones = append(meter.zones, &fakeEnergyZone{
142+
name: zoneName,
143+
index: i,
144+
path: filepath.Join(defaultRaplPath, fmt.Sprintf("energy_%s", zoneName)),
145+
maxEnergy: 1000000,
146+
increment: Energy(100 + zoneIncrementFactor[zoneName]),
147+
randomFactor: 0.5,
148+
})
149+
}
150+
151+
for _, opt := range opts {
152+
opt(meter)
153+
}
154+
155+
return meter, nil
156+
}
157+
158+
func (m *fakeRaplMeter) Name() string {
159+
return "fake-cpu-meter"
160+
}
161+
162+
func (m *fakeRaplMeter) Start(ctx context.Context) error {
163+
m.logger.Info("Starting fake CPU power meter")
164+
return nil
165+
}
166+
167+
func (m *fakeRaplMeter) Stop() error {
168+
m.logger.Info("Stopping fake CPU power meter")
169+
return nil
170+
}
171+
172+
func (m *fakeRaplMeter) Zones() ([]EnergyZone, error) {
173+
return m.zones, nil
174+
}

0 commit comments

Comments
 (0)