Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: unit-safe metadata, better error detection #93

Merged
merged 7 commits into from
Jul 7, 2024
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
5 changes: 5 additions & 0 deletions measure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
<artifactId>power-server-metadata</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-math3</artifactId>
<version>3.6.1</version>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.laprun.sustainability.power.measure;

import java.time.Duration;
import java.util.ArrayList;

import net.laprun.sustainability.power.SensorMetadata;
Expand Down Expand Up @@ -32,7 +33,7 @@ public void recordMeasure(double[] components) {
}

// record min / max totals
final var recordedTotal = PowerMeasure.sumOfComponents(recorded);
final var recordedTotal = PowerMeasure.sumOfSelectedComponents(recorded, metadata().totalComponents());
if (recordedTotal < minTotal) {
minTotal = recordedTotal;
}
Expand All @@ -43,11 +44,11 @@ public void recordMeasure(double[] components) {

@Override
public double total() {
return PowerMeasure.sumOfComponents(totals);
return PowerMeasure.sumOfSelectedComponents(totals, metadata().totalComponents());
}

public long duration() {
return System.currentTimeMillis() - startedAt;
public Duration duration() {
return Duration.ofMillis(System.currentTimeMillis() - startedAt);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,52 @@
package net.laprun.sustainability.power.measure;

import java.util.Arrays;
import java.time.Duration;
import java.util.List;
import java.util.stream.IntStream;

import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;

import net.laprun.sustainability.power.SensorMetadata;

public interface PowerMeasure {
private static double sumOfComponents(double[] recorded) {
var componentSum = 0.0;
for (double value : recorded) {
componentSum += value;
}
return componentSum;
}

static double sumOfSelectedComponents(double[] recorded, int... indices) {
if (indices == null || indices.length == 0) {
return sumOfComponents(recorded);
}
var componentSum = 0.0;
for (int index : indices) {
componentSum += recorded[index];
}
return componentSum;
}

static String asString(PowerMeasure measure) {
final var durationInSeconds = measure.duration().getSeconds();
final var samples = measure.numberOfSamples();
final var measuredMilliWatts = measure.total();
final var stdDevs = measure.standardDeviations();
return String.format("%s / avg: %s / std dev: %.3f [min: %.3f, max: %.3f] (%ds, %s samples)",
readableWithUnit(measuredMilliWatts), readableWithUnit(measure.average()), stdDevs.aggregate,
measure.minMeasuredTotal(), measure.maxMeasuredTotal(), durationInSeconds, samples);
}

static String readableWithUnit(double milliWatts) {
String unit = milliWatts >= 1000 ? "W" : "mW";
double power = milliWatts >= 1000 ? milliWatts / 1000 : milliWatts;
return String.format("%.3f%s", power, unit);
}

int numberOfSamples();

long duration();
Duration duration();

default double average() {
return total() / numberOfSamples();
Expand All @@ -25,42 +62,31 @@ default double average() {

double maxMeasuredTotal();

static double sumOfComponents(double[] recorded) {
var componentSum = 0.0;
for (double value : recorded) {
componentSum += value;
}
return componentSum;
}

default StdDev standardDeviations() {
final var cardinality = metadata().componentCardinality();
final var stdDevs = new double[cardinality];
final var aggregate = new double[1];
final var samples = numberOfSamples() - 1; // unbiased so we remove one sample
final var sqrdAverages = Arrays.stream(averagesPerComponent()).map(m -> m * m).toArray();
final var sqrdAverage = average() * average();
// need to compute the average of variances then square root that to get the "aggregate" standard deviation,
// see: https://stats.stackexchange.com/a/26647
// "vectorize" computation of variances: compute the variance for each component in parallel
final var totalComponents = metadata().totalComponents();
final DescriptiveStatistics[] perComponent = new DescriptiveStatistics[cardinality];
for (int i = 0; i < perComponent.length; i++) {
perComponent[i] = new DescriptiveStatistics();
}
final DescriptiveStatistics total = new DescriptiveStatistics();
IntStream.range(0, cardinality).parallel()
// compute variances for each component of the measure
.forEach(component -> {
final var sumOfSquares = measures().stream().parallel().peek(m -> {
// compute the std dev for total measure
final var total = sumOfComponents(m);
aggregate[0] += total * total;
}).mapToDouble(m -> m[component] * m[component]).sum();
stdDevs[component] = stdDev(sumOfSquares, sqrdAverages[component], samples);
aggregate[0] = stdDev(aggregate[0], sqrdAverage, samples);
measures().stream().parallel().forEach(measure -> {
perComponent[component].addValue(measure[component]);
total.addValue(sumOfSelectedComponents(measure, totalComponents));
});
});
return new StdDev(aggregate[0], stdDevs);
}

private static double stdDev(double sumOfSquares, double squaredAvg, int samples) {
return Math.sqrt((sumOfSquares / samples) - (((samples + 1) * squaredAvg) / samples));
final var stdDevs = new double[cardinality];
for (int i = 0; i < perComponent.length; i++) {
stdDevs[i] = perComponent[i].getStandardDeviation();
}
return new StdDev(total.getStandardDeviation(), stdDevs);
}

List<double[]> measures();

/**
* Records the standard deviations for the aggregated energy comsumption value (as returned by {@link #total()}) and
* per component
Expand All @@ -70,22 +96,4 @@ private static double stdDev(double sumOfSquares, double squaredAvg, int samples
*/
record StdDev(double aggregate, double[] perComponent) {
}

static String asString(PowerMeasure measure) {
final var durationInSeconds = measure.duration() / 1000;
final var samples = measure.numberOfSamples();
final var measuredMilliWatts = measure.total();
final var stdDevs = measure.standardDeviations();
return String.format("%s / avg: %s / std dev: %.3f [min: %.3f, max: %.3f] (%ds, %s samples)",
readableWithUnit(measuredMilliWatts), readableWithUnit(measure.average()), stdDevs.aggregate,
measure.minMeasuredTotal(), measure.maxMeasuredTotal(), durationInSeconds, samples);
}

static String readableWithUnit(double milliWatts) {
String unit = milliWatts >= 1000 ? "W" : "mW";
double power = milliWatts >= 1000 ? milliWatts / 1000 : milliWatts;
return String.format("%.3f%s", power, unit);
}

List<double[]> measures();
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package net.laprun.sustainability.power.measure;

import java.time.Duration;

public class StoppedPowerMeasure extends AbstractPowerMeasure {
private final long duration;
private final Duration duration;
private final double total;
private final double min;
private final double max;
Expand All @@ -17,7 +19,7 @@ public StoppedPowerMeasure(PowerMeasure powerMeasure) {
}

@Override
public long duration() {
public Duration duration() {
return duration;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Map;

import org.junit.jupiter.api.Test;

import net.laprun.sustainability.power.SensorMetadata;
Expand All @@ -14,7 +16,7 @@ void testStatistics() {
final var m1c2 = 12.0;
final var m2c1 = 8.0;
final var m2c2 = 17.0;
final var metadata = new SensorMetadata(null, null) {
final var metadata = new SensorMetadata(Map.of(), null, new int[0]) {

@Override
public int componentCardinality() {
Expand All @@ -41,8 +43,8 @@ public int componentCardinality() {
assertEquals((m1c1 + m2c1) / 2, c1Avg);
assertEquals((m1c2 + m2c2) / 2, c2Avg);

final var stdVarForC1 = Math.sqrt(Math.pow(m1c1 - c1Avg, 2) + Math.pow(m2c1 - c1Avg, 2));
final var stdVarForC2 = Math.sqrt(Math.pow(m1c2 - c2Avg, 2) + Math.pow(m2c2 - c2Avg, 2));
final var stdVarForC1 = Math.sqrt((Math.pow(m1c1 - c1Avg, 2) + Math.pow(m2c1 - c1Avg, 2)) / (2 - 1));
final var stdVarForC2 = Math.sqrt((Math.pow(m1c2 - c2Avg, 2) + Math.pow(m2c2 - c2Avg, 2)) / (2 - 1));

assertEquals(stdVarForC1, measure.standardDeviations().perComponent()[0], 0.0001,
"Standard Deviation did not match the expected value");
Expand Down
8 changes: 8 additions & 0 deletions metadata/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
<name>power-server : metadata model</name>
<description>Metadata model used by the power-server project, extracted to allow reuse in client projects</description>

<dependencies>
<dependency>
<groupId>eu.hoefel</groupId>
<artifactId>units</artifactId>
<version>4.1.1</version>
</dependency>
</dependencies>

<build>
<pluginManagement>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,51 +1,66 @@
package net.laprun.sustainability.power;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
* The metadata associated with a power-consumption recording sensor. This allows to make sense of the data sent by the power
* server by providing information about each component (e.g. CPU) recorded by the sensor during each periodical measure.
*/
public class SensorMetadata {
/**
* The information associated with a recorded component
*
* @param name the name of the component (e.g. CPU)
* @param index the index at which the measure for this component is recorded in the {@link SensorMeasure#components} array
* @param description a short textual description of what this component is about when available (for automatically
* extracted components, this might be identical to the name)
* @param isAttributed whether or not this component provides an attributed value i.e. whether the value is already computed
* for the process during a measure or, to the contrary, if the measure is done globally and the computation of the
* 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)
*/
public record ComponentMetadata(String name, int index, String description, boolean isAttributed, String unit) {
}
@JsonProperty("metadata")
private final Map<String, ComponentMetadata> components;
@JsonProperty("documentation")
private final String documentation;
@JsonProperty("totalComponents")
private final int[] totalComponents;

/**
* Initializes sensor metadata information
*
* @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) {
this.components = components;
@JsonProperty("documentation") String documentation,
@JsonProperty("totalComponents") int[] totalComponents) {
this.components = Objects.requireNonNull(components, "Must provide components");
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;
}
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 (errors.hasErrors()) {
throw new IllegalArgumentException(errors.formatErrors());
}
}

@JsonProperty("metadata")
private final Map<String, ComponentMetadata> components;

@JsonProperty("documentation")
private final String documentation;

/**
* Determines whether a component with the specified name is known for this sensor
*
Expand Down Expand Up @@ -98,4 +113,61 @@ public Map<String, ComponentMetadata> components() {
public String documentation() {
return documentation;
}

/**
* Retrieves the indices of the components that can be used to compute a total
*
* @return the indices of the components that can be used to compute a total
*/
public int[] totalComponents() {
return totalComponents;
}

private static class Errors {
private List<String> errors;

void addError(String error) {
if (errors == null) {
errors = new ArrayList<>();
}
errors.add(error);
}

boolean hasErrors() {
return errors != null && !errors.isEmpty();
}

String formatErrors() {
if (errors == null) {
return "";
}
return errors.stream().collect(Collectors.joining("\n- ", "\n- ", ""));
}
}

/**
* The information associated with a recorded component
*
* @param name the name of the component (e.g. CPU)
* @param index the index at which the measure for this component is recorded in the {@link SensorMeasure#components()}
* array
* @param description a short textual description of what this component is about when available (for automatically
* extracted components, this might be identical to the name)
* @param isAttributed whether or not this component provides an attributed value i.e. whether the value is already computed
* for the process during a measure or, on the contrary, if the measure is done globally and the computation of the
* 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)
*/
public record ComponentMetadata(String name, int index, String description, boolean isAttributed, String unit) {
/**
* 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();
}
}
}
Loading