Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions analysis/src/test/java/ComputeTest.java
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
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;

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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String, ComponentMetadata> components,
@JsonProperty("documentation") String documentation,
@JsonProperty("totalComponents") int[] totalComponents) {
this.components = Objects.requireNonNull(components, "Must provide components");
public SensorMetadata(List<ComponentMetadata> 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<String, ComponentMetadata> 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
Expand Down Expand Up @@ -136,6 +169,27 @@ public int[] totalComponents() {
return totalComponents;
}

public static class Builder {
private final List<ComponentMetadata> 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<String> errors;

Expand Down Expand Up @@ -171,16 +225,27 @@ 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)
*
* @return {@code true} if this component's unit is commensurable to Watts, {@code false} otherwise
*/
@JsonIgnore
public boolean isWattCommensurable() {
return SensorUnit.of(unit).isWattCommensurable();
return unit != null && SensorUnit.of(unit).isWattCommensurable();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,17 @@ private IntelRAPLSensor(SortedMap<String, RAPLFile> files) {

raplFiles = files.values().toArray(new RAPLFile[0]);
final var rawOffset = files.size();
final var metadata = new HashMap<String, SensorMetadata.ComponentMetadata>(rawOffset * 2);
final var metadata = new ArrayList<SensorMetadata.ComponentMetadata>(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];
}

Expand Down
Loading
Loading