Skip to content

Commit 9503a04

Browse files
committed
feat: simplify SensorMetadata creation
Fixes #153
1 parent 52c8337 commit 9503a04

File tree

12 files changed

+170
-104
lines changed

12 files changed

+170
-104
lines changed

analysis/src/test/java/ComputeTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import static org.junit.jupiter.api.Assertions.assertEquals;
22

3-
import java.util.Map;
3+
import java.util.List;
44
import java.util.Random;
55
import java.util.random.RandomGenerator;
66

@@ -11,7 +11,7 @@
1111
import net.laprun.sustainability.power.measure.OngoingPowerMeasure;
1212

1313
public class ComputeTest {
14-
private final static SensorMetadata metadata = new SensorMetadata(Map.of(), null, new int[0]) {
14+
private final static SensorMetadata metadata = new SensorMetadata(List.of(), null) {
1515

1616
@Override
1717
public int componentCardinality() {

if-manifest-export/src/test/java/net/laprun/sustainability/power/impactframework/export/IFExporterTest.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import static org.junit.jupiter.api.Assertions.*;
44

5-
import java.util.Map;
6-
75
import org.junit.jupiter.api.Test;
86

97
import net.laprun.sustainability.power.SensorMetadata;
@@ -14,11 +12,11 @@ class IFExporterTest {
1412
public static final String COMPONENT1_NAME = "c1";
1513
public static final String COMPONENT2_NAME = "c2";
1614
public static final String COMPONENT3_NAME = "c3";
17-
private final static SensorMetadata metadata = new SensorMetadata(Map.of(
18-
COMPONENT1_NAME, new SensorMetadata.ComponentMetadata(COMPONENT1_NAME, 0, "component 1", true, "mW"),
19-
COMPONENT2_NAME, new SensorMetadata.ComponentMetadata(COMPONENT2_NAME, 1, "component 2", true, "mW"),
20-
COMPONENT3_NAME, new SensorMetadata.ComponentMetadata(COMPONENT3_NAME, 2, "always zero", false, "mW")), null,
21-
new int[] { 0, 1, 2 });
15+
private final static SensorMetadata metadata = SensorMetadata
16+
.withNewComponent(COMPONENT1_NAME, "component 1", true, "mW", true)
17+
.withNewComponent(COMPONENT2_NAME, "component 2", true, "mW", true)
18+
.withNewComponent(COMPONENT3_NAME, "component 3", true, "mW", true)
19+
.build();
2220

2321
@Test
2422
void export() {

measure/src/test/java/net/laprun/sustainability/power/measure/OngoingPowerMeasureTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
import static org.assertj.core.api.Assertions.assertThat;
44
import static org.junit.jupiter.api.Assertions.assertEquals;
55

6-
import java.util.Map;
6+
import java.util.List;
77
import java.util.stream.Stream;
88

99
import org.junit.jupiter.api.Test;
1010

1111
import net.laprun.sustainability.power.SensorMetadata;
1212

1313
public class OngoingPowerMeasureTest {
14-
private final static SensorMetadata metadata = new SensorMetadata(Map.of(), null, new int[0]) {
14+
private final static SensorMetadata metadata = new SensorMetadata(List.of(), null) {
1515

1616
@Override
1717
public int componentCardinality() {

metadata/src/main/java/net/laprun/sustainability/power/SensorMetadata.java

Lines changed: 88 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import java.util.ArrayList;
44
import java.util.Arrays;
5+
import java.util.BitSet;
56
import java.util.Collections;
67
import java.util.Comparator;
8+
import java.util.HashMap;
79
import java.util.List;
810
import java.util.Map;
911
import java.util.Objects;
@@ -30,37 +32,68 @@ public class SensorMetadata {
3032
*
3133
* @param components a map describing the metadata for each component
3234
* @param documentation a text providing any relevant information associated with the described sensor
33-
* @param totalComponents an array of indices indicating which components can be used to compute a total power consumption
34-
* metric for that sensor. Must use a unit commensurable with {@link SensorUnit#W}
3535
* @throws IllegalArgumentException if indices specified in {@code totalComponents} do not represent power measures
3636
* expressible in Watts or are not a valid index
3737
*/
38-
@JsonCreator
39-
public SensorMetadata(@JsonProperty("metadata") Map<String, ComponentMetadata> components,
40-
@JsonProperty("documentation") String documentation,
41-
@JsonProperty("totalComponents") int[] totalComponents) {
42-
this.components = Objects.requireNonNull(components, "Must provide components");
38+
public SensorMetadata(List<ComponentMetadata> components, String documentation) {
39+
Objects.requireNonNull(components, "Must provide components");
40+
final var cardinality = components.size();
41+
this.components = new HashMap<>(cardinality);
4342
this.documentation = documentation;
44-
this.totalComponents = Objects.requireNonNull(totalComponents, "Must provide total components");
4543
final var errors = new Errors();
46-
for (int index : totalComponents) {
47-
if (index < 0) {
48-
errors.addError(index + " is not a valid index");
49-
continue;
44+
final var indices = new BitSet(cardinality);
45+
components.forEach(component -> {
46+
// check that index is valid
47+
final var index = component.index;
48+
if (index < 0 || index >= cardinality) {
49+
errors.addError(index + " is not a valid index: must be between 0 and " + (cardinality - 1));
50+
} else if (indices.get(index)) {
51+
errors.addError("Multiple components are using index " + index + ": "
52+
+ components.stream().filter(cm -> index == cm.index).toList());
53+
} else {
54+
// record index as known
55+
indices.set(index);
56+
}
57+
58+
// check that component's unit is commensurable to Watts if included in total
59+
if (component.isIncludedInTotal && !component.isWattCommensurable()) {
60+
errors.addError("Component " + component.name
61+
+ " is not commensurate with a power measure. It needs to be expressible in Watts.");
5062
}
51-
components.values().stream()
52-
.filter(cm -> index == cm.index)
53-
.findFirst()
54-
.ifPresentOrElse(component -> {
55-
if (!component.isWattCommensurable()) {
56-
errors.addError("Component " + component.name
57-
+ " is not commensurate with a power measure. It needs to be expressible in Watts.");
58-
}
59-
}, () -> errors.addError(index + " is not a valid index"));
63+
64+
if (this.components.containsKey(component.name)) {
65+
errors.addError("Multiple components are named '" + component.name + "': "
66+
+ components.stream().filter(cm -> cm.name.equals(component.name)).toList());
67+
} else {
68+
this.components.put(component.name, component);
69+
}
70+
});
71+
72+
// verify that all indices are covered
73+
if (indices.cardinality() != cardinality) {
74+
indices.flip(0, cardinality);
75+
errors.addError(
76+
"Components' indices should cover the full range of 0 to " + (cardinality - 1) + ". Missing indices: "
77+
+ indices);
6078
}
79+
6180
if (errors.hasErrors()) {
6281
throw new IllegalArgumentException(errors.formatErrors());
6382
}
83+
84+
this.totalComponents = indices.stream().toArray();
85+
}
86+
87+
@JsonCreator
88+
SensorMetadata(Map<String, ComponentMetadata> components, String documentation, int[] totalComponents) {
89+
this.components = components;
90+
this.documentation = documentation;
91+
this.totalComponents = totalComponents;
92+
}
93+
94+
public static SensorMetadata.Builder withNewComponent(String name, String description, boolean isAttributed, String unit,
95+
boolean participatesInTotal) {
96+
return new SensorMetadata.Builder().withNewComponent(name, description, isAttributed, unit, participatesInTotal);
6497
}
6598

6699
@Override
@@ -136,6 +169,27 @@ public int[] totalComponents() {
136169
return totalComponents;
137170
}
138171

172+
public static class Builder {
173+
private final List<ComponentMetadata> components = new ArrayList<>();
174+
private int currentIndex = 0;
175+
private String documentation;
176+
177+
public Builder withNewComponent(String name, String description, boolean isAttributed, String unit,
178+
boolean isIncludedInTotal) {
179+
components.add(new ComponentMetadata(name, currentIndex++, description, isAttributed, unit, isIncludedInTotal));
180+
return this;
181+
}
182+
183+
public Builder withDocumentation(String documentation) {
184+
this.documentation = documentation;
185+
return this;
186+
}
187+
188+
public SensorMetadata build() {
189+
return new SensorMetadata(components, documentation);
190+
}
191+
}
192+
139193
private static class Errors {
140194
private List<String> errors;
141195

@@ -171,16 +225,27 @@ String formatErrors() {
171225
* attributed share for each process needs to be performed. This is needed because some sensors only provide
172226
* system-wide measures instead of on a per-process basis.
173227
* @param unit a textual representation of the unit used for measures associated with this component (e.g. mW)
228+
* @param isIncludedInTotal whether or not this component takes part in the computation to get a total power consumption
229+
* metric for that sensor. Components that take part of the total computation must use a unit commensurable with
230+
* {@link SensorUnit#W}
174231
*/
175-
public record ComponentMetadata(String name, int index, String description, boolean isAttributed, String unit) {
232+
public record ComponentMetadata(String name, int index, String description, boolean isAttributed, String unit,
233+
boolean isIncludedInTotal) {
234+
235+
public ComponentMetadata {
236+
if (name == null) {
237+
throw new IllegalArgumentException("Component name cannot be null");
238+
}
239+
}
240+
176241
/**
177242
* Determines whether or not this component is measuring power (i.e. its value can be converted to Watts)
178243
*
179244
* @return {@code true} if this component's unit is commensurable to Watts, {@code false} otherwise
180245
*/
181246
@JsonIgnore
182247
public boolean isWattCommensurable() {
183-
return SensorUnit.of(unit).isWattCommensurable();
248+
return unit != null && SensorUnit.of(unit).isWattCommensurable();
184249
}
185250
}
186251
}

metadata/src/main/java/net/laprun/sustainability/power/SensorUnit.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ private SensorUnit(String symbol) {
1515
}
1616

1717
public static SensorUnit of(String unit) {
18+
Objects.requireNonNull(unit);
1819
return switch (unit) {
1920
case mW -> mWUnit;
2021
case W -> WUnit;

metadata/src/test/java/net/laprun/sustainability/power/SensorMetadataTest.java

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,70 @@
22

33
import static org.junit.jupiter.api.Assertions.*;
44

5-
import java.util.Collections;
6-
import java.util.Map;
5+
import java.util.List;
76

87
import org.junit.jupiter.api.Test;
98

109
class SensorMetadataTest {
10+
1111
@Test
1212
void shouldFailIfTotalComponentsAreOutOfRange() {
13+
final var name = "comp0";
1314
final var e = assertThrows(IllegalArgumentException.class,
14-
() -> new SensorMetadata(Collections.emptyMap(), "", new int[] { 0, 1, -1 }));
15+
() -> new SensorMetadata(List.of(new SensorMetadata.ComponentMetadata(name, -1, null, true, null, true)), ""));
1516
final var message = e.getMessage();
16-
assertTrue(message.contains("0"));
17-
assertTrue(message.contains("1"));
17+
assertTrue(message.contains(name) && message.contains("Watts"));
18+
assertTrue(message.contains("range"));
1819
assertTrue(message.contains("-1"));
1920
}
2021

2122
@Test
22-
void shouldFailIfTotalComponentsAreNotCommensurateToWatts() {
23+
void shouldFailIfComponentHasNullName() {
2324
final var e = assertThrows(IllegalArgumentException.class,
24-
() -> new SensorMetadata(
25-
Map.of("foo", new SensorMetadata.ComponentMetadata("foo", 0, "", false, SensorUnit.decimalPercentage)),
26-
"", new int[] { 0, 1, -1 }));
25+
() -> new SensorMetadata(List.of(new SensorMetadata.ComponentMetadata(null, -1, null, true, null, true)), ""));
2726
final var message = e.getMessage();
28-
assertTrue(message.contains("1"));
29-
assertTrue(message.contains("-1"));
27+
assertTrue(message.contains("Component name cannot be null"));
28+
}
29+
30+
@Test
31+
void shouldFailOnDuplicatedComponentNames() {
32+
final var name = "component";
33+
final var e = assertThrows(IllegalArgumentException.class,
34+
() -> new SensorMetadata(List.of(
35+
new SensorMetadata.ComponentMetadata(name, 0, null, true, null, false),
36+
new SensorMetadata.ComponentMetadata(name, 1, null, true, null, false)), ""));
37+
final var message = e.getMessage();
38+
assertTrue(message.contains(name) && message.contains("0") && message.contains("1"));
39+
}
40+
41+
@Test
42+
void shouldFailIfComponentsDoNotCoverFullRange() {
43+
final var e = assertThrows(IllegalArgumentException.class,
44+
() -> new SensorMetadata(List.of(
45+
new SensorMetadata.ComponentMetadata("foo", 0, null, true, null, false),
46+
new SensorMetadata.ComponentMetadata("component2", 0, null, true, null, false)), ""));
47+
final var message = e.getMessage();
48+
assertTrue(message.contains("Multiple components are using index 0"));
3049
assertTrue(message.contains("foo"));
50+
assertTrue(message.contains("component2"));
51+
assertTrue(message.contains("Missing indices: {1}"));
3152
}
3253

3354
@Test
34-
void shouldFailIfNoTotalComponentsAreProvided() {
35-
final var e = assertThrows(NullPointerException.class,
36-
() -> new SensorMetadata(Collections.emptyMap(), "", null));
55+
void shouldFailIfTotalComponentsAreNotCommensurateToWatts() {
56+
final var name = "foo";
57+
final var e = assertThrows(IllegalArgumentException.class,
58+
() -> new SensorMetadata(
59+
List.of(new SensorMetadata.ComponentMetadata(name, 0, "", false, SensorUnit.decimalPercentage, true)),
60+
""));
3761
final var message = e.getMessage();
38-
assertTrue(message.contains("Must provide total components"));
62+
assertTrue(message.contains(name));
3963
}
4064

4165
@Test
4266
void shouldFailIfNoComponentsAreProvided() {
4367
final var e = assertThrows(NullPointerException.class,
44-
() -> new SensorMetadata(null, "", null));
68+
() -> new SensorMetadata(null, ""));
4569
final var message = e.getMessage();
4670
assertTrue(message.contains("Must provide components"));
4771
}

server/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,20 +63,17 @@ private IntelRAPLSensor(SortedMap<String, RAPLFile> files) {
6363

6464
raplFiles = files.values().toArray(new RAPLFile[0]);
6565
final var rawOffset = files.size();
66-
final var metadata = new HashMap<String, SensorMetadata.ComponentMetadata>(rawOffset * 2);
66+
final var metadata = new ArrayList<SensorMetadata.ComponentMetadata>(rawOffset * 2);
6767
int fileNb = 0;
68-
final int[] totalComponents = new int[rawOffset];
6968
for (String name : files.keySet()) {
70-
metadata.put(name, new SensorMetadata.ComponentMetadata(name, fileNb, name, false, mW));
71-
totalComponents[fileNb] = fileNb;
69+
metadata.add(new SensorMetadata.ComponentMetadata(name, fileNb, name, false, mW, true));
7270
final var rawName = name + "_uj";
73-
metadata.put(rawName, new SensorMetadata.ComponentMetadata(rawName, fileNb + rawOffset,
74-
name + " (raw micro Joule data)", false, µJ));
71+
metadata.add(new SensorMetadata.ComponentMetadata(rawName, fileNb + rawOffset,
72+
name + " (raw micro Joule data)", false, µJ, false));
7573
fileNb++;
7674
}
7775
this.metadata = new SensorMetadata(metadata,
78-
"Linux RAPL derived information, see https://www.kernel.org/doc/html/latest/power/powercap/powercap.html",
79-
totalComponents);
76+
"Linux RAPL derived information, see https://www.kernel.org/doc/html/latest/power/powercap/powercap.html");
8077
lastMeasuredSensorValues = new double[raplFiles.length];
8178
}
8279

0 commit comments

Comments
 (0)