|
| 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