diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/SamplingMeasurer.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/SamplingMeasurer.java index ab191ea7..0ad287d8 100644 --- a/backend/src/main/java/net/laprun/sustainability/power/sensors/SamplingMeasurer.java +++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/SamplingMeasurer.java @@ -54,6 +54,8 @@ public Cancellable startTrackingApp(String appName, long pid, String session) th } RegisteredPID track(long pid) throws Exception { + final var registeredPID = sensor.register(pid); + if (!sensor.isStarted()) { sensor.start(samplingPeriod.toMillis()); periodicSensorCheck = Multi.createFrom().ticks() @@ -65,9 +67,7 @@ RegisteredPID track(long pid) throws Exception { .toAtLeast(1) .runSubscriptionOn(Infrastructure.getDefaultWorkerPool()); } - final var registeredPID = sensor.register(pid); - // todo: the timing of things could make it so that the pid has been removed before the map operation occurs so - // currently return -1 instead of null but this needs to be properly addressed + periodicSensorCheck = periodicSensorCheck.onCancellation().invoke(() -> sensor.unregister(registeredPID)); return registeredPID; } diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/AppleSiliconCPU.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/AppleSiliconCPU.java index b07f2f45..0cd3eb46 100644 --- a/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/AppleSiliconCPU.java +++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/AppleSiliconCPU.java @@ -14,7 +14,7 @@ class AppleSiliconCPU extends CPU { private static final SensorMetadata.ComponentMetadata gpuComponent = new SensorMetadata.ComponentMetadata(GPU, 1, "GPU power", true, mW); private static final SensorMetadata.ComponentMetadata aneComponent = new SensorMetadata.ComponentMetadata(ANE, 2, - "Apple Neural Engine power", false, mW); + "Apple Neural Engine power", true, mW); private static final SensorMetadata.ComponentMetadata cpuShareComponent = new SensorMetadata.ComponentMetadata(CPU_SHARE, 3, "Computed share of CPU", false, decimalPercentage); private static final String COMBINED = "Combined"; 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 cb0d197f..ec19a7dc 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,6 +9,10 @@ import net.laprun.sustainability.power.SensorMetadata; import net.laprun.sustainability.power.SensorUnit; +/** + * Totaler provides a way to compute the aggregated total for a subset of sensor measures, ensuring that the proper conversions + * between commensurate units are used. + */ public class Totaler { private final SensorUnit expectedResultUnit; private final Function formula; @@ -17,6 +21,19 @@ public class Totaler { private final boolean isAttributed; private Errors errors; + /** + * Create a new Totaler instance working with the specified {@link SensorMetadata} and computing a total measure using the + * provided expected result unit, adding (and converting to the target unit, if needed) values for the provided component + * indices. If no indices are provided, components will be automatically chosen as follows: only components using units + * compatible with the target unit will be used, and, among these, as mixing attributed and non-attributed values will + * result in useless results, priority will be given to attributed components. If no attributed component is found, then + * unattributed components will be considered. + * + * @param metadata the sensor metadata providing the component information to use as basis to compute an aggregate value + * @param expectedResultUnit a {@link SensorUnit} representing the unit with which the aggregate should be calculated + * @param totalComponentIndices optional list of component indices to take into account, if no such indices are provided, + * the Totaler will select "appropriate" indices automatically, if possible + */ public Totaler(SensorMetadata metadata, SensorUnit expectedResultUnit, int... totalComponentIndices) { this.expectedResultUnit = Objects.requireNonNull(expectedResultUnit, "Must specify expected result unit"); @@ -26,12 +43,21 @@ public Totaler(SensorMetadata metadata, SensorUnit expectedResultUnit, int... to 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); + // first, only select attributed components + var maybeComponents = createTotalComponents(metadata, expectedResultUnit, true); + attributed[0] = true; + // if there are no commensurate attributed components, look for unattributed ones + if (maybeComponents.length == 0) { + maybeComponents = createTotalComponents(metadata, expectedResultUnit, false); + attributed[0] = false; + } + if (maybeComponents.length == 0) { + addError("No components are compatible with the expected result unit " + expectedResultUnit); + validate(); // exit immediately + } + // record total indices + totalComponents = maybeComponents; totalComponentIndices = new int[totalComponents.length]; int i = 0; for (var component : totalComponents) { @@ -61,12 +87,23 @@ public Totaler(SensorMetadata metadata, SensorUnit expectedResultUnit, int... to validate(); } - private static boolean checkAggregatedAttribution(boolean isComponentAttributed, Boolean[] aggregateAttribution) { + private TotalComponent[] createTotalComponents(SensorMetadata metadata, SensorUnit expectedResultUnit, + boolean attributed) { + return metadata.components().values().stream() + .filter(cm -> attributed == cm.isAttributed()) + .filter(cm -> cm.unit().isCommensurableWith(expectedResultUnit)) + .map(cm -> new TotalComponent(cm.name(), cm.index(), cm.unit().factor(), cm.isAttributed())) + .toArray(TotalComponent[]::new); + } + + private boolean checkAggregatedAttribution(boolean isComponentAttributed, Boolean[] aggregateAttribution) { var currentAttribution = aggregateAttribution[0]; if (currentAttribution == null) { currentAttribution = isComponentAttributed; } else { - currentAttribution = currentAttribution && isComponentAttributed; + if (currentAttribution != isComponentAttributed) { + addError(Errors.ATTRIBUTION_MIX_ERROR); + } } aggregateAttribution[0] = currentAttribution; return isComponentAttributed; 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 38ba2ec0..33c94dcc 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 @@ -1,11 +1,13 @@ package net.laprun.sustainability.power.analysis.total; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.*; import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import net.laprun.sustainability.power.Errors; import net.laprun.sustainability.power.SensorMetadata; import net.laprun.sustainability.power.SensorUnit; @@ -45,8 +47,10 @@ void attributedShouldWork() { assertTrue(totaler.isAttributed()); totaler = new Totaler(metadata, expectedResultUnit, 3); assertFalse(totaler.isAttributed()); - totaler = new Totaler(metadata, expectedResultUnit, 0, 2, 3); - assertFalse(totaler.isAttributed()); + + assertThatThrownBy(() -> new Totaler(metadata, expectedResultUnit, 0, 2, 3)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(Errors.ATTRIBUTION_MIX_ERROR); } @Test @@ -54,14 +58,14 @@ 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") + .withNewComponent("cp3", null, true, "µW") + .withNewComponent("cp4", 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()); + assertTrue(totaler.isAttributed()); + assertArrayEquals(new int[] { 0, 2 }, totaler.componentIndices()); } @Test diff --git a/metadata/src/main/java/net/laprun/sustainability/power/Errors.java b/metadata/src/main/java/net/laprun/sustainability/power/Errors.java index 460e768f..1936ffdc 100644 --- a/metadata/src/main/java/net/laprun/sustainability/power/Errors.java +++ b/metadata/src/main/java/net/laprun/sustainability/power/Errors.java @@ -6,12 +6,15 @@ public class Errors { private List errors; + public static final String ATTRIBUTION_MIX_ERROR = "Cannot aggregate attributed and non-attributed components"; public void addError(String error) { if (errors == null) { errors = new ArrayList<>(); } - errors.add(error); + if (!errors.contains(error)) { + errors.add(error); + } } public boolean hasErrors() { diff --git a/persistence/src/main/java/net/laprun/sustainability/power/persistence/Persistence.java b/persistence/src/main/java/net/laprun/sustainability/power/persistence/Persistence.java index 2f67389c..9e9765ad 100644 --- a/persistence/src/main/java/net/laprun/sustainability/power/persistence/Persistence.java +++ b/persistence/src/main/java/net/laprun/sustainability/power/persistence/Persistence.java @@ -15,6 +15,10 @@ public class Persistence { @Transactional public Measure save(SensorMeasure measure, String appName, String session) { + if (SensorMeasure.missing == measure) { + Log.debugf("Ignoring missing measure for app: %s, session: %s", appName, session); + return null; + } final var persisted = new Measure(); persisted.components = measure.components(); persisted.appName = appName; @@ -35,7 +39,6 @@ public Measure save(SensorMeasure measure, String appName) { public Optional synthesizeAndAggregateForSession(String appName, String session, Function synthesizer) { return Measure.forApplicationSession(appName, session) - .filter(measure -> measure.components.length != 1) .map(synthesizer) .reduce(Double::sum); }