diff --git a/analysis/src/test/java/ComputeTest.java b/analysis/src/test/java/ComputeTest.java index d751526..ad5c333 100644 --- a/analysis/src/test/java/ComputeTest.java +++ b/analysis/src/test/java/ComputeTest.java @@ -1,6 +1,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.Map; +import java.util.List; import java.util.Random; import java.util.random.RandomGenerator; @@ -11,7 +11,7 @@ import net.laprun.sustainability.power.measure.OngoingPowerMeasure; public class ComputeTest { - private final static SensorMetadata metadata = new SensorMetadata(Map.of(), null, new int[0]) { + private final static SensorMetadata metadata = new SensorMetadata(List.of(), null) { @Override public int componentCardinality() { diff --git a/if-manifest-export/src/test/java/net/laprun/sustainability/power/impactframework/export/IFExporterTest.java b/if-manifest-export/src/test/java/net/laprun/sustainability/power/impactframework/export/IFExporterTest.java index af0e665..f5616f9 100644 --- a/if-manifest-export/src/test/java/net/laprun/sustainability/power/impactframework/export/IFExporterTest.java +++ b/if-manifest-export/src/test/java/net/laprun/sustainability/power/impactframework/export/IFExporterTest.java @@ -2,8 +2,6 @@ import static org.junit.jupiter.api.Assertions.*; -import java.util.Map; - import org.junit.jupiter.api.Test; import net.laprun.sustainability.power.SensorMetadata; @@ -14,11 +12,11 @@ class IFExporterTest { public static final String COMPONENT1_NAME = "c1"; public static final String COMPONENT2_NAME = "c2"; public static final String COMPONENT3_NAME = "c3"; - private final static SensorMetadata metadata = new SensorMetadata(Map.of( - COMPONENT1_NAME, new SensorMetadata.ComponentMetadata(COMPONENT1_NAME, 0, "component 1", true, "mW"), - COMPONENT2_NAME, new SensorMetadata.ComponentMetadata(COMPONENT2_NAME, 1, "component 2", true, "mW"), - COMPONENT3_NAME, new SensorMetadata.ComponentMetadata(COMPONENT3_NAME, 2, "always zero", false, "mW")), null, - new int[] { 0, 1, 2 }); + private final static SensorMetadata metadata = SensorMetadata + .withNewComponent(COMPONENT1_NAME, "component 1", true, "mW", true) + .withNewComponent(COMPONENT2_NAME, "component 2", true, "mW", true) + .withNewComponent(COMPONENT3_NAME, "component 3", true, "mW", true) + .build(); @Test void export() { 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 8369e8f..939a582 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,7 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.Map; +import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.Test; @@ -11,7 +11,7 @@ import net.laprun.sustainability.power.SensorMetadata; public class OngoingPowerMeasureTest { - private final static SensorMetadata metadata = new SensorMetadata(Map.of(), null, new int[0]) { + private final static SensorMetadata metadata = new SensorMetadata(List.of(), null) { @Override public int componentCardinality() { 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 88dbec4..6f294d6 100644 --- a/metadata/src/main/java/net/laprun/sustainability/power/SensorMetadata.java +++ b/metadata/src/main/java/net/laprun/sustainability/power/SensorMetadata.java @@ -2,8 +2,10 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.BitSet; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -30,37 +32,68 @@ public class SensorMetadata { * * @param components a map describing the metadata for each component * @param documentation a text providing any relevant information associated with the described sensor - * @param totalComponents an array of indices indicating which components can be used to compute a total power consumption - * metric for that sensor. Must use a unit commensurable with {@link SensorUnit#W} * @throws IllegalArgumentException if indices specified in {@code totalComponents} do not represent power measures * expressible in Watts or are not a valid index */ - @JsonCreator - public SensorMetadata(@JsonProperty("metadata") Map components, - @JsonProperty("documentation") String documentation, - @JsonProperty("totalComponents") int[] totalComponents) { - this.components = Objects.requireNonNull(components, "Must provide components"); + public SensorMetadata(List components, String documentation) { + Objects.requireNonNull(components, "Must provide components"); + final var cardinality = components.size(); + this.components = new HashMap<>(cardinality); this.documentation = documentation; - this.totalComponents = Objects.requireNonNull(totalComponents, "Must provide total components"); final var errors = new Errors(); - for (int index : totalComponents) { - if (index < 0) { - errors.addError(index + " is not a valid index"); - continue; + final var indices = new BitSet(cardinality); + components.forEach(component -> { + // check that index is valid + final var index = component.index; + if (index < 0 || index >= cardinality) { + errors.addError(index + " is not a valid index: must be between 0 and " + (cardinality - 1)); + } else if (indices.get(index)) { + errors.addError("Multiple components are using index " + index + ": " + + components.stream().filter(cm -> index == cm.index).toList()); + } 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."); } - components.values().stream() - .filter(cm -> index == cm.index) - .findFirst() - .ifPresentOrElse(component -> { - if (!component.isWattCommensurable()) { - errors.addError("Component " + component.name - + " is not commensurate with a power measure. It needs to be expressible in Watts."); - } - }, () -> errors.addError(index + " is not a valid index")); + + if (this.components.containsKey(component.name)) { + errors.addError("Multiple components are named '" + component.name + "': " + + components.stream().filter(cm -> cm.name.equals(component.name)).toList()); + } else { + this.components.put(component.name, component); + } + }); + + // verify that all indices are covered + if (indices.cardinality() != cardinality) { + indices.flip(0, cardinality); + errors.addError( + "Components' indices should cover the full range of 0 to " + (cardinality - 1) + ". Missing indices: " + + indices); } + if (errors.hasErrors()) { throw new IllegalArgumentException(errors.formatErrors()); } + + this.totalComponents = indices.stream().toArray(); + } + + @JsonCreator + SensorMetadata(Map components, String documentation, int[] totalComponents) { + this.components = components; + this.documentation = documentation; + this.totalComponents = totalComponents; + } + + public static SensorMetadata.Builder withNewComponent(String name, String description, boolean isAttributed, String unit, + boolean participatesInTotal) { + return new SensorMetadata.Builder().withNewComponent(name, description, isAttributed, unit, participatesInTotal); } @Override @@ -136,6 +169,27 @@ public int[] totalComponents() { return totalComponents; } + public static class Builder { + private final List components = new ArrayList<>(); + private int currentIndex = 0; + private String documentation; + + public Builder withNewComponent(String name, String description, boolean isAttributed, String unit, + boolean isIncludedInTotal) { + components.add(new ComponentMetadata(name, currentIndex++, description, isAttributed, unit, isIncludedInTotal)); + return this; + } + + public Builder withDocumentation(String documentation) { + this.documentation = documentation; + return this; + } + + public SensorMetadata build() { + return new SensorMetadata(components, documentation); + } + } + private static class Errors { private List errors; @@ -171,8 +225,19 @@ String formatErrors() { * attributed share for each process needs to be performed. This is needed because some sensors only provide * system-wide measures instead of on a per-process basis. * @param unit a textual representation of the unit used for measures associated with this component (e.g. mW) + * @param isIncludedInTotal whether or not this component takes part in the computation to get a total power consumption + * metric for that sensor. Components that take part of the total computation must use a unit commensurable with + * {@link SensorUnit#W} */ - public record ComponentMetadata(String name, int index, String description, boolean isAttributed, String unit) { + public record ComponentMetadata(String name, int index, String description, boolean isAttributed, String unit, + boolean isIncludedInTotal) { + + public ComponentMetadata { + if (name == null) { + throw new IllegalArgumentException("Component name cannot be null"); + } + } + /** * Determines whether or not this component is measuring power (i.e. its value can be converted to Watts) * @@ -180,7 +245,7 @@ public record ComponentMetadata(String name, int index, String description, bool */ @JsonIgnore public boolean isWattCommensurable() { - return SensorUnit.of(unit).isWattCommensurable(); + return unit != null && SensorUnit.of(unit).isWattCommensurable(); } } } diff --git a/metadata/src/main/java/net/laprun/sustainability/power/SensorUnit.java b/metadata/src/main/java/net/laprun/sustainability/power/SensorUnit.java index 1642fba..b11186e 100644 --- a/metadata/src/main/java/net/laprun/sustainability/power/SensorUnit.java +++ b/metadata/src/main/java/net/laprun/sustainability/power/SensorUnit.java @@ -15,6 +15,7 @@ private SensorUnit(String symbol) { } public static SensorUnit of(String unit) { + Objects.requireNonNull(unit); return switch (unit) { case mW -> mWUnit; case W -> WUnit; 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 2409f1e..39a3414 100644 --- a/metadata/src/test/java/net/laprun/sustainability/power/SensorMetadataTest.java +++ b/metadata/src/test/java/net/laprun/sustainability/power/SensorMetadataTest.java @@ -2,46 +2,70 @@ import static org.junit.jupiter.api.Assertions.*; -import java.util.Collections; -import java.util.Map; +import java.util.List; import org.junit.jupiter.api.Test; class SensorMetadataTest { + @Test void shouldFailIfTotalComponentsAreOutOfRange() { + final var name = "comp0"; final var e = assertThrows(IllegalArgumentException.class, - () -> new SensorMetadata(Collections.emptyMap(), "", new int[] { 0, 1, -1 })); + () -> new SensorMetadata(List.of(new SensorMetadata.ComponentMetadata(name, -1, null, true, null, true)), "")); final var message = e.getMessage(); - assertTrue(message.contains("0")); - assertTrue(message.contains("1")); + assertTrue(message.contains(name) && message.contains("Watts")); + assertTrue(message.contains("range")); assertTrue(message.contains("-1")); } @Test - void shouldFailIfTotalComponentsAreNotCommensurateToWatts() { + void shouldFailIfComponentHasNullName() { final var e = assertThrows(IllegalArgumentException.class, - () -> new SensorMetadata( - Map.of("foo", new SensorMetadata.ComponentMetadata("foo", 0, "", false, SensorUnit.decimalPercentage)), - "", new int[] { 0, 1, -1 })); + () -> new SensorMetadata(List.of(new SensorMetadata.ComponentMetadata(null, -1, null, true, null, true)), "")); final var message = e.getMessage(); - assertTrue(message.contains("1")); - assertTrue(message.contains("-1")); + assertTrue(message.contains("Component name cannot be null")); + } + + @Test + void shouldFailOnDuplicatedComponentNames() { + final var name = "component"; + final var e = assertThrows(IllegalArgumentException.class, + () -> new SensorMetadata(List.of( + new SensorMetadata.ComponentMetadata(name, 0, null, true, null, false), + new SensorMetadata.ComponentMetadata(name, 1, null, true, null, false)), "")); + final var message = e.getMessage(); + assertTrue(message.contains(name) && message.contains("0") && message.contains("1")); + } + + @Test + void shouldFailIfComponentsDoNotCoverFullRange() { + final var e = assertThrows(IllegalArgumentException.class, + () -> new SensorMetadata(List.of( + new SensorMetadata.ComponentMetadata("foo", 0, null, true, null, false), + new SensorMetadata.ComponentMetadata("component2", 0, null, true, null, false)), "")); + final var message = e.getMessage(); + assertTrue(message.contains("Multiple components are using index 0")); assertTrue(message.contains("foo")); + assertTrue(message.contains("component2")); + assertTrue(message.contains("Missing indices: {1}")); } @Test - void shouldFailIfNoTotalComponentsAreProvided() { - final var e = assertThrows(NullPointerException.class, - () -> new SensorMetadata(Collections.emptyMap(), "", null)); + void shouldFailIfTotalComponentsAreNotCommensurateToWatts() { + final var name = "foo"; + final var e = assertThrows(IllegalArgumentException.class, + () -> new SensorMetadata( + List.of(new SensorMetadata.ComponentMetadata(name, 0, "", false, SensorUnit.decimalPercentage, true)), + "")); final var message = e.getMessage(); - assertTrue(message.contains("Must provide total components")); + assertTrue(message.contains(name)); } @Test void shouldFailIfNoComponentsAreProvided() { final var e = assertThrows(NullPointerException.class, - () -> new SensorMetadata(null, "", null)); + () -> new SensorMetadata(null, "")); final var message = e.getMessage(); assertTrue(message.contains("Must provide components")); } diff --git a/server/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java b/server/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java index ab31984..ff8a224 100644 --- a/server/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java +++ b/server/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java @@ -63,20 +63,17 @@ private IntelRAPLSensor(SortedMap files) { raplFiles = files.values().toArray(new RAPLFile[0]); final var rawOffset = files.size(); - final var metadata = new HashMap(rawOffset * 2); + final var metadata = new ArrayList(rawOffset * 2); int fileNb = 0; - final int[] totalComponents = new int[rawOffset]; for (String name : files.keySet()) { - metadata.put(name, new SensorMetadata.ComponentMetadata(name, fileNb, name, false, mW)); - totalComponents[fileNb] = fileNb; + metadata.add(new SensorMetadata.ComponentMetadata(name, fileNb, name, false, mW, true)); final var rawName = name + "_uj"; - metadata.put(rawName, new SensorMetadata.ComponentMetadata(rawName, fileNb + rawOffset, - name + " (raw micro Joule data)", false, µJ)); + metadata.add(new SensorMetadata.ComponentMetadata(rawName, fileNb + rawOffset, + name + " (raw micro Joule data)", false, µJ, false)); fileNb++; } this.metadata = new SensorMetadata(metadata, - "Linux RAPL derived information, see https://www.kernel.org/doc/html/latest/power/powercap/powercap.html", - totalComponents); + "Linux RAPL derived information, see https://www.kernel.org/doc/html/latest/power/powercap/powercap.html"); lastMeasuredSensorValues = new double[raplFiles.length]; } diff --git a/server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/AppleSiliconCPU.java b/server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/AppleSiliconCPU.java index 64773f1..a1a6804 100644 --- a/server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/AppleSiliconCPU.java +++ b/server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/AppleSiliconCPU.java @@ -3,38 +3,29 @@ import static net.laprun.sustainability.power.SensorUnit.*; import static net.laprun.sustainability.power.sensors.macos.powermetrics.MacOSPowermetricsSensor.*; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Map; import net.laprun.sustainability.power.SensorMetadata; class AppleSiliconCPU extends CPU { - private static final List defaultTotalComponents = List.of(0, 1, 2); private static final SensorMetadata.ComponentMetadata cpuComponent = new SensorMetadata.ComponentMetadata(CPU, 0, - "CPU power", true, mW); + "CPU power", true, mW, true); private static final SensorMetadata.ComponentMetadata gpuComponent = new SensorMetadata.ComponentMetadata(GPU, 1, - "GPU power", true, mW); + "GPU power", true, mW, true); private static final SensorMetadata.ComponentMetadata aneComponent = new SensorMetadata.ComponentMetadata(ANE, 2, - "Apple Neural Engine power", false, mW); + "Apple Neural Engine power", false, mW, true); private static final SensorMetadata.ComponentMetadata cpuShareComponent = new SensorMetadata.ComponentMetadata(CPU_SHARE, 3, - "Computed share of CPU", false, decimalPercentage); + "Computed share of CPU", false, decimalPercentage, false); private static final String COMBINED = "Combined"; private static final String POWER_INDICATOR = " Power: "; private static final int POWER_INDICATOR_LENGTH = POWER_INDICATOR.length(); - private final List totalComponents = new ArrayList<>(); public AppleSiliconCPU() { } @Override - int[] getTotalComponents() { - return totalComponents.stream().mapToInt(i -> i).toArray(); - } - - @Override - public void addComponentIfFound(String line, Map components) { + public void addComponentIfFound(String line, List components) { // looking for line fitting the: " Power: xxx mW" pattern, where "name" will be a considered metadata component final var powerIndex = line.indexOf(" Power"); // lines with `-` as the second char are disregarded as of the form: "E-Cluster Power: 6 mW" which fits the metadata pattern but shouldn't be considered @@ -43,7 +34,7 @@ public void addComponentIfFound(String line, Map components) { + private void addComponentTo(String name, List components) { switch (name) { case CPU, GPU, ANE: // already pre-added @@ -53,8 +44,7 @@ private void addComponentTo(String name, Map components) { + boolean doneAfterComponentsInitialization(List components) { // init map with known components - components.put(MacOSPowermetricsSensor.CPU, cpuComponent); - components.put(GPU, gpuComponent); - components.put(ANE, aneComponent); - components.put(CPU_SHARE, cpuShareComponent); - totalComponents.addAll(defaultTotalComponents); + components.add(cpuComponent); + components.add(gpuComponent); + components.add(aneComponent); + components.add(cpuShareComponent); return false; } } diff --git a/server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/CPU.java b/server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/CPU.java index e1c3353..d02b674 100644 --- a/server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/CPU.java +++ b/server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/CPU.java @@ -1,14 +1,14 @@ package net.laprun.sustainability.power.sensors.macos.powermetrics; import java.util.HashMap; -import java.util.Map; +import java.util.List; import net.laprun.sustainability.power.SensorMetadata; abstract class CPU { private SensorMetadata metadata; - void addComponentIfFound(String line, Map components) { + void addComponentIfFound(String line, List components) { throw new IllegalStateException("Shouldn't be called as this processing is unneeded for this implementation"); } @@ -22,7 +22,5 @@ void setMetadata(SensorMetadata metadata) { this.metadata = metadata; } - abstract boolean doneAfterComponentsInitialization(Map components); - - abstract int[] getTotalComponents(); + abstract boolean doneAfterComponentsInitialization(List components); } diff --git a/server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/IntelCPU.java b/server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/IntelCPU.java index 6b4017d..c59f38d 100644 --- a/server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/IntelCPU.java +++ b/server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/IntelCPU.java @@ -5,21 +5,16 @@ import static net.laprun.sustainability.power.sensors.macos.powermetrics.MacOSPowermetricsSensor.PACKAGE; import java.util.HashMap; -import java.util.Map; +import java.util.List; import net.laprun.sustainability.power.SensorMetadata; class IntelCPU extends CPU { private static final SensorMetadata.ComponentMetadata packageComponent = new SensorMetadata.ComponentMetadata(PACKAGE, 0, - "Intel energy model derived package power (CPUs+GT+SA)", true, W); + "Intel energy model derived package power (CPUs+GT+SA)", true, W, true); private static final SensorMetadata.ComponentMetadata cpuShareComponent = new SensorMetadata.ComponentMetadata(CPU_SHARE, 1, - "Computed share of CPU", false, decimalPercentage); - - @Override - int[] getTotalComponents() { - return new int[] { 0 }; - } + "Computed share of CPU", false, decimalPercentage, false); @Override public boolean doneExtractingPowerComponents(String line, HashMap powerComponents) { @@ -41,9 +36,9 @@ public boolean doneExtractingPowerComponents(String line, HashMap components) { - components.put(PACKAGE, packageComponent); - components.put(CPU_SHARE, cpuShareComponent); + boolean doneAfterComponentsInitialization(List components) { + components.add(packageComponent); + components.add(cpuShareComponent); return true; } } diff --git a/server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/MacOSPowermetricsSensor.java b/server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/MacOSPowermetricsSensor.java index 191be71..3530c28 100644 --- a/server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/MacOSPowermetricsSensor.java +++ b/server/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/MacOSPowermetricsSensor.java @@ -4,9 +4,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; -import java.util.Map; import java.util.regex.Pattern; import io.quarkus.logging.Log; @@ -59,7 +59,7 @@ public MacOSPowermetricsSensor() { void initMetadata(InputStream inputStream) { try (BufferedReader input = new BufferedReader(new InputStreamReader(inputStream))) { String line; - Map components = new HashMap<>(); + var components = new ArrayList(); while ((line = input.readLine()) != null) { if (cpu == null) { // if we reached the OS line while cpu is still null, we're looking at an Apple Silicon CPU @@ -87,8 +87,7 @@ void initMetadata(InputStream inputStream) { } final var metadata = new SensorMetadata(components, - "macOS powermetrics derived information, see https://firefox-source-docs.mozilla.org/performance/powermetrics.html", - cpu.getTotalComponents()); + "macOS powermetrics derived information, see https://firefox-source-docs.mozilla.org/performance/powermetrics.html"); cpu.setMetadata(metadata); Log.info("Detected metadata:\n" + metadata); } catch (IOException e) { diff --git a/server/src/main/java/net/laprun/sustainability/power/sensors/test/TestPowerSensor.java b/server/src/main/java/net/laprun/sustainability/power/sensors/test/TestPowerSensor.java index ae811ea..8c654b5 100644 --- a/server/src/main/java/net/laprun/sustainability/power/sensors/test/TestPowerSensor.java +++ b/server/src/main/java/net/laprun/sustainability/power/sensors/test/TestPowerSensor.java @@ -2,7 +2,7 @@ import static net.laprun.sustainability.power.SensorUnit.mW; -import java.util.Map; +import java.util.List; import net.laprun.sustainability.power.SensorMeasure; import net.laprun.sustainability.power.SensorMetadata; @@ -14,8 +14,8 @@ public class TestPowerSensor extends AbstractPowerSensor { public static final String CPU = "cpu"; public static final SensorMetadata DEFAULT = new SensorMetadata( - Map.of(CPU, new SensorMetadata.ComponentMetadata(CPU, 0, "CPU", true, mW)), - "Test PowerSensor returning random values for a single 'cpu' component", new int[] { 0 }); + List.of(new SensorMetadata.ComponentMetadata(CPU, 0, "CPU", true, mW, true)), + "Test PowerSensor returning random values for a single 'cpu' component"); private final SensorMetadata metadata; private boolean started;