Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: RAPL sensor now properly use CPU share instead of full measure #13

Merged
merged 9 commits into from
Nov 6, 2023
Merged
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