diff --git a/analysis/src/main/java/net/laprun/sustainability/power/analysis/MeanComponentProcessor.java b/analysis/src/main/java/net/laprun/sustainability/power/analysis/MeanComponentProcessor.java index ecaf70e..a037232 100644 --- a/analysis/src/main/java/net/laprun/sustainability/power/analysis/MeanComponentProcessor.java +++ b/analysis/src/main/java/net/laprun/sustainability/power/analysis/MeanComponentProcessor.java @@ -10,4 +10,18 @@ public void recordComponentValue(double value, long timestamp) { count++; mean = mean == 0 ? value : (previousSize * mean + value) / count; } + + public double mean() { + return mean; + } + + @Override + public String name() { + return "mean"; + } + + @Override + public String output() { + return "" + mean; + } } diff --git a/analysis/src/test/java/ComputeTest.java b/analysis/src/test/java/ComputeTest.java index ad5c333..d601634 100644 --- a/analysis/src/test/java/ComputeTest.java +++ b/analysis/src/test/java/ComputeTest.java @@ -1,6 +1,5 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.List; import java.util.Random; import java.util.random.RandomGenerator; @@ -8,16 +7,16 @@ import net.laprun.sustainability.power.SensorMetadata; import net.laprun.sustainability.power.analysis.Compute; +import net.laprun.sustainability.power.analysis.MeanComponentProcessor; import net.laprun.sustainability.power.measure.OngoingPowerMeasure; +import net.laprun.sustainability.power.measure.PowerMeasure; public class ComputeTest { - private final static SensorMetadata metadata = new SensorMetadata(List.of(), null) { - - @Override - public int componentCardinality() { - return 3; - } - }; + private final static SensorMetadata metadata = SensorMetadata + .withNewComponent("cp1", null, true, null, false) + .withNewComponent("cp2", null, true, null, false) + .withNewComponent("cp3", null, true, null, false) + .build(); @Test void standardDeviationShouldWork() { @@ -79,6 +78,9 @@ void averageShouldWork() { final var m3c3 = 0.0; final var measure = new OngoingPowerMeasure(metadata); + measure.registerProcessorFor(0, new MeanComponentProcessor()); + measure.registerProcessorFor(1, new MeanComponentProcessor()); + measure.registerProcessorFor(2, new MeanComponentProcessor()); final var components = new double[metadata.componentCardinality()]; components[0] = m1c1; @@ -102,9 +104,18 @@ void averageShouldWork() { assertEquals((m1c1 + m2c1 + m3c1) / 3, c1Avg, 0.0001, "Average did not match the expected value"); + final var processors = measure.processors(); + assertEquals(c1Avg, + processors.processorFor(0, MeanComponentProcessor.class).map(MeanComponentProcessor::mean).orElseThrow()); assertEquals((m1c2 + m2c2 + m3c2) / 3, c2Avg, 0.0001, "Average did not match the expected value"); + assertEquals(c2Avg, + processors.processorFor(1, MeanComponentProcessor.class).map(MeanComponentProcessor::mean).orElseThrow()); assertEquals(0, c3Avg, 0.0001, "Average did not match the expected value"); + assertEquals(0, + processors.processorFor(2, MeanComponentProcessor.class).map(MeanComponentProcessor::mean).orElseThrow()); + + System.out.println(PowerMeasure.asString(measure)); } } diff --git a/if-manifest-export/src/main/java/net/laprun/sustainability/power/impactframework/export/IFExporter.java b/if-manifest-export/src/main/java/net/laprun/sustainability/power/impactframework/export/IFExporter.java index 9bfdcd1..45d8e62 100644 --- a/if-manifest-export/src/main/java/net/laprun/sustainability/power/impactframework/export/IFExporter.java +++ b/if-manifest-export/src/main/java/net/laprun/sustainability/power/impactframework/export/IFExporter.java @@ -2,10 +2,10 @@ import java.time.Instant; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.TreeMap; import net.laprun.sustainability.impactframework.manifest.Child; import net.laprun.sustainability.impactframework.manifest.Initialize; @@ -14,7 +14,6 @@ import net.laprun.sustainability.impactframework.manifest.Metadata; import net.laprun.sustainability.impactframework.manifest.Plugin; import net.laprun.sustainability.impactframework.manifest.Tree; -import net.laprun.sustainability.power.SensorMetadata; import net.laprun.sustainability.power.measure.PowerMeasure; import net.laprun.sustainability.power.measure.StoppedPowerMeasure; @@ -32,11 +31,13 @@ public static Manifest export(StoppedPowerMeasure measure) { final List inputs = new ArrayList<>(samples); final var sensorMetadata = measure.metadata(); final int samplingFrequency = (int) measure.duration().dividedBy(samples).toMillis(); - final var components = sensorMetadata.components(); + final String[] inputNames = new String[components.size()]; + components.values().forEach(component -> inputNames[component.index()] = getInputValueName(component.name())); + for (int i = 0; i < samples; i++) { final var values = measure.getNthTimestampedMeasures(i); - inputs.add(toInput(samplingFrequency, values, components)); + inputs.add(toInput(samplingFrequency, values, inputNames)); } return new Manifest(ifMetadata, defaultInitialize, @@ -48,10 +49,12 @@ public static String getInputValueName(String componentName) { } private static Input toInput(int samplingFrequency, PowerMeasure.TimestampedMeasures values, - Map components) { - final var inputValues = new TreeMap(); - components.values().forEach( - component -> inputValues.put(getInputValueName(component.name()), values.measures()[component.index()])); + String[] inputNames) { + final var inputValues = new LinkedHashMap(inputNames.length); + final var measures = values.measures(); + for (int i = 0; i < inputNames.length; i++) { + inputValues.put(inputNames[i], measures[i]); + } return new Input(Instant.ofEpochMilli(values.timestamp()), samplingFrequency, inputValues); } } diff --git a/measure/pom.xml b/measure/pom.xml index 72b6248..5914d17 100644 --- a/measure/pom.xml +++ b/measure/pom.xml @@ -21,8 +21,6 @@ org.assertj assertj-core - 3.26.3 - test diff --git a/measure/src/main/java/net/laprun/sustainability/power/analysis/Analyzers.java b/measure/src/main/java/net/laprun/sustainability/power/analysis/Analyzers.java deleted file mode 100644 index b0eed83..0000000 --- a/measure/src/main/java/net/laprun/sustainability/power/analysis/Analyzers.java +++ /dev/null @@ -1,4 +0,0 @@ -package net.laprun.sustainability.power.analysis; - -public interface Analyzers { -} diff --git a/measure/src/main/java/net/laprun/sustainability/power/analysis/ComponentProcessor.java b/measure/src/main/java/net/laprun/sustainability/power/analysis/ComponentProcessor.java index 00beaac..716b978 100644 --- a/measure/src/main/java/net/laprun/sustainability/power/analysis/ComponentProcessor.java +++ b/measure/src/main/java/net/laprun/sustainability/power/analysis/ComponentProcessor.java @@ -3,4 +3,12 @@ public interface ComponentProcessor { default void recordComponentValue(double value, long timestamp) { } + + default String name() { + return this.getClass().getSimpleName(); + } + + default String output() { + return ""; + } } diff --git a/measure/src/main/java/net/laprun/sustainability/power/analysis/DefaultProcessors.java b/measure/src/main/java/net/laprun/sustainability/power/analysis/DefaultProcessors.java new file mode 100644 index 0000000..0caf66f --- /dev/null +++ b/measure/src/main/java/net/laprun/sustainability/power/analysis/DefaultProcessors.java @@ -0,0 +1,89 @@ +package net.laprun.sustainability.power.analysis; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import net.laprun.sustainability.power.SensorMetadata; + +public class DefaultProcessors implements Processors { + private final List[] processors; + private ComponentProcessor totalProcessor; + + @SuppressWarnings("unchecked") + public DefaultProcessors(int componentCardinality) { + this.processors = new List[componentCardinality]; + } + + @Override + public void recordMeasure(double[] components, long timestamp) { + for (var index = 0; index < components.length; index++) { + final var fIndex = index; + final var componentProcessors = processors[index]; + if (componentProcessors != null) { + componentProcessors.forEach(proc -> proc.recordComponentValue(components[fIndex], timestamp)); + } + } + } + + @Override + public void registerProcessorFor(int componentIndex, ComponentProcessor processor) { + if (processor != null) { + var processorsForComponent = processors[componentIndex]; + if (processorsForComponent == null) { + processorsForComponent = new ArrayList<>(); + processors[componentIndex] = processorsForComponent; + } + processorsForComponent.add(processor); + } + } + + @Override + public void registerTotalProcessor(ComponentProcessor processor) { + this.totalProcessor = processor; + } + + @Override + public void recordTotal(double total, long timestamp) { + if (totalProcessor != null) { + totalProcessor.recordComponentValue(total, timestamp); + } + } + + @Override + public Optional totalProcessor() { + return Optional.ofNullable(totalProcessor); + } + + @Override + public List processorsFor(int componentIndex) { + final var componentProcessors = processors[componentIndex]; + return Objects.requireNonNullElseGet(componentProcessors, List::of); + } + + @Override + public Optional processorFor(int componentIndex, Class expectedType) { + final var componentProcessors = processors[componentIndex]; + if (componentProcessors != null) { + return componentProcessors.stream().filter(expectedType::isInstance).map(expectedType::cast).findFirst(); + } else { + return Optional.empty(); + } + } + + @Override + public String output(SensorMetadata metadata) { + StringBuilder builder = new StringBuilder(); + builder.append("# Processors\n"); + for (int i = 0; i < processors.length; i++) { + final var name = metadata.metadataFor(i).name(); + builder.append(" - ").append(name).append(" component:\n"); + for (var processor : processorsFor(i)) { + builder.append(" * ").append(processor.name()) + .append(": ").append(processor.output()).append("\n"); + } + } + return builder.toString(); + } +} diff --git a/measure/src/main/java/net/laprun/sustainability/power/analysis/Processors.java b/measure/src/main/java/net/laprun/sustainability/power/analysis/Processors.java new file mode 100644 index 0000000..372bf57 --- /dev/null +++ b/measure/src/main/java/net/laprun/sustainability/power/analysis/Processors.java @@ -0,0 +1,39 @@ +package net.laprun.sustainability.power.analysis; + +import java.util.List; +import java.util.Optional; + +import net.laprun.sustainability.power.SensorMetadata; + +public interface Processors { + Processors empty = new Processors() { + }; + + default void recordMeasure(double[] components, long timestamp) { + } + + default void recordTotal(double total, long timestamp) { + } + + default void registerProcessorFor(int componentIndex, ComponentProcessor processor) { + } + + default void registerTotalProcessor(ComponentProcessor processor) { + } + + default Optional totalProcessor() { + return Optional.empty(); + } + + default List processorsFor(int componentIndex) { + return List.of(); + } + + default Optional processorFor(int componentIndex, Class expectedType) { + return Optional.empty(); + } + + default String output(SensorMetadata metadata) { + return ""; + } +} diff --git a/measure/src/main/java/net/laprun/sustainability/power/measure/OngoingPowerMeasure.java b/measure/src/main/java/net/laprun/sustainability/power/measure/OngoingPowerMeasure.java index 2d2ebf4..04a2b2f 100644 --- a/measure/src/main/java/net/laprun/sustainability/power/measure/OngoingPowerMeasure.java +++ b/measure/src/main/java/net/laprun/sustainability/power/measure/OngoingPowerMeasure.java @@ -1,45 +1,66 @@ package net.laprun.sustainability.power.measure; import java.time.Duration; +import java.util.Arrays; import java.util.BitSet; -import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.DoubleStream; import java.util.stream.IntStream; import java.util.stream.Stream; import net.laprun.sustainability.power.SensorMetadata; import net.laprun.sustainability.power.analysis.ComponentProcessor; +import net.laprun.sustainability.power.analysis.DefaultProcessors; +import net.laprun.sustainability.power.analysis.Processors; public class OngoingPowerMeasure implements PowerMeasure { private static final int DEFAULT_SIZE = 32; - private final SensorMetadata sensorMetadata; + private final SensorMetadata metadata; private final long startedAt; private final BitSet nonZeroComponents; - private final int[] totalComponents; private final int totalIndex; private final double[][] measures; - private final ComponentProcessor[] analyzers; + private final boolean shouldComputeTotals; private double minTotal = Double.MAX_VALUE; private double maxTotal; private double accumulatedTotal; private int samples; private long[] timestamps; + private Processors processors = Processors.empty; - public OngoingPowerMeasure(SensorMetadata sensorMetadata, ComponentProcessor... analyzers) { - this.sensorMetadata = sensorMetadata; + public OngoingPowerMeasure(SensorMetadata metadata) { startedAt = System.currentTimeMillis(); - final var numComponents = sensorMetadata.componentCardinality(); - // we also record the aggregated total for each component participating in the aggregated value - final var measuresNb = numComponents + 1; + final var numComponents = metadata.componentCardinality(); + + // check if we need to add an aggregated total component + final var totalComponents = metadata.totalComponents(); + final int measuresNb; + if (totalComponents.length > 0) { + shouldComputeTotals = true; + measuresNb = numComponents + 1; + + final var sumMsg = Arrays.stream(totalComponents) + .mapToObj(i -> metadata.metadataFor(i).name()) + .collect(Collectors.joining("+", "Aggregated total from (", ")")); + + // todo: compute total component properly (same unit, convert to base unit all components) + this.metadata = SensorMetadata.from(metadata) + .withNewComponent("total", sumMsg, true, "mW", false) + .build(); + totalIndex = numComponents; + } else { + shouldComputeTotals = false; + measuresNb = numComponents; + this.metadata = metadata; + totalIndex = -1; + } + measures = new double[measuresNb][DEFAULT_SIZE]; timestamps = new long[DEFAULT_SIZE]; - totalIndex = numComponents; // we don't need to record the total component as a non-zero component since it's almost never zero and we compute the std dev separately nonZeroComponents = new BitSet(numComponents); - totalComponents = sensorMetadata.totalComponents(); - this.analyzers = Objects.requireNonNullElseGet(analyzers, () -> new ComponentProcessor[0]); } @Override @@ -49,33 +70,39 @@ public int numberOfSamples() { @Override public SensorMetadata metadata() { - return sensorMetadata; + return metadata; } public void recordMeasure(double[] components) { samples++; + final var timestamp = System.currentTimeMillis(); for (int component = 0; component < components.length; component++) { final var componentValue = components[component]; // record that the value is not zero if (componentValue != 0) { nonZeroComponents.set(component); } - recordComponentValue(component, componentValue); + recordComponentValue(component, componentValue, timestamp); } // record min / max totals - final var recordedTotal = Compute.sumOfSelectedComponents(components, totalComponents); - recordComponentValue(totalIndex, recordedTotal); - accumulatedTotal += recordedTotal; - if (recordedTotal < minTotal) { - minTotal = recordedTotal; - } - if (recordedTotal > maxTotal) { - maxTotal = recordedTotal; + if (shouldComputeTotals) { + final double recordedTotal = Compute.sumOfSelectedComponents(components, metadata.totalComponents()); + recordComponentValue(totalIndex, recordedTotal, timestamp); + accumulatedTotal += recordedTotal; + if (recordedTotal < minTotal) { + minTotal = recordedTotal; + } + if (recordedTotal > maxTotal) { + maxTotal = recordedTotal; + } + processors.recordTotal(recordedTotal, timestamp); } + + processors.recordMeasure(components, timestamp); } - private void recordComponentValue(int component, double value) { + private void recordComponentValue(int component, double value, long timestamp) { final var currentSize = measures[component].length; if (currentSize <= samples) { final var newSize = currentSize * 2; @@ -88,12 +115,8 @@ private void recordComponentValue(int component, double value) { System.arraycopy(timestamps, 0, newTimestamps, 0, currentSize); timestamps = newTimestamps; } - final var timestamp = System.currentTimeMillis(); - timestamps[component] = timestamp; + timestamps[samples - 1] = timestamp; measures[component][samples - 1] = value; - for (var analyzer : analyzers) { - analyzer.recordComponentValue(value, timestamp); - } } @Override @@ -163,7 +186,27 @@ public TimestampedMeasures getNthTimestampedMeasures(int n) { } @Override - public ComponentProcessor[] analyzers() { - return analyzers; + public Processors processors() { + return processors; + } + + @Override + public void registerProcessorFor(int component, ComponentProcessor processor) { + if (processor != null) { + if (Processors.empty == processors) { + processors = new DefaultProcessors(metadata.componentCardinality()); + } + processors.registerProcessorFor(component, processor); + } + } + + @Override + public void registerTotalProcessor(ComponentProcessor processor) { + if (processor != null) { + if (Processors.empty == processors) { + processors = new DefaultProcessors(metadata.componentCardinality()); + } + processors.registerTotalProcessor(processor); + } } } diff --git a/measure/src/main/java/net/laprun/sustainability/power/measure/PowerMeasure.java b/measure/src/main/java/net/laprun/sustainability/power/measure/PowerMeasure.java index 81dc449..a8688d2 100644 --- a/measure/src/main/java/net/laprun/sustainability/power/measure/PowerMeasure.java +++ b/measure/src/main/java/net/laprun/sustainability/power/measure/PowerMeasure.java @@ -7,6 +7,7 @@ import net.laprun.sustainability.power.SensorMetadata; import net.laprun.sustainability.power.analysis.ComponentProcessor; +import net.laprun.sustainability.power.analysis.Processors; public interface PowerMeasure { @@ -15,9 +16,10 @@ static String asString(PowerMeasure measure) { final var durationInSeconds = measure.duration().getSeconds(); final var samples = measure.numberOfSamples(); final var measuredMilliWatts = measure.total(); - return String.format("%s [min: %.3f, max: %.3f] (%ds, %s samples)", + return String.format("%s [min: %.3f, max: %.3f] (%ds, %s samples)\n---\n%s", readableWithUnit(measuredMilliWatts), - measure.minMeasuredTotal(), measure.maxMeasuredTotal(), durationInSeconds, samples); + measure.minMeasuredTotal(), measure.maxMeasuredTotal(), durationInSeconds, samples, + measure.processors().output(measure.metadata())); } static String readableWithUnit(double milliWatts) { @@ -52,5 +54,9 @@ record TimestampedValue(long timestamp, double value) { record TimestampedMeasures(long timestamp, double[] measures) { } - ComponentProcessor[] analyzers(); + Processors processors(); + + void registerProcessorFor(int component, ComponentProcessor processor); + + void registerTotalProcessor(ComponentProcessor processor); } diff --git a/measure/src/main/java/net/laprun/sustainability/power/measure/StoppedPowerMeasure.java b/measure/src/main/java/net/laprun/sustainability/power/measure/StoppedPowerMeasure.java index cc826e9..74d2c92 100644 --- a/measure/src/main/java/net/laprun/sustainability/power/measure/StoppedPowerMeasure.java +++ b/measure/src/main/java/net/laprun/sustainability/power/measure/StoppedPowerMeasure.java @@ -7,6 +7,8 @@ import net.laprun.sustainability.power.SensorMetadata; import net.laprun.sustainability.power.analysis.ComponentProcessor; +import net.laprun.sustainability.power.analysis.DefaultProcessors; +import net.laprun.sustainability.power.analysis.Processors; @SuppressWarnings("unused") public class StoppedPowerMeasure implements PowerMeasure { @@ -16,7 +18,7 @@ public class StoppedPowerMeasure implements PowerMeasure { private final double total; private final double min; private final double max; - private final ComponentProcessor[] processors; + private Processors processors; public StoppedPowerMeasure(OngoingPowerMeasure powerMeasure) { this.measure = powerMeasure; @@ -26,7 +28,7 @@ public StoppedPowerMeasure(OngoingPowerMeasure powerMeasure) { this.max = powerMeasure.maxMeasuredTotal(); this.samples = powerMeasure.numberOfSamples(); final var cardinality = metadata().componentCardinality(); - processors = powerMeasure.analyzers(); + processors = powerMeasure.processors(); } @Override @@ -87,7 +89,27 @@ private int ensureIndex(int upToIndex) { } @Override - public ComponentProcessor[] analyzers() { + public Processors processors() { return processors; } + + @Override + public void registerProcessorFor(int component, ComponentProcessor processor) { + if (processor != null) { + if (Processors.empty == processors) { + processors = new DefaultProcessors(metadata().componentCardinality()); + } + processors.registerProcessorFor(component, processor); + } + } + + @Override + public void registerTotalProcessor(ComponentProcessor processor) { + if (processor != null) { + if (Processors.empty == processors) { + processors = new DefaultProcessors(metadata().componentCardinality()); + } + processors.registerTotalProcessor(processor); + } + } } diff --git a/measure/src/test/java/net/laprun/sustainability/power/measure/OngoingPowerMeasureTest.java b/measure/src/test/java/net/laprun/sustainability/power/measure/OngoingPowerMeasureTest.java index 939a582..bab89cf 100644 --- a/measure/src/test/java/net/laprun/sustainability/power/measure/OngoingPowerMeasureTest.java +++ b/measure/src/test/java/net/laprun/sustainability/power/measure/OngoingPowerMeasureTest.java @@ -3,21 +3,34 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.ArrayList; import java.util.List; +import java.util.Random; +import java.util.random.RandomGenerator; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import net.laprun.sustainability.power.SensorMetadata; +import net.laprun.sustainability.power.analysis.ComponentProcessor; public class OngoingPowerMeasureTest { - private final static SensorMetadata metadata = new SensorMetadata(List.of(), null) { + private final static SensorMetadata metadata = SensorMetadata + .withNewComponent("cp1", null, true, "mW", true) + .withNewComponent("cp2", null, true, "W", true) + .withNewComponent("cp3", null, true, "kW", true) + .build(); - @Override - public int componentCardinality() { - return 3; - } - }; + @Test + void checkThatTotalComponentIsProperlyAdded() { + final var metadata = SensorMetadata + .withNewComponent("cp1", null, true, null, false) + .withNewComponent("cp2", null, true, null, false) + .withNewComponent("cp3", null, true, null, false) + .build(); + var measure = new OngoingPowerMeasure(metadata); + assertThat(measure.metadata().totalComponents()).isEmpty(); + } @Test void testBasics() { @@ -56,8 +69,65 @@ void testBasics() { assertThat(measure.getMeasuresFor(1)).hasValue(new double[] { m1c2, m2c2, m3c2 }); assertThat(measure.getMeasuresFor(2)).isEmpty(); + var measures = measure.getNthTimestampedMeasures(0); + assertThat(measures.measures()).isEqualTo(new double[] { m1c1, m1c2, m1c3, m1total }); + measures = measure.getNthTimestampedMeasures(1); + assertThat(measures.measures()).isEqualTo(new double[] { m2c1, m2c2, m2c3, m2total }); + measures = measure.getNthTimestampedMeasures(2); + assertThat(measures.measures()).isEqualTo(new double[] { m3c1, m3c2, m3c3, m3total }); + assertEquals(m1c1 + m1c2 + m1c3 + m2c1 + m2c2 + m2c3 + m3c1 + m3c2 + m3c3, measure.total()); assertEquals(Stream.of(m1total, m2total, m3total).min(Double::compareTo).orElseThrow(), measure.minMeasuredTotal()); assertEquals(Stream.of(m1total, m2total, m3total).max(Double::compareTo).orElseThrow(), measure.maxMeasuredTotal()); } + + @Test + void processorsShouldBeCalled() { + final var random = Random.from(RandomGenerator.getDefault()); + final var m1c1 = random.nextDouble(); + final var m1c2 = random.nextDouble(); + final var m2c1 = random.nextDouble(); + final var m2c2 = random.nextDouble(); + + final var measure = new OngoingPowerMeasure(metadata); + measure.registerProcessorFor(0, new TestComponentProcessor()); + + // check that we can also associate a processor for the totals + measure.registerTotalProcessor(new TestComponentProcessor()); + + final var components = new double[metadata.componentCardinality()]; + components[0] = m1c1; + components[1] = m1c2; + measure.recordMeasure(components); + + components[0] = m2c1; + components[1] = m2c2; + measure.recordMeasure(components); + + final var processors = measure.processors(); + assertThat(processors.processorsFor(0)).hasSize(1); + assertThat(processors.processorsFor(1)).isEmpty(); + assertThat(processors.processorsFor(2)).isEmpty(); + + var maybeProc = processors.processorFor(0, TestComponentProcessor.class); + assertThat(maybeProc).isPresent(); + var processor = maybeProc.get(); + assertThat(processor.values.getFirst().value()).isEqualTo(m1c1); + assertThat(processor.values.getLast().value()).isEqualTo(m2c1); + + maybeProc = processors.totalProcessor().map(TestComponentProcessor.class::cast); + assertThat(maybeProc).isPresent(); + processor = maybeProc.get(); + assertThat(processor.values.getFirst().value()).isEqualTo(m1c1 + m1c2); + assertThat(processor.values.getLast().value()).isEqualTo(m2c1 + m2c2); + } + + private static class TestComponentProcessor implements ComponentProcessor { + final List values = new ArrayList<>(); + + @Override + public void recordComponentValue(double value, long timestamp) { + values.add(new PowerMeasure.TimestampedValue(timestamp, value)); + } + } } diff --git a/metadata/pom.xml b/metadata/pom.xml index 682585c..afb4bdb 100644 --- a/metadata/pom.xml +++ b/metadata/pom.xml @@ -16,7 +16,10 @@ eu.hoefel units - 4.1.1 + + + org.assertj + assertj-core @@ -37,4 +40,4 @@ - \ No newline at end of file + diff --git a/metadata/src/main/java/net/laprun/sustainability/power/SensorMetadata.java b/metadata/src/main/java/net/laprun/sustainability/power/SensorMetadata.java index 6f294d6..44d0393 100644 --- a/metadata/src/main/java/net/laprun/sustainability/power/SensorMetadata.java +++ b/metadata/src/main/java/net/laprun/sustainability/power/SensorMetadata.java @@ -42,23 +42,33 @@ public SensorMetadata(List components, String documentation) this.documentation = documentation; final var errors = new Errors(); final var indices = new BitSet(cardinality); + final var totalIndices = new BitSet(cardinality); components.forEach(component -> { // check that index is valid final var index = component.index; + boolean indexValid = true; if (index < 0 || index >= cardinality) { errors.addError(index + " is not a valid index: must be between 0 and " + (cardinality - 1)); + indexValid = false; } else if (indices.get(index)) { errors.addError("Multiple components are using index " + index + ": " + components.stream().filter(cm -> index == cm.index).toList()); + indexValid = false; } else { // record index as known indices.set(index); } // check that component's unit is commensurable to Watts if included in total - if (component.isIncludedInTotal && !component.isWattCommensurable()) { - errors.addError("Component " + component.name - + " is not commensurate with a power measure. It needs to be expressible in Watts."); + if (component.isIncludedInTotal) { + if (indexValid) { + totalIndices.set(index); + } + + if (!component.isWattCommensurable()) { + errors.addError("Component " + component.name + + " is not commensurate with a power measure. It needs to be expressible in Watts."); + } } if (this.components.containsKey(component.name)) { @@ -81,7 +91,7 @@ public SensorMetadata(List components, String documentation) throw new IllegalArgumentException(errors.formatErrors()); } - this.totalComponents = indices.stream().toArray(); + this.totalComponents = totalIndices.stream().toArray(); } @JsonCreator @@ -96,6 +106,15 @@ public static SensorMetadata.Builder withNewComponent(String name, String descri return new SensorMetadata.Builder().withNewComponent(name, description, isAttributed, unit, participatesInTotal); } + public static SensorMetadata.Builder from(SensorMetadata sensorMetadata) { + final var builder = new Builder(); + sensorMetadata.components.values().stream().sorted(Comparator.comparing(ComponentMetadata::index)) + .forEach(component -> builder.withNewComponent(component.name, component.description, component.isAttributed, + component.unit, + component.isIncludedInTotal)); + return builder; + } + @Override public String toString() { final var sb = new StringBuilder(); @@ -169,6 +188,20 @@ public int[] totalComponents() { return totalComponents; } + /** + * Retrieves the metadata associated with the specified component index if it exists. + * + * @param componentIndex the index of the component we want to retrieve the metadata for + * @return the {@link ComponentMetadata} associated with the specified index if it exists + * @throws IllegalArgumentException if no component is associated with the specified index + */ + public ComponentMetadata metadataFor(int componentIndex) { + return components.values().stream() + .filter(cm -> componentIndex == cm.index) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No component was found for index " + componentIndex)); + } + public static class Builder { private final List components = new ArrayList<>(); private int currentIndex = 0; diff --git a/metadata/src/test/java/net/laprun/sustainability/power/SensorMetadataTest.java b/metadata/src/test/java/net/laprun/sustainability/power/SensorMetadataTest.java index 39a3414..d395f78 100644 --- a/metadata/src/test/java/net/laprun/sustainability/power/SensorMetadataTest.java +++ b/metadata/src/test/java/net/laprun/sustainability/power/SensorMetadataTest.java @@ -1,5 +1,6 @@ package net.laprun.sustainability.power; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; import java.util.List; @@ -69,4 +70,26 @@ void shouldFailIfNoComponentsAreProvided() { final var message = e.getMessage(); assertTrue(message.contains("Must provide components")); } + + @Test + void shouldProperlyRecordTotalComponents() { + final String COMPONENT1_NAME = "c1"; + final String COMPONENT2_NAME = "c2"; + final String COMPONENT3_NAME = "c3"; + var metadata = SensorMetadata + .withNewComponent(COMPONENT1_NAME, "component 1", true, "mW", true) + .withNewComponent(COMPONENT2_NAME, "component 2", true, "mW", false) + .withNewComponent(COMPONENT3_NAME, "component 3", true, "mW", true) + .build(); + + assertArrayEquals(new int[] { 0, 2 }, metadata.totalComponents()); + + metadata = SensorMetadata + .withNewComponent("cp1", null, true, null, false) + .withNewComponent("cp2", null, true, null, false) + .withNewComponent("cp3", null, true, null, false) + .build(); + + assertThat(metadata.totalComponents()).isEmpty(); + } } diff --git a/pom.xml b/pom.xml index 48c9a6f..b32c9c6 100644 --- a/pom.xml +++ b/pom.xml @@ -44,8 +44,12 @@ UTF-8 quarkus-bom io.quarkus - 3.17.4 true + + 3.17.4 + 3.26.3 + 4.1.1 + 3.5.2 3.5.2 2.24.1 @@ -82,6 +86,17 @@ pom import + + eu.hoefel + units + ${units.version} + + + org.assertj + assertj-core + ${assertj.version} + test +