Skip to content

Commit

Permalink
fix: RAPL sensor now properly use CPU share instead of full measure (#13
Browse files Browse the repository at this point in the history
)
  • Loading branch information
metacosm committed Nov 6, 2023
1 parent fe96c2d commit bba6ca2
Show file tree
Hide file tree
Showing 11 changed files with 259 additions and 49 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ running `sudo chmod +r` on these files. Note that this change will only persist
To use the extension:

1. Clone this repository locally
2. Build the code using `mvn install -DskipTests` (tests currently only work on macOS)
2. Build the code using `mvn install`
3. Add the extension to the application which energy consumption you wish to measure. Since the extension is not yet
released, you will need to add it manually as a dependency to your application:
```xml
Expand Down
5 changes: 5 additions & 0 deletions runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@
import io.quarkiverse.power.runtime.sensors.*;

public class PowerMeasurer<M extends IncrementableMeasure> {
public static final OperatingSystemMXBean osBean;

private static final OperatingSystemMXBean osBean;
static {
osBean = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
// take two measures to avoid initial zero values
osBean.getProcessCpuLoad();
osBean.getCpuLoad();
osBean.getProcessCpuLoad();
osBean.getCpuLoad();
}

private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
Expand All @@ -34,6 +38,16 @@ public PowerMeasurer(PowerSensor<M> sensor) {
this.sensor = sensor;
}

public double cpuShareOfJVMProcess() {
final var processCpuLoad = osBean.getProcessCpuLoad();
final var cpuLoad = osBean.getCpuLoad();
return (processCpuLoad < 0 || cpuLoad <= 0) ? 0 : processCpuLoad / cpuLoad;
}

PowerSensor<M> sensor() {
return sensor;
}

public boolean isRunning() {
return measure != null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ public static PowerSensor<? extends IncrementableMeasure> determinePowerSensor()
String osName = originalOSName.toLowerCase();

if (osName.contains("mac os x")) {
return MacOSPowermetricsSensor.instance;
return new MacOSPowermetricsSensor();
}

if (!osName.contains("linux")) {
throw new RuntimeException("Unsupported platform: " + originalOSName);
}
return IntelRAPLSensor.instance;
return new IntelRAPLSensor();
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,67 @@
package io.quarkiverse.power.runtime.sensors.linux.rapl;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;

import io.quarkiverse.power.runtime.SensorMeasure;
import io.quarkiverse.power.runtime.sensors.IncrementableMeasure;

public class IntelRAPLMeasure implements IncrementableMeasure {
private final long initial;
private long cpu;
private final long startedAt = System.currentTimeMillis();
private final Map<String, Accumulator> values = new HashMap<>();

static class Accumulator {
private final AtomicLong previous = new AtomicLong(0);
private double accumulated;

private Accumulator recordNewValue(long currentValue, double share, long duration) {
accumulated += (currentValue - previous.getAndSet(currentValue)) * share / duration;
return this;
}

public double accumulated() {
return accumulated;
}
}

double getValue(String name) {
final var value = values.get(name);
return value == null ? 0.0 : value.accumulated() / 1000;
}

public IntelRAPLMeasure(long initial) {
this.initial = initial;
void updateValue(String name, long current, double cpuShare, long duration) {
values.computeIfAbsent(name, k -> new Accumulator()).recordNewValue(current, cpuShare, duration);
}

@Override
public double cpu() {
return ((double) cpu / durationSinceStart()) / 1_000;
return getValue("package-0");
}

@Override
public void addCPU(double v) {
cpu += ((long) v - initial);
public Optional<Double> gpu() {
return Optional.ofNullable(values.get(SensorMeasure.GPU)).map(value -> value.accumulated() / 1000);
}

private long durationSinceStart() {
return System.currentTimeMillis() - startedAt;
@Override
public Optional<Double> byKey(String key) {
final var v = IncrementableMeasure.super.byKey(key); // first get from default implementation
if (v.isEmpty()) {
// try local keys
return Optional.ofNullable(values.get(key)).map(value -> value.accumulated() / 1000);
} else {
return v;
}
}

@Override
public double total() {
return values.values().stream().map(Accumulator::accumulated).reduce(Double::sum).orElse(0.0) / 1000;
}

@Override
public void addCPU(double v) {
throw new IllegalStateException("Shouldn't be called");
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,92 @@
package io.quarkiverse.power.runtime.sensors.linux.rapl;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;

import io.quarkiverse.power.runtime.PowerMeasurer;
import io.quarkiverse.power.runtime.sensors.OngoingPowerMeasure;
import io.quarkiverse.power.runtime.sensors.PowerSensor;

public class IntelRAPLSensor implements PowerSensor<IntelRAPLMeasure> {
private final Map<String, RAPLFile> raplFiles = new HashMap<>();
private long frequency;

public static final IntelRAPLSensor instance = new IntelRAPLSensor();
private final List<Path> raplFiles = new ArrayList<>(3);
interface RAPLFile {
long extractPowerMeasure();

static RAPLFile createFrom(Path file) {
return ByteBufferRAPLFile.createFrom(file);
}
}

private static class NaiveRAPLFile implements RAPLFile {
private final Path path;

private NaiveRAPLFile(Path path) {
this.path = path;
}

@Override
public long extractPowerMeasure() {
try {
return Long.parseLong(Files.readString(path).trim());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

static class ByteBufferRAPLFile implements RAPLFile {
private static final int CAPACITY = 64;
private final ByteBuffer buffer;
private final FileChannel channel;

private ByteBufferRAPLFile(FileChannel channel) {
this.channel = channel;
buffer = ByteBuffer.allocate(CAPACITY);
}

static RAPLFile createFrom(Path file) {
try {
return new ByteBufferRAPLFile(new RandomAccessFile(file.toFile(), "r").getChannel());
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}

public long extractPowerMeasure() {
try {
channel.read(buffer);
} catch (IOException e) {
throw new RuntimeException(e);
}
long value = 0;
// will work even better if we can hard code as a static final const the length, in case won't change or is defined by spec
for (int i = 0; i < CAPACITY; i++) {
byte digit = buffer.get(i);
if (digit >= '0' && digit <= '9') {
value = value * 10 + (digit - '0');
} else {
if (digit == '\n') {
return value;
}
// Invalid character; handle accordingly or throw an exception
throw new NumberFormatException("Invalid character in input: '" + Character.toString(digit) + "'");
}
}
return value;
}
}

public IntelRAPLSensor() {
// if we total system energy is not available, read package and DRAM if possible
// todo: extract more granular information
// todo: check Intel doc
if (!checkAvailablity("/sys/class/powercap/intel-rapl/intel-rapl:1/energy_uj")) {
checkAvailablity("/sys/class/powercap/intel-rapl/intel-rapl:0/energy_uj");
Expand All @@ -30,32 +100,44 @@ public IntelRAPLSensor() {

private boolean checkAvailablity(String raplFileAsString) {
final var raplFile = Path.of(raplFileAsString);
if (Files.exists(raplFile) && Files.isReadable(raplFile)) {
raplFiles.add(raplFile);
if (isReadable(raplFile)) {
// get metric name
final var nameFile = raplFile.resolveSibling("name");
if (!isReadable(nameFile)) {
throw new IllegalStateException("No name associated with " + raplFileAsString);
}

try {
final var name = Files.readString(nameFile).trim();
raplFiles.put(name, RAPLFile.createFrom(raplFile));
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
return false;
}

private static boolean isReadable(Path file) {
return Files.exists(file) && Files.isReadable(file);
}

@Override
public OngoingPowerMeasure<IntelRAPLMeasure> start(long duration, long frequency, Writer out) throws Exception {
return new OngoingPowerMeasure<>(new IntelRAPLMeasure(extractPowerMeasure()));
this.frequency = frequency;
IntelRAPLMeasure measure = new IntelRAPLMeasure();
update(measure);
return new OngoingPowerMeasure<>(measure);
}

@Override
public void update(OngoingPowerMeasure<IntelRAPLMeasure> ongoingMeasure, Writer out) {
ongoingMeasure.addCPU(extractPowerMeasure());
private void update(IntelRAPLMeasure measure) {
double cpuShare = PowerMeasurer.instance().cpuShareOfJVMProcess();
raplFiles.forEach((name, buffer) -> measure.updateValue(name, buffer.extractPowerMeasure(), cpuShare, frequency));
}

private long extractPowerMeasure() {
long energyData = 0;
for (final Path raplFile : raplFiles) {
try {
energyData += Long.parseLong(Files.readString(raplFile).trim());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return energyData;
@Override
public void update(OngoingPowerMeasure<IntelRAPLMeasure> ongoingMeasure, Writer out) {
update(ongoingMeasure.sensorMeasure());
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package io.quarkiverse.power.runtime.sensors.macos.jmx;

import static io.quarkiverse.power.runtime.PowerMeasurer.osBean;

import java.io.BufferedReader;
import java.io.InputStreamReader;

import io.quarkiverse.power.runtime.PowerMeasurer;
import io.quarkiverse.power.runtime.sensors.OngoingPowerMeasure;
import io.quarkiverse.power.runtime.sensors.PowerSensor;
import io.quarkiverse.power.runtime.sensors.macos.AppleSiliconMeasure;
Expand Down Expand Up @@ -34,12 +33,10 @@ public void update(OngoingPowerMeasure<AppleSiliconMeasure> ongoingMeasure, Writ

// look for line that contains CPU power measure
if (line.startsWith("CPU Power")) {
final var processCpuLoad = osBean.getProcessCpuLoad();
final var cpuLoad = osBean.getCpuLoad();
if (processCpuLoad < 0 || cpuLoad <= 0) {
final var cpuShare = PowerMeasurer.instance().cpuShareOfJVMProcess();
if (cpuShare <= 0) {
break;
}
final var cpuShare = processCpuLoad / cpuLoad;
ongoingMeasure.addCPU(extractAttributedMeasure(line, cpuShare));
break;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
package io.quarkiverse.power.runtime.sensors.macos.powermetrics;

import static io.quarkiverse.power.runtime.PowerMeasurer.osBean;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;

import io.quarkiverse.power.runtime.PowerMeasure;
import io.quarkiverse.power.runtime.PowerMeasurer;
import io.quarkiverse.power.runtime.sensors.OngoingPowerMeasure;
import io.quarkiverse.power.runtime.sensors.PowerSensor;
import io.quarkiverse.power.runtime.sensors.macos.AppleSiliconMeasure;

public class MacOSPowermetricsSensor implements PowerSensor<AppleSiliconMeasure> {
private Process powermetrics;
public static PowerSensor<AppleSiliconMeasure> instance = new MacOSPowermetricsSensor();
private final static String pid = " " + ProcessHandle.current().pid() + " ";
private double accumulatedCPUShareDiff = 0.0;

Expand Down Expand Up @@ -80,9 +78,7 @@ AppleSiliconMeasure extractPowerMeasure(OngoingPowerMeasure<AppleSiliconMeasure>
if (!cpuDone) {
// look for line that contains CPU power measure
if (line.startsWith("CPU Power")) {
final var processCpuLoad = osBean.getProcessCpuLoad();
final var cpuLoad = osBean.getCpuLoad();
final var jmxCpuShare = (processCpuLoad < 0 || cpuLoad <= 0) ? 0 : processCpuLoad / cpuLoad;
final var jmxCpuShare = PowerMeasurer.instance().cpuShareOfJVMProcess();
accumulatedPower.addCPU(extractAttributedMeasure(line, cpuShare));
accumulatedCPUShareDiff += (cpuShare - jmxCpuShare);
cpuDone = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,23 @@
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import io.quarkiverse.power.runtime.sensors.IncrementableMeasure;
import io.quarkiverse.power.runtime.sensors.PowerSensor;
import io.quarkiverse.power.runtime.sensors.PowerSensorProducer;

public class PowerMeasurerTest {
@Test
void startShouldAccumulateOverSpecifiedDurationAndStop() throws Exception {
final var measurer = PowerMeasurer.instance();
PowerSensor<? extends IncrementableMeasure> sensor = PowerSensorProducer.determinePowerSensor();
sensor = Mockito.spy(sensor);
final var measurer = new PowerMeasurer<>(sensor);

measurer.start(1, 100, null);
Thread.sleep(2000);
final var measure = measurer.current();
assertEquals(10, measure.numberOfSamples());
Mockito.verify(sensor, Mockito.times(10)).update(Mockito.any(), Mockito.any());
}
}
Loading

0 comments on commit bba6ca2

Please sign in to comment.