From 0528c0623043ff776bc4fc1df1676fd2ca0a3c4e Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Wed, 12 Nov 2025 11:28:33 +0100 Subject: [PATCH 1/2] feat: Totaler selects commensurate components if none explicitly given --- .../analysis/total/TotalMeasureProcessor.java | 1 - .../total/TotalSyntheticComponent.java | 18 ++--- .../power/analysis/total/Totaler.java | 66 ++++++++++++++++--- .../analysis/total/TotalComputationTest.java | 33 ++++++++++ 4 files changed, 97 insertions(+), 21 deletions(-) diff --git a/measure/src/main/java/net/laprun/sustainability/power/analysis/total/TotalMeasureProcessor.java b/measure/src/main/java/net/laprun/sustainability/power/analysis/total/TotalMeasureProcessor.java index b05438c4..87e777f4 100644 --- a/measure/src/main/java/net/laprun/sustainability/power/analysis/total/TotalMeasureProcessor.java +++ b/measure/src/main/java/net/laprun/sustainability/power/analysis/total/TotalMeasureProcessor.java @@ -12,7 +12,6 @@ public class TotalMeasureProcessor implements MeasureProcessor { public TotalMeasureProcessor(SensorMetadata metadata, SensorUnit expectedResultUnit, int... totalComponentIndices) { this.totaler = new Totaler(metadata, expectedResultUnit, totalComponentIndices); - totaler.validate(); } public double total() { diff --git a/measure/src/main/java/net/laprun/sustainability/power/analysis/total/TotalSyntheticComponent.java b/measure/src/main/java/net/laprun/sustainability/power/analysis/total/TotalSyntheticComponent.java index 0488b133..f56a1414 100644 --- a/measure/src/main/java/net/laprun/sustainability/power/analysis/total/TotalSyntheticComponent.java +++ b/measure/src/main/java/net/laprun/sustainability/power/analysis/total/TotalSyntheticComponent.java @@ -10,18 +10,14 @@ public class TotalSyntheticComponent implements SyntheticComponent { private final SensorMetadata.ComponentMetadata metadata; public TotalSyntheticComponent(SensorMetadata metadata, SensorUnit expectedResultUnit, int... totalComponentIndices) { - this.totaler = new Totaler(metadata, expectedResultUnit, totalComponentIndices); - final var isAttributed = metadata.components().values().stream() - .map(SensorMetadata.ComponentMetadata::isAttributed) - .reduce(Boolean::logicalAnd).orElse(false); - final var name = totaler.name(); - if (metadata.exists(name)) { - totaler.addError("Component " + name + " already exists"); - } - - totaler.validate(); + this(new Totaler(metadata, expectedResultUnit, totalComponentIndices)); + } - this.metadata = new SensorMetadata.ComponentMetadata(name, "Aggregated " + name, isAttributed, expectedResultUnit); + private TotalSyntheticComponent(Totaler totaler) { + this.totaler = totaler; + final var name = totaler.name(); + this.metadata = new SensorMetadata.ComponentMetadata(name, "Aggregated " + name, totaler.isAttributed(), + totaler.expectedResultUnit()); } @Override diff --git a/measure/src/main/java/net/laprun/sustainability/power/analysis/total/Totaler.java b/measure/src/main/java/net/laprun/sustainability/power/analysis/total/Totaler.java index 1b34b92a..41d633be 100644 --- a/measure/src/main/java/net/laprun/sustainability/power/analysis/total/Totaler.java +++ b/measure/src/main/java/net/laprun/sustainability/power/analysis/total/Totaler.java @@ -14,24 +14,65 @@ class Totaler { private final Function formula; private final String name; private final int[] totalComponentIndices; + private final boolean isAttributed; private Errors errors; Totaler(SensorMetadata metadata, SensorUnit expectedResultUnit, int... totalComponentIndices) { - Objects.requireNonNull(totalComponentIndices, "Must specify component indices that will aggregated in a total"); this.expectedResultUnit = Objects.requireNonNull(expectedResultUnit, "Must specify expected result unit"); errors = new Errors(); - final var totalComponents = Arrays.stream(totalComponentIndices) - .mapToObj(i -> from(metadata, i, expectedResultUnit, errors)) - .toArray(TotalComponent[]::new); + + final TotalComponent[] totalComponents; + final var attributed = new Boolean[1]; + if (totalComponentIndices == null || totalComponentIndices.length == 0) { + // automatically aggregate components commensurate with the expected result unit + totalComponents = metadata.components().values().stream() + .filter(cm -> cm.unit().isCommensurableWith(expectedResultUnit)) + .map(cm -> new TotalComponent(cm.name(), cm.index(), cm.unit().factor(), + checkAggregatedAttribution(cm.isAttributed(), attributed))) + .toArray(TotalComponent[]::new); + // record total indices + totalComponentIndices = new int[totalComponents.length]; + int i = 0; + for (var component : totalComponents) { + totalComponentIndices[i++] = component.index(); + } + } else { + totalComponents = Arrays.stream(totalComponentIndices) + .mapToObj(i -> { + final var cm = metadata.metadataFor(i); + checkAggregatedAttribution(cm.isAttributed(), attributed); + return from(cm, expectedResultUnit, errors); + }) + .toArray(TotalComponent[]::new); + } + name = Arrays.stream(totalComponents) .map(TotalComponent::name) .collect(Collectors.joining(" + ", "total (", ")")); + if (metadata.exists(name)) { + addError("Component " + name + " already exists"); + } + + isAttributed = attributed[0]; + formula = formulaFrom(totalComponents); this.totalComponentIndices = totalComponentIndices; + validate(); } - void validate() { + private static boolean checkAggregatedAttribution(boolean isComponentAttributed, Boolean[] aggregateAttribution) { + var currentAttribution = aggregateAttribution[0]; + if (currentAttribution == null) { + currentAttribution = isComponentAttributed; + } else { + currentAttribution = currentAttribution && isComponentAttributed; + } + aggregateAttribution[0] = currentAttribution; + return isComponentAttributed; + } + + private void validate() { if (errors.hasErrors()) { throw new IllegalArgumentException(errors.formatErrors()); } @@ -63,6 +104,14 @@ public double computeTotalFrom(double[] measure) { return convertToExpectedUnit(formula.apply(measure)); } + public boolean isAttributed() { + return isAttributed; + } + + int[] componentIndices() { + return totalComponentIndices; + } + private void checkValidated() { if (errors != null) { throw new IllegalStateException("Totaler must be validated before use!"); @@ -73,14 +122,13 @@ private double convertToExpectedUnit(double value) { return value * expectedResultUnit.base().conversionFactorTo(expectedResultUnit); } - private record TotalComponent(String name, int index, double factor) { + private record TotalComponent(String name, int index, double factor, boolean isAttributed) { double scaledValueFrom(double[] componentValues) { return componentValues[index] * factor; } } - private static TotalComponent from(SensorMetadata metadata, int index, SensorUnit expectedResultUnit, Errors errors) { - final var cm = metadata.metadataFor(index); + private static TotalComponent from(SensorMetadata.ComponentMetadata cm, SensorUnit expectedResultUnit, Errors errors) { final var name = cm.name(); final var unit = cm.unit(); if (!unit.isCommensurableWith(expectedResultUnit)) { @@ -89,7 +137,7 @@ private static TotalComponent from(SensorMetadata metadata, int index, SensorUni } final var factor = unit.factor(); - return new TotalComponent(name, index, factor); + return new TotalComponent(name, cm.index(), factor, cm.isAttributed()); } private static Function formulaFrom(TotalComponent[] totalComponents) { diff --git a/measure/src/test/java/net/laprun/sustainability/power/analysis/total/TotalComputationTest.java b/measure/src/test/java/net/laprun/sustainability/power/analysis/total/TotalComputationTest.java index aec13518..38ba2ec0 100644 --- a/measure/src/test/java/net/laprun/sustainability/power/analysis/total/TotalComputationTest.java +++ b/measure/src/test/java/net/laprun/sustainability/power/analysis/total/TotalComputationTest.java @@ -31,6 +31,39 @@ void totalShouldFailIfAllComponentsAreNotCommensurable() { + " is not commensurable with the expected base unit: " + expectedResultUnit)); } + @Test + void attributedShouldWork() { + final var metadata = SensorMetadata + .withNewComponent("cp1", null, true, "mW") + .withNewComponent("cp2", null, true, "mJ") + .withNewComponent("cp3", null, true, "mW") + .withNewComponent("cp24", null, false, "W") + .build(); + + final var expectedResultUnit = SensorUnit.W; + var totaler = new Totaler(metadata, expectedResultUnit, 0, 2); + assertTrue(totaler.isAttributed()); + totaler = new Totaler(metadata, expectedResultUnit, 3); + assertFalse(totaler.isAttributed()); + totaler = new Totaler(metadata, expectedResultUnit, 0, 2, 3); + assertFalse(totaler.isAttributed()); + } + + @Test + void shouldAutomaticallyPickCommensurateComponentsIfNoIndicesAreProvided() { + final var metadata = SensorMetadata + .withNewComponent("cp1", null, true, "mW") + .withNewComponent("cp2", null, true, "mJ") + .withNewComponent("cp3", null, true, "mW") + .withNewComponent("cp24", null, false, "W") + .build(); + + final var expectedResultUnit = SensorUnit.W; + var totaler = new Totaler(metadata, expectedResultUnit); + assertFalse(totaler.isAttributed()); + assertArrayEquals(new int[] { 0, 2, 3 }, totaler.componentIndices()); + } + @Test void testTotal() { final var metadata = SensorMetadata From 975dd6213bdb3d16a713335e4b1a6b6be20bf020 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Wed, 12 Nov 2025 11:33:06 +0100 Subject: [PATCH 2/2] feat: Power uses Totaler directly with automatically selected components This allows for the same logic to be used regardless of actual components provided by the platform. --- .../java/net/laprun/sustainability/cli/Power.java | 11 +++++------ .../sustainability/power/analysis/total/Totaler.java | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/cli/src/main/java/net/laprun/sustainability/cli/Power.java b/cli/src/main/java/net/laprun/sustainability/cli/Power.java index 4c13333e..46017d4d 100644 --- a/cli/src/main/java/net/laprun/sustainability/cli/Power.java +++ b/cli/src/main/java/net/laprun/sustainability/cli/Power.java @@ -12,7 +12,7 @@ import net.laprun.sustainability.power.Measure; import net.laprun.sustainability.power.ProcessUtils; import net.laprun.sustainability.power.SensorUnit; -import net.laprun.sustainability.power.analysis.total.TotalSyntheticComponent; +import net.laprun.sustainability.power.analysis.total.Totaler; import net.laprun.sustainability.power.nuprocess.BaseProcessHandler; import net.laprun.sustainability.power.persistence.Persistence; import net.laprun.sustainability.power.sensors.SamplingMeasurer; @@ -83,13 +83,12 @@ private Measure extractPowerConsumption(String applicationName) { // first read metadata final var metadata = measurer.metadata(); - // create a synthetic component to get the total power - final var totaler = new TotalSyntheticComponent(metadata, SensorUnit.W, 0, 1, 2); - + // get the total power + final var totaler = new Totaler(metadata, SensorUnit.W); final var appPower = measurer.persistence() .synthesizeAndAggregateForSession(applicationName, session, - m -> totaler.synthesizeFrom(m.components, m.startTime)) - .map(measure -> new Measure(measure, totaler.metadata().unit())) + m -> totaler.computeTotalFrom(m.components)) + .map(measure -> new Measure(measure, totaler.expectedResultUnit())) .orElseThrow(() -> new RuntimeException("Could not extract power consumption")); Quarkus.asyncExit(); diff --git a/measure/src/main/java/net/laprun/sustainability/power/analysis/total/Totaler.java b/measure/src/main/java/net/laprun/sustainability/power/analysis/total/Totaler.java index 41d633be..cb0d197f 100644 --- a/measure/src/main/java/net/laprun/sustainability/power/analysis/total/Totaler.java +++ b/measure/src/main/java/net/laprun/sustainability/power/analysis/total/Totaler.java @@ -9,7 +9,7 @@ import net.laprun.sustainability.power.SensorMetadata; import net.laprun.sustainability.power.SensorUnit; -class Totaler { +public class Totaler { private final SensorUnit expectedResultUnit; private final Function formula; private final String name; @@ -17,7 +17,7 @@ class Totaler { private final boolean isAttributed; private Errors errors; - Totaler(SensorMetadata metadata, SensorUnit expectedResultUnit, int... totalComponentIndices) { + public Totaler(SensorMetadata metadata, SensorUnit expectedResultUnit, int... totalComponentIndices) { this.expectedResultUnit = Objects.requireNonNull(expectedResultUnit, "Must specify expected result unit"); errors = new Errors();