From fdd497ecb9fe865512ae93057da3debe32cb8b76 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Sat, 11 Nov 2023 16:21:39 +0100 Subject: [PATCH] feat: support individual measure recording, separate measure from metadata --- .../devui/commands/StartCommand.java | 9 +- .../power/runtime/PowerMeasure.java | 10 +- .../power/runtime/PowerMeasurer.java | 15 +-- .../power/runtime/SensorMeasure.java | 24 +--- .../power/runtime/SensorMetadata.java | 7 + .../runtime/sensors/IncrementableMeasure.java | 11 -- .../runtime/sensors/OngoingPowerMeasure.java | 71 +++++----- .../power/runtime/sensors/PowerSensor.java | 11 +- .../runtime/sensors/PowerSensorProducer.java | 3 +- .../runtime/sensors/StoppedPowerMeasure.java | 44 +++---- .../linux/rapl/ByteBufferRAPLFile.java | 50 +++++++ .../sensors/linux/rapl/IntelRAPLMeasure.java | 68 +++------- .../sensors/linux/rapl/IntelRAPLSensor.java | 124 ++++++++---------- .../runtime/sensors/linux/rapl/RAPLFile.java | 11 ++ .../sensors/macos/AppleSiliconMeasure.java | 69 +++++----- .../sensors/macos/jmx/JMXCPUSensor.java | 15 ++- .../powermetrics/MacOSPowermetricsSensor.java | 44 +++++-- .../power/runtime/PowerMeasurerTest.java | 3 +- .../linux/rapl/IntelRAPLMeasureTest.java | 35 ----- .../sensors/linux/rapl/RAPLFileTest.java | 2 +- 20 files changed, 305 insertions(+), 321 deletions(-) create mode 100644 runtime/src/main/java/io/quarkiverse/power/runtime/SensorMetadata.java delete mode 100644 runtime/src/main/java/io/quarkiverse/power/runtime/sensors/IncrementableMeasure.java create mode 100644 runtime/src/main/java/io/quarkiverse/power/runtime/sensors/linux/rapl/ByteBufferRAPLFile.java create mode 100644 runtime/src/main/java/io/quarkiverse/power/runtime/sensors/linux/rapl/RAPLFile.java delete mode 100644 runtime/src/test/java/io/quarkiverse/power/runtime/sensors/linux/rapl/IntelRAPLMeasureTest.java diff --git a/deployment/src/main/java/io/quarkiverse/power/deployment/devui/commands/StartCommand.java b/deployment/src/main/java/io/quarkiverse/power/deployment/devui/commands/StartCommand.java index e6523cb..77ed41d 100644 --- a/deployment/src/main/java/io/quarkiverse/power/deployment/devui/commands/StartCommand.java +++ b/deployment/src/main/java/io/quarkiverse/power/deployment/devui/commands/StartCommand.java @@ -13,7 +13,7 @@ @CommandDefinition(name = "start", description = "Starts measuring power consumption of the current application") public class StartCommand extends QuarkusCommand { private final PowerMeasurer sensor; - private PowerMeasure baseline; + private PowerMeasure baseline; @Option(name = "stopAfter", shortName = 's', description = "Automatically stop the measures after the specified duration in seconds", defaultValue = "-1") private long duration; @@ -41,7 +41,10 @@ public CommandResult doExecute(CommandInvocation commandInvocation) { commandInvocation.println("Establishing baseline for 30s, please do not use your application until done."); commandInvocation.println("Power measurement will start as configured after this initial measure is done."); sensor.start(30, 1000); - sensor.onError(e -> commandInvocation.println("An error occurred: " + e.getMessage())); + sensor.onError(e -> { + commandInvocation.println("An error occurred: " + e.getMessage()); + e.printStackTrace(); + }); sensor.onCompleted((m) -> { baseline = m; outputConsumptionSinceStarted(baseline, commandInvocation, true); @@ -70,7 +73,7 @@ public CommandResult doExecute(CommandInvocation commandInvocation) { return CommandResult.SUCCESS; } - private void outputConsumptionSinceStarted(PowerMeasure measure, CommandInvocation out, boolean isBaseline) { + private void outputConsumptionSinceStarted(PowerMeasure measure, CommandInvocation out, boolean isBaseline) { final var title = isBaseline ? "\nBaseline => " : "\nMeasured => "; out.println(title + PowerMeasure.asString(measure)); if (!isBaseline) { diff --git a/runtime/src/main/java/io/quarkiverse/power/runtime/PowerMeasure.java b/runtime/src/main/java/io/quarkiverse/power/runtime/PowerMeasure.java index 77eceb2..44356ce 100644 --- a/runtime/src/main/java/io/quarkiverse/power/runtime/PowerMeasure.java +++ b/runtime/src/main/java/io/quarkiverse/power/runtime/PowerMeasure.java @@ -1,17 +1,17 @@ package io.quarkiverse.power.runtime; -public interface PowerMeasure extends SensorMeasure { +import java.util.List; + +public interface PowerMeasure extends SensorMeasure { int numberOfSamples(); long duration(); - M sensorMeasure(); - default double average() { return total() / numberOfSamples(); } - static String asString(PowerMeasure measure) { + static String asString(PowerMeasure measure) { final var durationInSeconds = measure.duration() / 1000; final var samples = measure.numberOfSamples(); final var measuredMilliWatts = measure.total(); @@ -25,4 +25,6 @@ static String readableWithUnit(double milliWatts) { double power = milliWatts >= 1000 ? milliWatts / 1000 : milliWatts; return String.format("%.3f%s", power, unit); } + + List measures(); } \ No newline at end of file diff --git a/runtime/src/main/java/io/quarkiverse/power/runtime/PowerMeasurer.java b/runtime/src/main/java/io/quarkiverse/power/runtime/PowerMeasurer.java index 97aaced..ebfcaa2 100644 --- a/runtime/src/main/java/io/quarkiverse/power/runtime/PowerMeasurer.java +++ b/runtime/src/main/java/io/quarkiverse/power/runtime/PowerMeasurer.java @@ -10,7 +10,7 @@ import io.quarkiverse.power.runtime.sensors.*; -public class PowerMeasurer { +public class PowerMeasurer { private static final OperatingSystemMXBean osBean; static { @@ -25,10 +25,10 @@ public class PowerMeasurer { private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); private ScheduledFuture scheduled; private final PowerSensor sensor; - private OngoingPowerMeasure measure; + private OngoingPowerMeasure measure; - private Consumer> completed; - private BiConsumer> sampled; + private Consumer completed; + private BiConsumer sampled; private Consumer errorHandler; private final static PowerMeasurer instance = new PowerMeasurer<>( @@ -48,11 +48,11 @@ public double cpuShareOfJVMProcess() { return (processCpuLoad < 0 || cpuLoad <= 0) ? 0 : processCpuLoad / cpuLoad; } - public void onCompleted(Consumer> completed) { + public void onCompleted(Consumer completed) { this.completed = completed; } - public void onSampled(BiConsumer> sampled) { + public void onSampled(BiConsumer sampled) { this.sampled = sampled; } @@ -88,7 +88,6 @@ public void start(long durationInSeconds, long frequencyInMilliseconds) private void update() { try { sensor.update(measure); - measure.incrementSamples(); if (this.sampled != null) { sampled.accept(measure.numberOfSamples(), measure); } @@ -117,7 +116,7 @@ public void stop() { sensor.stop(); scheduled.cancel(true); // record the result - final var measured = new StoppedPowerMeasure<>(measure); + final var measured = new StoppedPowerMeasure(measure); // then set the measure to null to mark that we're ready for a new measure measure = null; // and finally, but only then, run the completion handler diff --git a/runtime/src/main/java/io/quarkiverse/power/runtime/SensorMeasure.java b/runtime/src/main/java/io/quarkiverse/power/runtime/SensorMeasure.java index dc775f4..a7ea85d 100644 --- a/runtime/src/main/java/io/quarkiverse/power/runtime/SensorMeasure.java +++ b/runtime/src/main/java/io/quarkiverse/power/runtime/SensorMeasure.java @@ -1,28 +1,8 @@ package io.quarkiverse.power.runtime; -import java.util.Optional; - public interface SensorMeasure { - String CPU = "cpu"; - String GPU = "gpu"; - String TOTAL = "total"; - - double cpu(); - - default Optional gpu() { - return Optional.empty(); - } - default Optional byKey(String key) { - return switch (key) { - case CPU -> Optional.of(cpu()); - case GPU -> gpu(); - case TOTAL -> Optional.of(total()); - default -> Optional.empty(); - }; - } + double total(); - default double total() { - return cpu() + gpu().orElse(0.0); - } + SensorMetadata metadata(); } diff --git a/runtime/src/main/java/io/quarkiverse/power/runtime/SensorMetadata.java b/runtime/src/main/java/io/quarkiverse/power/runtime/SensorMetadata.java new file mode 100644 index 0000000..2419fc3 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/power/runtime/SensorMetadata.java @@ -0,0 +1,7 @@ +package io.quarkiverse.power.runtime; + +public interface SensorMetadata { + int indexFor(String component); + + int componentCardinality(); +} diff --git a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/IncrementableMeasure.java b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/IncrementableMeasure.java deleted file mode 100644 index 97afb7c..0000000 --- a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/IncrementableMeasure.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.quarkiverse.power.runtime.sensors; - -import io.quarkiverse.power.runtime.SensorMeasure; - -public interface IncrementableMeasure extends SensorMeasure { - - void addCPU(double v); - - default void addGPU(double v) { - } -} diff --git a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/OngoingPowerMeasure.java b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/OngoingPowerMeasure.java index 0fd1781..765c1b1 100644 --- a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/OngoingPowerMeasure.java +++ b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/OngoingPowerMeasure.java @@ -1,64 +1,67 @@ package io.quarkiverse.power.runtime.sensors; -import java.util.Optional; +import java.util.ArrayList; +import java.util.List; import io.quarkiverse.power.runtime.PowerMeasure; +import io.quarkiverse.power.runtime.SensorMetadata; -public class OngoingPowerMeasure - implements IncrementableMeasure, PowerMeasure { - private final M measure; +public class OngoingPowerMeasure implements PowerMeasure { + private final SensorMetadata sensorMetadata; private final long startedAt; - private int samplesNb; + private final List measures = new ArrayList<>(); + private double[] current; + private double total; - public OngoingPowerMeasure(M measure) { + public OngoingPowerMeasure(SensorMetadata sensorMetadata) { startedAt = System.currentTimeMillis(); - samplesNb = 0; - this.measure = measure; + this.sensorMetadata = sensorMetadata; } - public void incrementSamples() { - samplesNb++; + public void startNewMeasure() { + if (current != null) { + throw new IllegalStateException("A new measure cannot be started while one is still ongoing"); + } + current = new double[sensorMetadata.componentCardinality()]; } - @Override - public double cpu() { - return measure.cpu(); + public void setComponent(int index, double value) { + current[index] = value; } - @Override - public Optional gpu() { - return measure.gpu(); + public double[] stopMeasure() { + final var recorded = new double[current.length]; + System.arraycopy(current, 0, recorded, 0, current.length); + measures.add(recorded); + var currentMeasureTotal = 0.0; + for (double value : recorded) { + currentMeasureTotal += value; + } + total += currentMeasureTotal; + current = null; + return recorded; } @Override - public Optional byKey(String key) { - return measure.byKey(key); + public List measures() { + return measures; } @Override public double total() { - return measure.total(); - } - - public int numberOfSamples() { - return samplesNb; - } - - public long duration() { - return System.currentTimeMillis() - startedAt; + return total; } @Override - public void addCPU(double v) { - measure.addCPU(v); + public SensorMetadata metadata() { + return sensorMetadata; } - @Override - public void addGPU(double v) { - measure.addGPU(v); + public int numberOfSamples() { + return measures.size(); } - public M sensorMeasure() { - return measure; + public long duration() { + return System.currentTimeMillis() - startedAt; } } diff --git a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/PowerSensor.java b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/PowerSensor.java index 893a499..9c0eb6f 100644 --- a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/PowerSensor.java +++ b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/PowerSensor.java @@ -3,17 +3,20 @@ import java.util.Optional; import io.quarkiverse.power.runtime.PowerMeasure; +import io.quarkiverse.power.runtime.SensorMeasure; -public interface PowerSensor { +public interface PowerSensor { - OngoingPowerMeasure start(long duration, long frequency) throws Exception; + OngoingPowerMeasure start(long duration, long frequency) throws Exception; default void stop() { } - void update(OngoingPowerMeasure ongoingMeasure); + void update(OngoingPowerMeasure ongoingMeasure); - default Optional additionalInfo(PowerMeasure measure) { + default Optional additionalInfo(PowerMeasure measure) { return Optional.empty(); } + + T measureFor(double[] measureComponents); } diff --git a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/PowerSensorProducer.java b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/PowerSensorProducer.java index 9df8323..5f0c9bc 100644 --- a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/PowerSensorProducer.java +++ b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/PowerSensorProducer.java @@ -3,6 +3,7 @@ import jakarta.enterprise.inject.Produces; import jakarta.inject.Singleton; +import io.quarkiverse.power.runtime.SensorMeasure; import io.quarkiverse.power.runtime.sensors.linux.rapl.IntelRAPLSensor; import io.quarkiverse.power.runtime.sensors.macos.powermetrics.MacOSPowermetricsSensor; @@ -13,7 +14,7 @@ public PowerSensor sensor() { return determinePowerSensor(); } - public static PowerSensor determinePowerSensor() { + public static PowerSensor determinePowerSensor() { final var originalOSName = System.getProperty("os.name"); String osName = originalOSName.toLowerCase(); diff --git a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/StoppedPowerMeasure.java b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/StoppedPowerMeasure.java index 82c5716..38e88b6 100644 --- a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/StoppedPowerMeasure.java +++ b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/StoppedPowerMeasure.java @@ -1,53 +1,47 @@ package io.quarkiverse.power.runtime.sensors; -import java.util.Optional; +import java.util.List; import io.quarkiverse.power.runtime.PowerMeasure; -import io.quarkiverse.power.runtime.SensorMeasure; +import io.quarkiverse.power.runtime.SensorMetadata; -public class StoppedPowerMeasure implements SensorMeasure, PowerMeasure { - private final M measure; +public class StoppedPowerMeasure implements PowerMeasure { + private final SensorMetadata metadata; private final long duration; private final int samples; + private final List measures; + private final double total; - public StoppedPowerMeasure(PowerMeasure powerMeasure) { - this.measure = powerMeasure.sensorMeasure(); + public StoppedPowerMeasure(PowerMeasure powerMeasure) { + this.metadata = powerMeasure.metadata(); this.duration = powerMeasure.duration(); this.samples = powerMeasure.numberOfSamples(); + this.measures = powerMeasure.measures(); + this.total = powerMeasure.total(); } @Override - public double cpu() { - return measure.cpu(); + public int numberOfSamples() { + return samples; } @Override - public Optional gpu() { - return measure.gpu(); + public long duration() { + return duration; } @Override - public Optional byKey(String key) { - return measure.byKey(key); + public List measures() { + return measures; } @Override public double total() { - return measure.total(); - } - - @Override - public int numberOfSamples() { - return samples; - } - - @Override - public long duration() { - return duration; + return total; } @Override - public M sensorMeasure() { - return measure; + public SensorMetadata metadata() { + return metadata; } } diff --git a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/linux/rapl/ByteBufferRAPLFile.java b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/linux/rapl/ByteBufferRAPLFile.java new file mode 100644 index 0000000..4c5e3d5 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/linux/rapl/ByteBufferRAPLFile.java @@ -0,0 +1,50 @@ +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.Path; + +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; + } +} diff --git a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/linux/rapl/IntelRAPLMeasure.java b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/linux/rapl/IntelRAPLMeasure.java index a11d0cd..b238197 100644 --- a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/linux/rapl/IntelRAPLMeasure.java +++ b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/linux/rapl/IntelRAPLMeasure.java @@ -1,67 +1,35 @@ 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 java.util.Arrays; import io.quarkiverse.power.runtime.SensorMeasure; -import io.quarkiverse.power.runtime.sensors.IncrementableMeasure; +import io.quarkiverse.power.runtime.SensorMetadata; -public class IntelRAPLMeasure implements IncrementableMeasure { - private final Map values = new HashMap<>(); +public class IntelRAPLMeasure implements SensorMeasure { - static class Accumulator { - private final AtomicLong previous = new AtomicLong(0); - private double accumulated; + private final SensorMetadata metadata; + private final double[] measure; - 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; - } - - 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 getValue("package-0"); - } - - @Override - public Optional gpu() { - return Optional.ofNullable(values.get(SensorMeasure.GPU)).map(value -> value.accumulated() / 1000); - } - - @Override - public Optional 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; + IntelRAPLMeasure(SensorMetadata metadata, double[] measure) { + if (measure.length != metadata.componentCardinality()) { + throw new IllegalArgumentException( + "Provided measure " + Arrays.toString(measure) + " doesn't match provided metadata: " + metadata); } + this.metadata = metadata; + this.measure = measure; } @Override public double total() { - return values.values().stream().map(Accumulator::accumulated).reduce(Double::sum).orElse(0.0) / 1000; + double total = 0.0; + for (double value : measure) { + total += value; + } + return total; } @Override - public void addCPU(double v) { - throw new IllegalStateException("Shouldn't be called"); + public SensorMetadata metadata() { + return metadata; } } diff --git a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/linux/rapl/IntelRAPLSensor.java b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/linux/rapl/IntelRAPLSensor.java index d347adc..fc60b77 100644 --- a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/linux/rapl/IntelRAPLSensor.java +++ b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/linux/rapl/IntelRAPLSensor.java @@ -1,87 +1,61 @@ 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.HashMap; -import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; import io.quarkiverse.power.runtime.PowerMeasurer; +import io.quarkiverse.power.runtime.SensorMetadata; import io.quarkiverse.power.runtime.sensors.OngoingPowerMeasure; import io.quarkiverse.power.runtime.sensors.PowerSensor; public class IntelRAPLSensor implements PowerSensor { - private final Map raplFiles = new HashMap<>(); + private final RAPLFile[] raplFiles; + private final SensorMetadata metadata; + private final double[] lastMeasuredSensorValues; private long frequency; - interface RAPLFile { - long extractPowerMeasure(); - - static RAPLFile createFrom(Path file) { - return ByteBufferRAPLFile.createFrom(file); + public IntelRAPLSensor() { + // if we total system energy is not available, read package and DRAM if possible + // todo: check Intel doc + final var files = new TreeMap(); + if (!addFileIfReadable("/sys/class/powercap/intel-rapl/intel-rapl:1/energy_uj", files)) { + addFileIfReadable("/sys/class/powercap/intel-rapl/intel-rapl:0/energy_uj", files); + addFileIfReadable("/sys/class/powercap/intel-rapl/intel-rapl:0/intel-rapl:0:2/energy_uj", files); } - } - - 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); + if (files.isEmpty()) { + throw new RuntimeException("Failed to get RAPL energy readings, probably due to lack of read access "); } - static RAPLFile createFrom(Path file) { - try { - return new ByteBufferRAPLFile(new RandomAccessFile(file.toFile(), "r").getChannel()); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } + raplFiles = files.values().toArray(new RAPLFile[0]); + final var metadata = new HashMap(files.size()); + int fileNb = 0; + for (String name : files.keySet()) { + metadata.put(name, fileNb++); } - - 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) + "'"); + this.metadata = new SensorMetadata() { + @Override + public int indexFor(String component) { + final var index = metadata.get(component); + if (index == null) { + throw new IllegalArgumentException("Unknow component: " + component); } + return index; } - return value; - } - } - public IntelRAPLSensor() { - // if we total system energy is not available, read package and DRAM if possible - // 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"); - checkAvailablity("/sys/class/powercap/intel-rapl/intel-rapl:0/intel-rapl:0:2/energy_uj"); - } - - if (raplFiles.isEmpty()) { - throw new RuntimeException("Failed to get RAPL energy readings, probably due to lack of read access "); - } + @Override + public int componentCardinality() { + return metadata.size(); + } + }; + lastMeasuredSensorValues = new double[raplFiles.length]; } - private boolean checkAvailablity(String raplFileAsString) { + private boolean addFileIfReadable(String raplFileAsString, SortedMap files) { final var raplFile = Path.of(raplFileAsString); if (isReadable(raplFile)) { // get metric name @@ -92,7 +66,7 @@ private boolean checkAvailablity(String raplFileAsString) { try { final var name = Files.readString(nameFile).trim(); - raplFiles.put(name, RAPLFile.createFrom(raplFile)); + files.put(name, RAPLFile.createFrom(raplFile)); } catch (IOException e) { e.printStackTrace(); return false; @@ -107,20 +81,32 @@ private static boolean isReadable(Path file) { } @Override - public OngoingPowerMeasure start(long duration, long frequency) throws Exception { + public OngoingPowerMeasure start(long duration, long frequency) throws Exception { this.frequency = frequency; - IntelRAPLMeasure measure = new IntelRAPLMeasure(); - update(measure); - return new OngoingPowerMeasure<>(measure); + + // perform an initial measure to prime the data + final var ongoingMeasure = new OngoingPowerMeasure(metadata); + update(ongoingMeasure); + return ongoingMeasure; } - private void update(IntelRAPLMeasure measure) { + private double computeNewComponentValue(int componentIndex, long sensorValue, double cpuShare) { + return (sensorValue - lastMeasuredSensorValues[componentIndex]) * cpuShare / frequency / 1000; + } + + @Override + public void update(OngoingPowerMeasure ongoingMeasure) { double cpuShare = PowerMeasurer.instance().cpuShareOfJVMProcess(); - raplFiles.forEach((name, buffer) -> measure.updateValue(name, buffer.extractPowerMeasure(), cpuShare, frequency)); + for (int i = 0; i < raplFiles.length; i++) { + final var value = raplFiles[i].extractPowerMeasure(); + final var newComponentValue = computeNewComponentValue(i, value, cpuShare); + ongoingMeasure.setComponent(i, newComponentValue); + lastMeasuredSensorValues[i] = newComponentValue; + } } @Override - public void update(OngoingPowerMeasure ongoingMeasure) { - update(ongoingMeasure.sensorMeasure()); + public IntelRAPLMeasure measureFor(double[] measureComponents) { + return new IntelRAPLMeasure(metadata, measureComponents); } } diff --git a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/linux/rapl/RAPLFile.java b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/linux/rapl/RAPLFile.java new file mode 100644 index 0000000..5d63392 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/linux/rapl/RAPLFile.java @@ -0,0 +1,11 @@ +package io.quarkiverse.power.runtime.sensors.linux.rapl; + +import java.nio.file.Path; + +interface RAPLFile { + long extractPowerMeasure(); + + static RAPLFile createFrom(Path file) { + return ByteBufferRAPLFile.createFrom(file); + } +} diff --git a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/macos/AppleSiliconMeasure.java b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/macos/AppleSiliconMeasure.java index 7e75967..d087b71 100644 --- a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/macos/AppleSiliconMeasure.java +++ b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/macos/AppleSiliconMeasure.java @@ -1,34 +1,36 @@ package io.quarkiverse.power.runtime.sensors.macos; -import java.util.Optional; +import io.quarkiverse.power.runtime.SensorMeasure; +import io.quarkiverse.power.runtime.SensorMetadata; -import io.quarkiverse.power.runtime.sensors.IncrementableMeasure; - -public class AppleSiliconMeasure implements IncrementableMeasure { - private double cpu; - private double gpu; - private double ane; +public class AppleSiliconMeasure implements SensorMeasure { + private final double cpu; + private final double gpu; + private final double ane; public static final String ANE = "ane"; - - @Override - public double cpu() { - return cpu; - } - - @Override - public Optional gpu() { - return Optional.of(gpu); - } - - @Override - public Optional byKey(String key) { - return switch (key) { - case CPU -> Optional.of(cpu()); - case GPU -> gpu(); - case ANE -> Optional.of(ane); - case TOTAL -> Optional.of(total()); - default -> Optional.empty(); - }; + public static final String CPU = "cpu"; + public static final String GPU = "gpu"; + public static final SensorMetadata METADATA = new SensorMetadata() { + @Override + public int indexFor(String component) { + return switch (component) { + case CPU -> 0; + case GPU -> 1; + case ANE -> 2; + default -> throw new IllegalArgumentException("Unknown component: " + component); + }; + } + + @Override + public int componentCardinality() { + return 3; + } + }; + + public AppleSiliconMeasure(double[] components) { + this.cpu = components[METADATA.indexFor(CPU)]; + this.gpu = components[METADATA.indexFor(GPU)]; + this.ane = components[METADATA.indexFor(ANE)]; } @Override @@ -36,15 +38,12 @@ public double total() { return cpu + gpu + ane; } - public void addCPU(double v) { - cpu += v; - } - - public void addGPU(double v) { - gpu += v; + @Override + public SensorMetadata metadata() { + return METADATA; } - public void addANE(double v) { - ane += v; + public double cpu() { + return cpu; } } diff --git a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/macos/jmx/JMXCPUSensor.java b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/macos/jmx/JMXCPUSensor.java index f924909..87ff18a 100644 --- a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/macos/jmx/JMXCPUSensor.java +++ b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/macos/jmx/JMXCPUSensor.java @@ -12,16 +12,18 @@ public class JMXCPUSensor implements PowerSensor { public static PowerSensor instance = new JMXCPUSensor(); private Process powermetrics; + private int cpu; @Override - public OngoingPowerMeasure start(long duration, long frequency) + public OngoingPowerMeasure start(long duration, long frequency) throws Exception { final var freq = Long.toString(Math.round(frequency)); powermetrics = Runtime.getRuntime().exec("sudo powermetrics --samplers cpu_power -i " + freq); - return new OngoingPowerMeasure<>(new AppleSiliconMeasure()); + cpu = AppleSiliconMeasure.METADATA.indexFor(AppleSiliconMeasure.CPU); + return new OngoingPowerMeasure(AppleSiliconMeasure.METADATA); } - public void update(OngoingPowerMeasure ongoingMeasure) { + public void update(OngoingPowerMeasure ongoingMeasure) { try { // Should not be closed since it closes the process BufferedReader input = new BufferedReader(new InputStreamReader(powermetrics.getInputStream())); @@ -37,7 +39,7 @@ public void update(OngoingPowerMeasure ongoingMeasure) { if (cpuShare <= 0) { break; } - ongoingMeasure.addCPU(extractAttributedMeasure(line, cpuShare)); + ongoingMeasure.setComponent(cpu, extractAttributedMeasure(line, cpuShare)); break; } } @@ -46,6 +48,11 @@ public void update(OngoingPowerMeasure ongoingMeasure) { } } + @Override + public AppleSiliconMeasure measureFor(double[] measureComponents) { + return new AppleSiliconMeasure(measureComponents); + } + private static double extractAttributedMeasure(String line, double attributionRatio) { final var powerValue = line.split(":")[1]; final var powerInMilliwatts = powerValue.split("m")[0]; diff --git a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/macos/powermetrics/MacOSPowermetricsSensor.java b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/macos/powermetrics/MacOSPowermetricsSensor.java index c0b414c..53eee42 100644 --- a/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/macos/powermetrics/MacOSPowermetricsSensor.java +++ b/runtime/src/main/java/io/quarkiverse/power/runtime/sensors/macos/powermetrics/MacOSPowermetricsSensor.java @@ -15,6 +15,15 @@ public class MacOSPowermetricsSensor implements PowerSensor private Process powermetrics; private final static String pid = " " + ProcessHandle.current().pid() + " "; private double accumulatedCPUShareDiff = 0.0; + private final int cpu; + private final int gpu; + private final int ane; + + public MacOSPowermetricsSensor() { + ane = AppleSiliconMeasure.METADATA.indexFor(AppleSiliconMeasure.ANE); + cpu = AppleSiliconMeasure.METADATA.indexFor(AppleSiliconMeasure.CPU); + gpu = AppleSiliconMeasure.METADATA.indexFor(AppleSiliconMeasure.GPU); + } private static class ProcessRecord { final double cpu; @@ -30,18 +39,18 @@ public ProcessRecord(String line) { } @Override - public void update(OngoingPowerMeasure ongoingMeasure) { - extractPowerMeasure(ongoingMeasure, powermetrics.getInputStream(), pid); + public void update(OngoingPowerMeasure ongoingMeasure) { + extractPowerMeasure(ongoingMeasure, powermetrics.getInputStream(), pid, false); } AppleSiliconMeasure extractPowerMeasure(InputStream powerMeasureInput, long pid) { - return extractPowerMeasure(new OngoingPowerMeasure<>(new AppleSiliconMeasure()), powerMeasureInput, " " + pid + " "); + return extractPowerMeasure(new OngoingPowerMeasure(AppleSiliconMeasure.METADATA), powerMeasureInput, " " + pid + " ", + true); } - AppleSiliconMeasure extractPowerMeasure(OngoingPowerMeasure ongoingMeasure, + AppleSiliconMeasure extractPowerMeasure(OngoingPowerMeasure ongoingMeasure, InputStream powerMeasureInput, - String paddedPIDAsString) { - final var accumulatedPower = ongoingMeasure.sensorMeasure(); + String paddedPIDAsString, boolean returnCurrent) { try { // Should not be closed since it closes the process BufferedReader input = new BufferedReader(new InputStreamReader(powerMeasureInput)); @@ -49,6 +58,8 @@ AppleSiliconMeasure extractPowerMeasure(OngoingPowerMeasure double cpuShare = -1, gpuShare = -1; boolean totalDone = false; boolean cpuDone = false; + // start measure + ongoingMeasure.startNewMeasure(); while ((line = input.readLine()) != null) { if (line.isEmpty() || line.startsWith("*")) { continue; @@ -80,7 +91,7 @@ AppleSiliconMeasure extractPowerMeasure(OngoingPowerMeasure // look for line that contains CPU power measure if (line.startsWith("CPU Power")) { final var jmxCpuShare = PowerMeasurer.instance().cpuShareOfJVMProcess(); - accumulatedPower.addCPU(extractAttributedMeasure(line, cpuShare)); + ongoingMeasure.setComponent(cpu, extractAttributedMeasure(line, cpuShare)); accumulatedCPUShareDiff += (cpuShare - jmxCpuShare); cpuDone = true; } @@ -88,19 +99,21 @@ AppleSiliconMeasure extractPowerMeasure(OngoingPowerMeasure } if (line.startsWith("GPU Power")) { - accumulatedPower.addGPU(extractAttributedMeasure(line, gpuShare)); + ongoingMeasure.setComponent(gpu, extractAttributedMeasure(line, gpuShare)); continue; } if (line.startsWith("ANE Power")) { - accumulatedPower.addANE(extractAttributedMeasure(line, 1)); + ongoingMeasure.setComponent(ane, extractAttributedMeasure(line, 1)); break; } } + + final var measure = ongoingMeasure.stopMeasure(); + return returnCurrent ? new AppleSiliconMeasure(measure) : null; } catch (Exception exception) { throw new RuntimeException(exception); } - return accumulatedPower; } private static double extractAttributedMeasure(String line, double attributionRatio) { @@ -110,13 +123,13 @@ private static double extractAttributedMeasure(String line, double attributionRa } @Override - public OngoingPowerMeasure start(long duration, long frequency) throws Exception { + public OngoingPowerMeasure start(long duration, long frequency) throws Exception { final var freq = Long.toString(Math.round(frequency)); powermetrics = Runtime.getRuntime() .exec("sudo powermetrics --samplers cpu_power,tasks --show-process-samp-norm --show-process-gpu -i " + freq); accumulatedCPUShareDiff = 0.0; - return new OngoingPowerMeasure<>(new AppleSiliconMeasure()); + return new OngoingPowerMeasure(AppleSiliconMeasure.METADATA); } @Override @@ -125,7 +138,12 @@ public void stop() { } @Override - public Optional additionalInfo(PowerMeasure measure) { + public Optional additionalInfo(PowerMeasure measure) { return Optional.of("Powermetrics vs JMX CPU share accumulated difference: " + accumulatedCPUShareDiff); } + + @Override + public AppleSiliconMeasure measureFor(double[] measureComponents) { + return new AppleSiliconMeasure(measureComponents); + } } diff --git a/runtime/src/test/java/io/quarkiverse/power/runtime/PowerMeasurerTest.java b/runtime/src/test/java/io/quarkiverse/power/runtime/PowerMeasurerTest.java index 9f5eac7..2daa60e 100644 --- a/runtime/src/test/java/io/quarkiverse/power/runtime/PowerMeasurerTest.java +++ b/runtime/src/test/java/io/quarkiverse/power/runtime/PowerMeasurerTest.java @@ -5,14 +5,13 @@ 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 PowerSensor sensor = Mockito.spy(PowerSensorProducer.determinePowerSensor()); + final PowerSensor sensor = Mockito.spy(PowerSensorProducer.determinePowerSensor()); final var measurer = new PowerMeasurer<>(sensor); measurer.start(1, 100); diff --git a/runtime/src/test/java/io/quarkiverse/power/runtime/sensors/linux/rapl/IntelRAPLMeasureTest.java b/runtime/src/test/java/io/quarkiverse/power/runtime/sensors/linux/rapl/IntelRAPLMeasureTest.java deleted file mode 100644 index 886fd5e..0000000 --- a/runtime/src/test/java/io/quarkiverse/power/runtime/sensors/linux/rapl/IntelRAPLMeasureTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.quarkiverse.power.runtime.sensors.linux.rapl; - -import static io.quarkiverse.power.runtime.SensorMeasure.GPU; -import static org.junit.jupiter.api.Assertions.*; - -import org.junit.jupiter.api.Test; - -import io.quarkiverse.power.runtime.SensorMeasure; - -class IntelRAPLMeasureTest { - - private static final String CPU = "package-0"; - - @Test - void updateValue() { - final var measure = new IntelRAPLMeasure(); - measure.updateValue(CPU, 10000, 0.5, 100); // add 50 to cpu - measure.updateValue(GPU, 10000, 0.2, 100); // add 20 to gpu - measure.updateValue(CPU, 20000, 0.4, 100); // add 40 to cpu - measure.updateValue(CPU, 30000, 0.3, 100); // add 30 to cpu - measure.updateValue(GPU, 30000, 0.4, 100); // add 80 to cpu - - final var cpu1 = (10000 * 0.5 / 100); - final var cpu2 = (20000 - 10000) * 0.4 / 100; - final var cpu3 = (30000 - 20000) * 0.3 / 100; - final var gpu1 = 10000 * 0.2 / 100; - final var gpu2 = (30000 - 10000) * 0.4 / 100; - assertEquals((cpu1 + cpu2 + cpu3) / 1000, measure.cpu()); - assertEquals((cpu1 + cpu2 + cpu3) / 1000, measure.getValue(CPU)); - assertEquals((gpu1 + gpu2) / 1000, measure.getValue(GPU)); - assertEquals(measure.cpu() + measure.getValue(GPU), measure.total()); - assertEquals(measure.getValue(GPU), measure.gpu().orElseThrow()); - assertEquals(measure.total(), measure.byKey(SensorMeasure.TOTAL).orElseThrow()); - } -} diff --git a/runtime/src/test/java/io/quarkiverse/power/runtime/sensors/linux/rapl/RAPLFileTest.java b/runtime/src/test/java/io/quarkiverse/power/runtime/sensors/linux/rapl/RAPLFileTest.java index 3170074..b0b7b8c 100644 --- a/runtime/src/test/java/io/quarkiverse/power/runtime/sensors/linux/rapl/RAPLFileTest.java +++ b/runtime/src/test/java/io/quarkiverse/power/runtime/sensors/linux/rapl/RAPLFileTest.java @@ -24,7 +24,7 @@ private static void writeThenRead() throws IOException, InterruptedException { Files.writeString(file, value + "\n"); Thread.sleep(50); - final var raplFile = IntelRAPLSensor.ByteBufferRAPLFile.createFrom(file); + final var raplFile = ByteBufferRAPLFile.createFrom(file); final var measure = raplFile.extractPowerMeasure(); assertEquals(value, measure); }