callsPerMethod = new ConcurrentHashMap<>();
16 | callsPerMethod.computeIfAbsent("test", callCount -> new LongAdder()).increment();
17 | assertThat(callsPerMethod.get("test").intValue()).isEqualTo(1);
18 | callsPerMethod.computeIfAbsent("test", callCount -> new LongAdder()).increment();
19 | assertThat(callsPerMethod.get("test").intValue()).isEqualTo(2);
20 | callsPerMethod.computeIfAbsent("test2", callCount -> new LongAdder()).increment();
21 | assertThat(callsPerMethod.get("test2").intValue()).isEqualTo(1);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/group/msg/jpowermonitor/MeasureMethodProvider.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor;
2 |
3 | import group.msg.jpowermonitor.config.dto.JPowerMonitorCfg;
4 | import group.msg.jpowermonitor.measurement.csv.CommaSeparatedValuesReader;
5 | import group.msg.jpowermonitor.measurement.est.EstimationReader;
6 | import group.msg.jpowermonitor.measurement.lhm.LibreHardwareMonitorReader;
7 |
8 | /**
9 | * Factory for creating the MeasureMethod from the config.
10 | *
11 | * @see MeasureMethod
12 | */
13 | public class MeasureMethodProvider {
14 | public static MeasureMethod resolveMeasureMethod(JPowerMonitorCfg config) {
15 | if ("csv".equals(config.getMeasurement().getMethod())) {
16 | return new CommaSeparatedValuesReader(config);
17 | } else if ("lhm".equals(config.getMeasurement().getMethod())) {
18 | return new LibreHardwareMonitorReader(config);
19 | } else if ("est".equals(config.getMeasurement().getMethod())) {
20 | return new EstimationReader(config);
21 | } else {
22 | throw new JPowerMonitorException("Unknown measure method " + config.getMeasurement().getMethod());
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/group/msg/jpowermonitor/dto/DataPoint.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.dto;
2 |
3 | import group.msg.jpowermonitor.agent.Unit;
4 | import group.msg.jpowermonitor.config.dto.JPowerMonitorCfg;
5 | import group.msg.jpowermonitor.util.Converter;
6 | import lombok.Value;
7 |
8 | import java.time.LocalDateTime;
9 |
10 | /**
11 | * One data point. In case of energy data point, contains the co2 representation of the value, too.
12 | */
13 | @Value
14 | public class DataPoint implements PowerQuestionable {
15 | String name;
16 | Double value;
17 | Unit unit;
18 | LocalDateTime time;
19 | String threadName;
20 |
21 | /**
22 | * The CO2 value in grams, only != null, if the Unit is WATT (energy value).
23 | */
24 | Double co2Value;
25 |
26 | public DataPoint(String name, Double value, Unit unit, LocalDateTime time, String threadName) {
27 | this.name = name;
28 | this.value = value;
29 | this.unit = unit;
30 | this.time = time;
31 | this.threadName = threadName;
32 | if (Unit.JOULE.equals(unit)) {
33 | co2Value = Converter.convertJouleToCarbonDioxideGrams(value, JPowerMonitorCfg.getCo2EmissionFactor());
34 | } else {
35 | co2Value = null;
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/test/java/group/msg/jpowermonitor/CfgProviderForTests.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor;
2 |
3 | import group.msg.jpowermonitor.config.JPowerMonitorCfgProvider;
4 | import group.msg.jpowermonitor.config.dto.JPowerMonitorCfg;
5 | import org.yaml.snakeyaml.Yaml;
6 |
7 | import java.io.InputStream;
8 |
9 | public class CfgProviderForTests implements JPowerMonitorCfgProvider {
10 |
11 | public JPowerMonitorCfg readConfig(Class> testClass) throws JPowerMonitorException {
12 | return readConfig(testClass.getSimpleName() + ".yaml");
13 | }
14 |
15 | @Override
16 | public JPowerMonitorCfg getCachedConfig() throws JPowerMonitorException {
17 | throw new JPowerMonitorException("Cache not implemented for " + CfgProviderForTests.class);
18 | }
19 |
20 | @Override
21 | public JPowerMonitorCfg readConfig(String source) throws JPowerMonitorException {
22 | ClassLoader cl = CfgProviderForTests.class.getClassLoader();
23 | try (InputStream input = cl.getResourceAsStream(source)) {
24 | return new Yaml().loadAs(input, JPowerMonitorCfg.class);
25 | } catch (Exception exc) {
26 | throw new JPowerMonitorException(String.format("Cannot load config for tests from '%s'", source), exc);
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | name: Java CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | permissions:
10 | checks: write
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
17 | with:
18 | disable-sudo: true
19 | egress-policy: block
20 | allowed-endpoints: >
21 | api.adoptopenjdk.net:443
22 | downloads.gradle-dn.com:443
23 | github-cloud.githubusercontent.com:443
24 | github.com:443
25 | jcenter.bintray.com:443
26 | objects.githubusercontent.com:443
27 | plugins.gradle.org:443
28 | repo.maven.apache.org:443
29 | services.gradle.org:443
30 | plugins-artifacts.gradle.org:443
31 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
32 | with:
33 | lfs: true
34 | - uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
35 | with:
36 | distribution: 'adopt'
37 | java-version: '17'
38 | cache: 'gradle'
39 | - uses: gradle/actions/wrapper-validation@v5
40 | - run: ./gradlew --no-daemon check
41 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | # This is the Gradle build system for JVM applications
2 | # https://gradle.org/
3 | # https://github.com/gradle/gradle
4 | image: docker:20-git
5 |
6 | # Disable the Gradle daemon for Continuous Integration servers as correctness
7 | # is usually a priority over speed in CI environments. Using a fresh
8 | # runtime for each build is more reliable since the runtime is completely
9 | # isolated from any previous builds.
10 | variables:
11 | GRADLE_OPTS: "-Dorg.gradle.daemon=false"
12 |
13 | services:
14 | - docker:dind
15 |
16 | before_script:
17 | - export GRADLE_USER_HOME=`pwd`/.gradle
18 | - apk --no-cache add openjdk11 --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community
19 | - which java
20 | - export JAVA_HOME=/usr/lib/jvm/java-11-openjdk
21 | - java -version
22 | - apk update && apk add bash
23 |
24 | build:
25 | stage: build
26 | tags:
27 | - coc-gitlab-runner
28 | script:
29 | - bash ./gradlew assemble
30 | cache:
31 | key: "$CI_COMMIT_REF_NAME"
32 | policy: push
33 | paths:
34 | - build
35 | - .gradle
36 |
37 | test:
38 | stage: test
39 | tags:
40 | - coc-gitlab-runner
41 | script:
42 | - bash ./gradlew check
43 | cache:
44 | key: "$CI_COMMIT_REF_NAME"
45 | policy: pull
46 | paths:
47 | - build
48 | - .gradle
49 |
--------------------------------------------------------------------------------
/src/test/resources/CommaSeparatedValuesReaderTest.yaml:
--------------------------------------------------------------------------------
1 | initCycles: 1
2 | samplingIntervalForInitInMs: 1
3 | calmDownIntervalInMs: 1
4 | percentageOfSamplesAtBeginningToDiscard: 1
5 | samplingIntervalInMs: 1
6 | measurement:
7 | # Specify which measurement method to use. Possible values: lhm, csv
8 | method: 'lhm'
9 | # Configuration for reading from csv file. E.g. output from HWInfo
10 | csv:
11 | # Path to csv file to read measure values from
12 | inputFile: 'hwinfo-test.csv'
13 | # Which line in the csv input file contains the current measured values? The first or the last? This depends on the measurement tool. Possible value: first, last
14 | lineToRead: 'first'
15 | # Columns to read, index starts at 0.
16 | columns:
17 | - { index: 2, name: 'CPU Power', energyInIdleMode: 1.01 }
18 | encoding: 'UTF-8'
19 | delimiter: ','
20 | # Configuration for reading from Libre Hardware Monitor
21 | lhm:
22 | url: 'some.test.url'
23 | paths:
24 | - { path: [ 'pc', 'cpu', 'path1', 'path2' ], energyInIdleMode: }
25 | csvRecording:
26 | resultCsv: 'test_energyconsumption.csv'
27 | measurementCsv: 'test_measurement.csv'
28 | javaAgent:
29 | packageFilter: [ 'com.something', 'com.anything' ]
30 | measurementIntervalInMs: 1
31 | gatherStatisticsIntervalInMs: 1
32 | writeEnergyMeasurementsToCsvIntervalInS: 1
33 |
--------------------------------------------------------------------------------
/src/main/java/group/msg/jpowermonitor/agent/Unit.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.agent;
2 |
3 | import lombok.Getter;
4 |
5 | /**
6 | * Units used in jPowerMonitor (sensor values and outputs).
7 | *
8 | * @see group.msg.jpowermonitor.dto.Quantity
9 | * @see group.msg.jpowermonitor.dto.DataPoint
10 | * @see group.msg.jpowermonitor.dto.PowerQuestionable
11 | */
12 | @Getter
13 | public enum Unit {
14 | JOULE("J"), WATT("W"), WATTHOURS("Wh"), KILOWATTHOURS("kWh"), GRAMS_CO2("gCO2"), NONE("");
15 | private final String abbreviation;
16 |
17 | Unit(String abbreviation) {
18 | this.abbreviation = abbreviation;
19 | }
20 |
21 | public String toString() {
22 | return abbreviation;
23 | }
24 |
25 | public static Unit fromAbbreviation(String abbreviation) {
26 | if (abbreviation == null) {
27 | return Unit.NONE;
28 | }
29 | switch (abbreviation) {
30 | case "J":
31 | return Unit.JOULE;
32 | case "W":
33 | return Unit.WATT;
34 | case "Wh":
35 | return Unit.WATTHOURS;
36 | case "kWh":
37 | return Unit.KILOWATTHOURS;
38 | case "gCO2":
39 | return Unit.GRAMS_CO2;
40 | default:
41 | return Unit.NONE;
42 | }
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/gradle/jar.gradle:
--------------------------------------------------------------------------------
1 | import java.text.SimpleDateFormat
2 |
3 | def gitHash = {
4 | Process procHash = 'git rev-parse HEAD'.execute()
5 | Process procDirty = 'git status --short'.execute()
6 | procDirty.waitFor()
7 | procHash.waitFor()
8 | return procHash.text.trim() + (procDirty.text.isEmpty() ? "" : " (dirty)")
9 | }
10 |
11 | jar {
12 | enabled = true
13 | manifest {
14 | attributes(
15 | 'Implementation-Title': rootProject.name,
16 | 'Implementation-Version': project.version,
17 | 'Build-Jdk': "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']})",
18 | 'Build-Timestamp': new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(new Date()),
19 | 'Build-Revision': gitHash(),
20 | 'Compatibility': java {sourceCompatibility},
21 | 'Built-By': System.getProperty('user.name'),
22 | 'Premain-Class': getProperty('premain.classname'),
23 | 'Can-Redefine-Classes': false,
24 | 'Can-Set-Native-Method-Prefix': false
25 | )
26 | }
27 | exclude("group/msg/jpowermonitor/demo") // do not include Demo class into Ja.
28 | exclude("*.yaml")
29 | exclude("simplelogger.properties")
30 | exclude("*.json")
31 | }
32 |
33 | java {
34 | withJavadocJar()
35 | withSourcesJar()
36 | }
37 |
--------------------------------------------------------------------------------
/src/test/java/group/msg/jpowermonitor/junit/JPowerMonitorExtensionTest.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.junit;
2 |
3 | import org.junit.jupiter.params.ParameterizedTest;
4 | import org.junit.jupiter.params.provider.CsvSource;
5 |
6 | import static org.assertj.core.api.Assertions.assertThat;
7 |
8 | class JPowerMonitorExtensionTest {
9 |
10 | @ParameterizedTest
11 | @CsvSource({
12 | "0,0,0",
13 | "100,10,10",
14 | "100,100,100",
15 | "100,-1,0"
16 | })
17 | void firstXPercent(int listSize, double percentage, int expectedResult) {
18 | assertThat(new JPowerMonitorExtension().firstXPercent(listSize, percentage)).isEqualTo(expectedResult);
19 | }
20 |
21 | @ParameterizedTest
22 | @CsvSource({
23 | "0.9988, 1.00",
24 | "2.355, 2.36",
25 | "2.354, 2.35",
26 | })
27 | void roundScale2(double toRound, double rounded) {
28 | assertThat(new JPowerMonitorExtension().roundScale2(toRound)).isEqualTo(rounded);
29 | }
30 |
31 | @ParameterizedTest
32 | @CsvSource({
33 | "0.9233988, 0.9234",
34 | "2.323355, 2.32335", // 2.323355 * 100000 = 232335.49999999997 we accept this minor inaccuracy for the sake of performance when rounding!
35 | "2.323354, 2.32335",
36 | })
37 | void roundScale5(double toRound, double rounded) {
38 | assertThat(new JPowerMonitorExtension().roundScale5(toRound)).isEqualTo(rounded);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/test/resources/MyTest.yaml:
--------------------------------------------------------------------------------
1 | initCycles: 0
2 | samplingIntervalForInitInMs: 1
3 | calmDownIntervalInMs: 1
4 | percentageOfSamplesAtBeginningToDiscard: 1
5 | samplingIntervalInMs: 1
6 | measurement:
7 | # Specify which measurement method to use. Possible values: lhm, csv
8 | method: 'csv'
9 | # Configuration for reading from csv file. E.g. output from HWInfo
10 | csv:
11 | # Path to csv file to read measure values from
12 | inputFile: 'hwinfo-test.csv'
13 | # Which line in the csv input file contains the current measured values? The first or the last? This depends on the measurement tool. Possible value: first, last
14 | lineToRead: 'last'
15 | # Columns to read, index starts at 0.
16 | columns:
17 | - { index: 2, name: 'CPU Power', energyInIdleMode: 2.0 }
18 | # Encoding to use for reading the csv input file
19 | encoding: 'UTF-8'
20 | # Delimiter to use for separating the columns in the csv input file
21 | delimiter: ','
22 | # Configuration for reading from Libre Hardware Monitor
23 | lhm:
24 | url: 'some.test.url'
25 | paths:
26 | - { path: [ 'pc', 'cpu', 'path1', 'path2' ], energyInIdleMode: }
27 | csvRecording:
28 | resultCsv: 'test_energyconsumption.csv'
29 | measurementCsv: 'test_measurement.csv'
30 | javaAgent:
31 | packageFilter: [ 'com.something', 'com.anything' ]
32 | measurementIntervalInMs: 1
33 | gatherStatisticsIntervalInMs: 1
34 | writeEnergyMeasurementsToCsvIntervalInS: 1
35 |
--------------------------------------------------------------------------------
/src/test/resources/EndlessLoopTest.yaml:
--------------------------------------------------------------------------------
1 | initCycles: 0
2 | samplingIntervalForInitInMs: 1
3 | calmDownIntervalInMs: 1
4 | percentageOfSamplesAtBeginningToDiscard: 1
5 | samplingIntervalInMs: 1
6 | measurement:
7 | # Specify which measurement method to use. Possible values: lhm, csv
8 | method: 'csv'
9 | # Configuration for reading from csv file. E.g. output from HWInfo
10 | csv:
11 | # Path to csv file to read measure values from
12 | inputFile: 'hwinfo-test.csv'
13 | # Which line in the csv input file contains the current measured values? The first or the last? This depends on the measurement tool. Possible value: first, last
14 | lineToRead: 'last'
15 | # Columns to read, index starts at 0.
16 | columns:
17 | - { index: 2, name: 'CPU Power', energyInIdleMode: 2.01 }
18 | # Encoding to use for reading the csv input file
19 | encoding: 'UTF-8'
20 | # Delimiter to use for separating the columns in the csv input file
21 | delimiter: ','
22 | # Configuration for reading from Libre Hardware Monitor
23 | lhm:
24 | url: 'some.test.url'
25 | paths:
26 | - { path: [ 'pc', 'cpu', 'path1', 'path2' ], energyInIdleMode: }
27 | csvRecording:
28 | resultCsv: 'test_energyconsumption.csv'
29 | measurementCsv: 'test_measurement.csv'
30 | javaAgent:
31 | packageFilter: [ 'com.something', 'com.anything' ]
32 | measurementIntervalInMs: 1
33 | gatherStatisticsIntervalInMs: 1
34 | writeEnergyMeasurementsToCsvIntervalInS: 1
35 |
--------------------------------------------------------------------------------
/src/test/resources/DefaultConfigProviderTest.yaml:
--------------------------------------------------------------------------------
1 | initCycles: 7
2 | samplingIntervalForInitInMs: 8
3 | calmDownIntervalInMs: 9
4 | percentageOfSamplesAtBeginningToDiscard: 3
5 | samplingIntervalInMs: 4
6 | carbonDioxideEmissionFactor: 777
7 | measurement:
8 | # Specify which measurement method to use. Possible values: lhm, csv
9 | method: 'lhm'
10 | # Configuration for reading from csv file. E.g. output from HWInfo
11 | csv:
12 | # Path to csv file to read measure values from
13 | inputFile: 'mycsv.csv'
14 | # Which line in the csv input file contains the current measured values? The first or the last? This depends on the measurement tool. Possible value: first, last
15 | lineToRead: 'first'
16 | # Columns to read, index starts at 0.
17 | columns:
18 | - { index: 42, name: 'CPU Power' }
19 | # Encoding to use for reading the csv input file
20 | encoding: 'UTF-16'
21 | # Delimiter to use for separating the columns in the csv input file
22 | delimiter: ';'
23 | # Configuration for reading from Libre Hardware Monitor
24 | lhm:
25 | url: 'some.test.url'
26 | paths:
27 | - { path: [ 'pc', 'cpu', 'path1', 'path2' ], energyInIdleMode: }
28 | csvRecording:
29 | resultCsv: 'test_energyconsumption.csv'
30 | measurementCsv: 'test_measurement.csv'
31 | javaAgent:
32 | packageFilter: [ 'com.something', 'com.anything' ]
33 | measurementIntervalInMs: 2
34 | gatherStatisticsIntervalInMs: 3
35 | writeEnergyMeasurementsToCsvIntervalInS: 4
36 |
--------------------------------------------------------------------------------
/src/main/java/group/msg/jpowermonitor/config/JPowerMonitorCfgProvider.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.config;
2 |
3 | import group.msg.jpowermonitor.JPowerMonitorException;
4 | import group.msg.jpowermonitor.config.dto.JPowerMonitorCfg;
5 |
6 | /**
7 | * Interface for reading JPowerMonitor configuration.
8 | *
9 | * Any specific reading is part of the implementation, this might include finding the source as well
10 | * as caching.
11 | */
12 | public interface JPowerMonitorCfgProvider {
13 |
14 | JPowerMonitorCfg getCachedConfig() throws JPowerMonitorException;
15 |
16 | /**
17 | * Reads a JPowerMonitor configuration using the given source.
18 | *
19 | * @param source Source to read from, it strongly depends on the implementation what #source
20 | * should be, can be a resource, a file or whatever
21 | * @return a valid configuration
22 | * @throws JPowerMonitorException On any error that occurs during reading the configuration
23 | */
24 | JPowerMonitorCfg readConfig(String source) throws JPowerMonitorException;
25 |
26 | /**
27 | * Checks wether the given source name is a valid configuration source.
28 | *
29 | * The default implementations assumes any non-empty and non-null string as a valid source.
30 | * Other implementations can overwrite this to implement different logic.
31 | *
32 | * @param source Source name that has to be checked
33 | * @return true if the given source is a valid source name
34 | */
35 | default boolean isValidSource(String source) {
36 | return source != null && !source.isEmpty();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/test/resources/EstimationReaderTest.yaml:
--------------------------------------------------------------------------------
1 | initCycles: 1
2 | samplingIntervalForInitInMs: 1
3 | calmDownIntervalInMs: 1
4 | percentageOfSamplesAtBeginningToDiscard: 1
5 | samplingIntervalInMs: 1
6 | measurement:
7 | # Specify which measurement method to use. Possible values: lhm, csv
8 | method: 'est'
9 | # Configuration for reading from csv file. E.g. output from HWInfo
10 | csv:
11 | # Path to csv file to read measure values from
12 | inputFile: 'hwinfo-test.csv'
13 | # Which line in the csv input file contains the current measured values? The first or the last? This depends on the measurement tool. Possible value: first, last
14 | lineToRead: 'first'
15 | # Columns to read, index starts at 0.
16 | columns:
17 | - { index: 2, name: 'CPU Power', energyInIdleMode: 1.01 }
18 | encoding: 'UTF-8'
19 | delimiter: ','
20 | est:
21 | # Compare https://www.cloudcarbonfootprint.org/docs/methodology/#energy-estimate-watt-hours
22 | # Defaults are the average values from AWS: 0.74 - 3.5
23 | # Find the values for your VM here: https://github.com/cloud-carbon-footprint/cloud-carbon-coefficients/tree/main/data
24 | # or here: https://github.com/re-cinq/emissions-data/tree/main/data/v2
25 | # Determine AWS instance type in terminal: ´curl http://169.254.169.254/latest/meta-data/instance-type´
26 | cpuMinWatts: 8
27 | cpuMaxWatts: 40
28 | csvRecording:
29 | resultCsv: 'test_energyconsumption.csv'
30 | measurementCsv: 'test_measurement.csv'
31 | javaAgent:
32 | packageFilter: [ 'com.something', 'com.anything' ]
33 | measurementIntervalInMs: 1
34 | gatherStatisticsIntervalInMs: 1
35 | writeEnergyMeasurementsToCsvIntervalInS: 1
36 |
--------------------------------------------------------------------------------
/src/test/resources/ReplaceInStringTest.yaml:
--------------------------------------------------------------------------------
1 | initCycles: 1
2 | samplingIntervalForInitInMs: 1
3 | calmDownIntervalInMs: 1
4 | percentageOfSamplesAtBeginningToDiscard: 1
5 | samplingIntervalInMs: 1
6 | measurement:
7 | # Specify which measurement method to use. Possible values: lhm, csv
8 | method: 'est'
9 | # Configuration for reading from csv file. E.g. output from HWInfo
10 | csv:
11 | # Path to csv file to read measure values from
12 | inputFile: 'hwinfo-test.csv'
13 | # Which line in the csv input file contains the current measured values? The first or the last? This depends on the measurement tool. Possible value: first, last
14 | lineToRead: 'first'
15 | # Columns to read, index starts at 0.
16 | columns:
17 | - { index: 2, name: 'CPU Power', energyInIdleMode: 1.01 }
18 | encoding: 'UTF-8'
19 | delimiter: ','
20 | est:
21 | # Compare https://www.cloudcarbonfootprint.org/docs/methodology/#energy-estimate-watt-hours
22 | # Defaults are the average values from AWS: 0.74 - 3.5
23 | # Find the values for your VM here: https://github.com/cloud-carbon-footprint/cloud-carbon-coefficients/tree/main/data
24 | # or here: https://github.com/re-cinq/emissions-data/tree/main/data/v2
25 | # Determine AWS instance type in terminal: ´curl http://169.254.169.254/latest/meta-data/instance-type´
26 | cpuMinWatts: 8
27 | cpuMaxWatts: 40
28 | csvRecording:
29 | resultCsv: 'test_energyconsumption.csv'
30 | measurementCsv: 'test_measurement.csv'
31 | javaAgent:
32 | packageFilter: [ 'com.something', 'com.anything' ]
33 | measurementIntervalInMs: 1
34 | gatherStatisticsIntervalInMs: 1
35 | writeEnergyMeasurementsToCsvIntervalInS: 1
36 |
--------------------------------------------------------------------------------
/src/main/java/group/msg/jpowermonitor/dto/Activity.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.dto;
2 |
3 | import java.time.LocalDateTime;
4 |
5 | /**
6 | * Represents an act of work as part of a measured time frame.
7 | * As an example, we take a time frame of 1,000ms. We look every 10ms
8 | * what kind of work our application does, e.g. which thread called which method.
9 | * In this case, an {@link Activity} represents (10ms / 1,000ms) = 1/100 part of
10 | * the work done, and thus is entitled to the same percentage of a quantity based
11 | * on the time frame. Assuming it took the CPU package 31W to execute our example
12 | * (constantly over 1,000ms), an {@link Activity} would represent (1/100 * 31W) = 0.31W
13 | * power over the same period and should be treated as such.
14 | */
15 | public interface Activity {
16 | /**
17 | * @return name of the thread the activity was part of when measured
18 | */
19 | String getThreadName();
20 |
21 | /**
22 | * @return {@link LocalDateTime} when the activity was measured
23 | */
24 | LocalDateTime getTime();
25 |
26 | /**
27 | * @param asFiltered if the filter configured in javaAgent.packageFilter should be applied, see jpowermonitor.yaml
28 | * @return identifier of the kind of work measured, e.g. a method name
29 | */
30 | String getIdentifier(boolean asFiltered);
31 |
32 | /**
33 | * @return the {@link Quantity} represented by this activity
34 | */
35 | Quantity getRepresentedQuantity();
36 |
37 | /**
38 | * @return if the activity can be used for further processing, e.g. if a {@link Quantity}
39 | * has been attributed already
40 | */
41 | boolean isFinalized();
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/java/group/msg/jpowermonitor/MeasureMethod.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor;
2 |
3 | import group.msg.jpowermonitor.dto.DataPoint;
4 | import org.jetbrains.annotations.NotNull;
5 |
6 | import java.util.List;
7 | import java.util.Map;
8 |
9 | /**
10 | * Interface for different types of measuring the consumed energy.
11 | * E.g. Libre Hardware Monitor or HWiNFO.
12 | */
13 | public interface MeasureMethod {
14 | /**
15 | * Measure all data points for the configured paths.
16 | *
17 | * @return all data point for the configured paths.
18 | * @throws JPowerMonitorException if measurement tool is not available.
19 | */
20 | @NotNull
21 | default List measure() throws JPowerMonitorException {
22 | return List.of(measureFirstConfiguredPath());
23 | }
24 |
25 | /**
26 | * Measure only the first configured path.
27 | *
28 | * @return the first data point in the configured paths.
29 | * @throws JPowerMonitorException if measurement tool is not available.
30 | */
31 | @NotNull
32 | DataPoint measureFirstConfiguredPath() throws JPowerMonitorException;
33 |
34 | /**
35 | * @return list of configured sensor paths.
36 | */
37 | @NotNull
38 | List configuredSensors();
39 |
40 | /**
41 | * The map of configured sensor paths with their configured default energy.
42 | * This is useful for a starting point to determine the energy in idle mode which may be
43 | * configured or measured in a separate method.
44 | *
45 | * @return Map of paths with default energy in idle mode (from config).
46 | */
47 | @NotNull
48 | Map defaultEnergyInIdleModeForMeasuredSensors();
49 | }
50 |
--------------------------------------------------------------------------------
/algorithm.txt:
--------------------------------------------------------------------------------
1 | measurement cycles:
2 | - STATISTICS_INTERVAL (ms)
3 | - MEASUREMENT_INTERVAL (ms)
4 |
5 | STATISTICS_INTERVAL
6 | - Summary: gather (method) activity
7 | - for all running threads, from top / newest of stacktrace
8 | - first element rated as computation time of 1 STATISTICS_INTERVAL / MEASUREMENT_INTERVAL
9 | - for prefixed report:
10 | - first element which matches at least one prefix rated as computation time of 1 STATISTICS_INTERVAL / MEASUREMENT_INTERVAL
11 | - increment METHOD_ACTIVITY accordingly
12 |
13 | MEASUREMENT_INTERVAL
14 | - summary
15 | - gather power consumption
16 | - aggregate method activity and power consumption to energy consumption per method
17 | - POWER_TOTAL (W) - get current CPU power per MEASUREMENT_INTERVAL
18 | - call e.g. Libre Hardware Monitor for current CPU power usage
19 | - assume power was the same for the entirety of MEASUREMENT_INTERVAL
20 | - get CPU times
21 | - THREAD_TIME (ns) - total CPU time of each running threads -> should time since last measurement?
22 | - APPLICATION_TIME (ns) - total CPU time of all running threads -> should be sum of should time since last measurement?
23 | - computation -> as separate task?
24 | - allocate power usage to threads per MEASUREMENT_INTERVAL
25 | - POWER_THREAD (W) = POWER_TOTAL * (THREAD_TIME / APPLICATION_TIME)
26 | - determine power usage of each method per MEASUREMENT_INTERVAL
27 | - POWER_METHOD (W) = POWER_THREAD * (METHOD_ACTIVITY * STATISTICS_INTERVAL / MEASUREMENT_INTERVAL) -> currently fix ration of 1 / 100
28 | - determine energy usage of each method in MEASUREMENT_INTERVAL (current assumption: MEASUREMENT_INTERVAL always 1000ms, so W = J)
29 | - ENERGY_METHOD (J) = POWER_METHOD * MEASUREMENT_INTERVAL / 1000
30 |
--------------------------------------------------------------------------------
/gradle/publish.gradle:
--------------------------------------------------------------------------------
1 | publishing {
2 | repositories {
3 | maven {
4 | credentials {
5 | username = project.hasProperty("ossrhToken") ? project.property('ossrhToken') : null
6 | password = project.hasProperty("ossrhTokenPassword") ? project.property('ossrhTokenPassword') : null
7 | }
8 | url = project.version.endsWith('-SNAPSHOT') ?
9 | 'https://s01.oss.sonatype.org/content/repositories/snapshots' :
10 | 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2'
11 | }
12 | }
13 | }
14 |
15 |
16 | publishing {
17 | publications {
18 | mavenJava(MavenPublication) {
19 | groupId = project.group
20 | artifactId = project.name
21 | version = project.version
22 | from components.java
23 |
24 | pom {
25 | name = "${rootProject.name}"
26 | description = project.description
27 | url = 'https://github.com/msg-systems/jpowermonitor'
28 |
29 | scm {
30 | connection = 'scm:git:https://github.com/msg-systems/jpowermonitor.git'
31 | developerConnection = 'scm:git:git@github.com/msg-systems/jpowermonitor.git'
32 | url = 'https://github.com/msg-systems/jpowermonitor.git'
33 | }
34 |
35 | licenses {
36 | license {
37 | name = 'Apache License, Version 2.0 (Apache-2.0)'
38 | url = 'https://opensource.org/license/apache-2-0/'
39 | distribution = 'repo'
40 | }
41 | }
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/test/java/com/msg/myapplication/EndlessLoopTest.java:
--------------------------------------------------------------------------------
1 | package com.msg.myapplication;
2 |
3 | import group.msg.jpowermonitor.dto.SensorValue;
4 | import group.msg.jpowermonitor.dto.SensorValues;
5 | import group.msg.jpowermonitor.junit.JPowerMonitorExtension;
6 | import group.msg.jpowermonitor.demo.StressCpuExample;
7 | import org.assertj.core.data.Offset;
8 | import org.junit.jupiter.api.AfterEach;
9 | import org.junit.jupiter.api.Test;
10 | import org.junit.jupiter.api.extension.ExtendWith;
11 |
12 | import java.util.List;
13 |
14 | import static group.msg.jpowermonitor.agent.Unit.WATT;
15 | import static org.assertj.core.api.Assertions.assertThat;
16 |
17 | @ExtendWith({JPowerMonitorExtension.class})
18 | public class EndlessLoopTest {
19 | @SensorValues
20 | private List valueList;
21 |
22 | @AfterEach
23 | void printValues() {
24 | assertThat(valueList).isNotNull();
25 | //
26 | // as we use a fix csv file, the outcome is fix for this test:
27 | valueList.forEach(x -> {
28 | assertThat(x.getValue()).isCloseTo(5.05, Offset.offset(0.01));
29 | assertThat(x.getPowerInIdleMode()).isEqualTo(2.01);
30 | assertThat(x.getName()).isEqualTo("CPU Power");
31 | assertThat(x.getUnit()).isEqualTo(WATT);
32 | });
33 | }
34 |
35 | @Test
36 | void endlessLoopCPUStressTest() {
37 | long ranSecs = StressCpuExample.runMeasurement(StressCpuExample.DEFAULT_SECONDS_TO_RUN, 1, StressCpuExample::iAm100Percent);
38 | assertThat(StressCpuExample.DEFAULT_SECONDS_TO_RUN <= ranSecs).isTrue();
39 | }
40 |
41 | @Test
42 | void parallelEndlessLoopCpuStressTest() {
43 | StressCpuExample.runParallelEndlessLoopCpuStressTest(Runtime.getRuntime().availableProcessors(), StressCpuExample.DEFAULT_SECONDS_TO_RUN);
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/gradle/dist.gradle:
--------------------------------------------------------------------------------
1 | javadoc {
2 | classpath = configurations.compileClasspath
3 | title = "${project.name} API Documentation $version"
4 | failOnError = true
5 | source = sourceSets.main.allJava
6 | options.optionFiles = [file("gradle/javadoc.options")]
7 | }
8 | tasks.register('copyJavadocToDist', Copy) {
9 | includeEmptyDirs = false
10 | from javadoc
11 | into "${layout.buildDirectory.get()}/${project.name}/doc"
12 | dependsOn javadoc
13 | }
14 | tasks.register('copyDocAndConfigFilesToDist', Copy) {
15 | includeEmptyDirs = false
16 | from('src/main/resources') { include '*-template.yaml' }
17 | from('.') { include 'README.md' }
18 | into "${layout.buildDirectory.get()}/${project.name}"
19 | }
20 | tasks.register('copySbomToDist', Copy) {
21 | from('build/reports') { include 'bom.*' }
22 | into "${layout.buildDirectory.get()}/${project.name}/sbom"
23 | dependsOn cyclonedxBom
24 | }
25 | tasks.register('copyLibsToDist', Copy) {
26 | includeEmptyDirs = false
27 | from jar
28 | include '**/*.jar'
29 | exclude "${project.name}*.jar"
30 | from sourceSets.main.runtimeClasspath
31 | into "${layout.buildDirectory.get()}/${project.name}/lib"
32 | dependsOn jar, copyDocAndConfigFilesToDist, copyJavadocToDist, copySbomToDist
33 | }
34 | tasks.register('copyMainLibToDist', Copy) {
35 | includeEmptyDirs = false
36 | from jar
37 | include "${project.name}*.jar"
38 | from sourceSets.main.runtimeClasspath
39 | into "${layout.buildDirectory.get()}/${project.name}"
40 | dependsOn jar, copyDocAndConfigFilesToDist, copyJavadocToDist, copySbomToDist
41 | }
42 | tasks.register('dist', Zip) {
43 | group = 'build'
44 | description("Creates the ${project.name}.zip with all jars")
45 | from("${layout.buildDirectory.get()}") {
46 | include "${project.name}/**"
47 | }
48 | archiveFileName = "${layout.buildDirectory.get()}/${project.name}.zip"
49 | dependsOn copyLibsToDist, copyMainLibToDist, copyDocAndConfigFilesToDist, copyJavadocToDist, copySbomToDist
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/src/test/java/group/msg/jpowermonitor/junit/CsvJUnitResultsWriterTest.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.junit;
2 |
3 | import org.junit.jupiter.params.ParameterizedTest;
4 | import org.junit.jupiter.params.provider.Arguments;
5 | import org.junit.jupiter.params.provider.MethodSource;
6 |
7 | import java.io.IOException;
8 | import java.nio.charset.StandardCharsets;
9 | import java.nio.file.Files;
10 | import java.nio.file.Path;
11 | import java.nio.file.Paths;
12 | import java.util.Locale;
13 | import java.util.stream.Stream;
14 |
15 | import static org.assertj.core.api.Assertions.assertThat;
16 | import static org.junit.jupiter.params.provider.Arguments.arguments;
17 |
18 | class CsvJUnitResultsWriterTest {
19 | private static final String NEW_LINE = System.lineSeparator();
20 |
21 | static Stream l10nTestConstructorValues() {
22 | return Stream.of(
23 | arguments(new Locale.Builder().setLanguage("en").setRegion("US").build(),
24 | "Time,Name,Sensor,Value,Unit,Baseload,Unit,Value+Baseload,Unit,Energy(Value),Unit,Energy(Value+Baseload),Unit,CO2 Value,Unit",
25 | "Time,Name,Sensor,Value,Unit"),
26 | arguments(new Locale.Builder().setLanguage("de").setRegion("DE").build(),
27 | "Uhrzeit;Name;Sensor;Wert;Einheit;Grundlast;Einheit;Wert+Grundlast;Einheit;Energie(Wert);Einheit;Energie(Wert+Grundlast);Einheit;CO2 Wert;Einheit",
28 | "Uhrzeit;Name;Sensor;Wert;Einheit"),
29 | arguments(new Locale.Builder().setLanguage("fr").setRegion("FR").build(),
30 | "Heure,Nom,Détecteur,Valeur,Unité,Grundlast,Unité,Valeur+charge de base,Unité,Energie(valeur),Unité,Energie(valeur+charge de base),Unité,CO2 Valeur,Unité",
31 | "Heure,Nom,Détecteur,Valeur,Unité")
32 | );
33 | }
34 |
35 | @ParameterizedTest
36 | @MethodSource("l10nTestConstructorValues")
37 | void testConstructor(Locale currentLocale, String expContentResultCsv, String expContentMeasurementCsv) throws IOException {
38 | // Arrange
39 | Path pathToResultCsv = Paths.get("build/tmp/testResultCsvWriter/constructorResult.csv");
40 | Path pathToMeasurementCsv = Paths.get("build/tmp/testResultCsvWriter/constructorMeasurement.csv");
41 | Files.deleteIfExists(pathToResultCsv);
42 | Files.deleteIfExists(pathToMeasurementCsv);
43 | Locale.setDefault(currentLocale);
44 | JUnitResultsWriter.setLocaleDependentValues();
45 | // Act
46 | new JUnitResultsWriter(pathToResultCsv, pathToMeasurementCsv, 485.0);
47 | // Assert
48 | assertThat(Files.readString(pathToResultCsv, StandardCharsets.UTF_8)).isEqualTo(expContentResultCsv + NEW_LINE); // trim carriage-return
49 | assertThat(Files.readString(pathToMeasurementCsv, StandardCharsets.UTF_8)).isEqualTo(expContentMeasurementCsv + NEW_LINE); // trim carriage-return
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/java/group/msg/jpowermonitor/util/HumanReadableTime.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.util;
2 |
3 | import java.util.Collections;
4 | import java.util.LinkedHashMap;
5 | import java.util.Map;
6 | import java.util.concurrent.TimeUnit;
7 |
8 | /**
9 | * Class for formatting time in nanos or millis to a readable format.
10 | */
11 | public class HumanReadableTime {
12 | private static final long MILLION = 1000 * 1000;
13 | private static final Map TIME_UNITS_NANOS = timeUnitsToNanos();
14 | private static final Map TIME_UNITS_MILLIS = timeUnitsToMillis();
15 |
16 | private static final double factorNanosToHours = 3600000000000.0;
17 |
18 | public static double nanosToHours(long nanos) {
19 | return nanos / factorNanosToHours;
20 | }
21 |
22 | static Map timeUnitsToNanos() {
23 | Map numMap = new LinkedHashMap<>(timeUnitsToMillis());
24 | numMap.put(TimeUnit.NANOSECONDS, "ns");
25 | return Collections.unmodifiableMap(numMap);
26 | }
27 |
28 | static Map timeUnitsToMillis() {
29 | Map numMap = new LinkedHashMap<>(); // order is important
30 | numMap.put(TimeUnit.DAYS, "d");
31 | numMap.put(TimeUnit.HOURS, "h");
32 | numMap.put(TimeUnit.MINUTES, "m");
33 | numMap.put(TimeUnit.SECONDS, "s");
34 | numMap.put(TimeUnit.MILLISECONDS, "ms");
35 | return Collections.unmodifiableMap(numMap);
36 | }
37 |
38 | public static String ofNanos(long nanos) {
39 | StringBuilder builder = new StringBuilder();
40 | long acc = nanos;
41 | int cutOff = 0;
42 | Map reference = nanos < MILLION ? TIME_UNITS_NANOS : TIME_UNITS_MILLIS;
43 | for (Map.Entry e : reference.entrySet()) {
44 | long convert = e.getKey().convert(acc, TimeUnit.NANOSECONDS);
45 | if (convert > 0) {
46 | builder.append(convert).append(e.getValue()).append(" ");
47 | acc -= TimeUnit.NANOSECONDS.convert(convert, e.getKey());
48 | cutOff = 1;
49 | }
50 | }
51 | return builder.substring(0, builder.length() - cutOff);
52 | }
53 |
54 | public static String ofMillis(long millis) {
55 | StringBuilder builder = new StringBuilder();
56 | if (millis == 0) {
57 | return "0ms";
58 | }
59 | long acc = millis;
60 | int cutOff = 0;
61 | for (Map.Entry e : TIME_UNITS_MILLIS.entrySet()) {
62 | long convert = e.getKey().convert(acc, TimeUnit.MILLISECONDS);
63 | if (convert > 0) {
64 | builder.append(convert).append(e.getValue()).append(" ");
65 | acc -= TimeUnit.MILLISECONDS.convert(convert, e.getKey());
66 | cutOff = 1;
67 | }
68 | }
69 | return builder.substring(0, builder.length() - cutOff);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/test/java/group/msg/jpowermonitor/util/ConverterTest.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.util;
2 |
3 | import org.assertj.core.data.Offset;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import static org.assertj.core.api.Assertions.assertThat;
7 |
8 | public class ConverterTest {
9 | @Test
10 | void convertJouleToWattHoursTest() {
11 | assertThat(Converter.convertJouleToWattHours(1.0)).isCloseTo(0.000277778, Offset.offset(0.000000001));
12 | assertThat(Converter.convertJouleToWattHours(1000.0)).isCloseTo(0.277778, Offset.offset(0.000001));
13 | assertThat(Converter.convertJouleToWattHours(3600.0)).isCloseTo(1.0, Offset.offset(0.0));
14 | assertThat(Converter.convertJouleToWattHours(10000000.0)).isCloseTo(2777.77, Offset.offset(0.01));
15 | }
16 |
17 | @Test
18 | void convertWattHoursToJouleTest() {
19 | assertThat(Converter.convertWattHoursToJoule(1.0)).isEqualByComparingTo(3600.0);
20 | assertThat(Converter.convertWattHoursToJoule(0.255)).isEqualByComparingTo(918.0);
21 | assertThat(Converter.convertWattHoursToJoule(0.277778)).isCloseTo(1000.0, Offset.offset(0.001));
22 | }
23 |
24 | @Test
25 | void convertJouleToKiloWattHoursTest() {
26 | assertThat(Converter.convertJouleToKiloWattHours(1.0)).isCloseTo(0.000000277778, Offset.offset(0.000000000001));
27 | assertThat(Converter.convertJouleToKiloWattHours(1000.0)).isCloseTo(0.000277778, Offset.offset(0.000000001));
28 | assertThat(Converter.convertJouleToKiloWattHours(3600.0)).isCloseTo(0.001, Offset.offset(0.001));
29 | assertThat(Converter.convertJouleToKiloWattHours(10000000.0)).isCloseTo(2.77, Offset.offset(0.01));
30 | assertThat(Converter.convertJouleToKiloWattHours(3600000)).isCloseTo(1.0, Offset.offset(0.0));
31 | }
32 |
33 | @Test
34 | void convertKiloWattHoursToCarbonDioxideTest() {
35 | assertThat(Converter.convertKiloWattHoursToCarbonDioxideGrams(1.0, 485.0)).isCloseTo(485.0, Offset.offset(0.1));
36 | assertThat(Converter.convertKiloWattHoursToCarbonDioxideGrams(0.000000277778, 485.0)).isCloseTo(0.001347222, Offset.offset(0.002));
37 | assertThat(Converter.convertKiloWattHoursToCarbonDioxideGrams(0.1, 485.0)).isCloseTo(48.5, Offset.offset(0.1));
38 | assertThat(Converter.convertKiloWattHoursToCarbonDioxideGrams(0.01, 485.0)).isCloseTo(4.85, Offset.offset(0.01));
39 | assertThat(Converter.convertKiloWattHoursToCarbonDioxideGrams(0.01, 300.0)).isCloseTo(3.00, Offset.offset(0.01));
40 | }
41 |
42 | @Test
43 | void convertJouleToCarbonDioxideGramsTest() {
44 | assertThat(Converter.convertJouleToCarbonDioxideGrams(1.0, 485.0)).isCloseTo(0.001347222, Offset.offset(0.002));
45 | assertThat(Converter.convertJouleToCarbonDioxideGrams(3600000, 485.0)).isCloseTo(485.0, Offset.offset(0.1));
46 | assertThat(Converter.convertJouleToCarbonDioxideGrams(10000000.0, 485.0)).isCloseTo(1347.22, Offset.offset(0.01));
47 | assertThat(Converter.convertJouleToCarbonDioxideGrams(3600000, 400.0)).isCloseTo(400.0, Offset.offset(0.1));
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 |
74 |
75 | @rem Execute Gradle
76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
77 |
78 | :end
79 | @rem End local scope for the variables with windows NT shell
80 | if %ERRORLEVEL% equ 0 goto mainEnd
81 |
82 | :fail
83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
84 | rem the _cmd.exe /c_ return code!
85 | set EXIT_CODE=%ERRORLEVEL%
86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
88 | exit /b %EXIT_CODE%
89 |
90 | :mainEnd
91 | if "%OS%"=="Windows_NT" endlocal
92 |
93 | :omega
94 |
--------------------------------------------------------------------------------
/src/test/java/group/msg/jpowermonitor/agent/MeasurePowerTest.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.agent;
2 |
3 | import group.msg.jpowermonitor.dto.DataPoint;
4 | import lombok.extern.slf4j.Slf4j;
5 | import org.junit.jupiter.api.Disabled;
6 | import org.junit.jupiter.api.condition.EnabledOnOs;
7 | import org.junit.jupiter.api.condition.OS;
8 |
9 | import static org.assertj.core.api.Assertions.assertThat;
10 |
11 | @EnabledOnOs(OS.WINDOWS)
12 | @Slf4j
13 | class MeasurePowerTest {
14 |
15 | private static final int REASONABLE_MEASUREMENT_INTERVAL_MS = 850;
16 |
17 | /**
18 | * Steps down wait interval (in 50ms steps) between measurements until implemented/configured MeasureMethod delivers the same value again.
19 | * -> e.g. for Libre Hardware Monitor between 750-850ms seems to be the minimal possible interval to get updatet values
20 | */
21 | @Disabled("Use this test to find the minimum viable measurement interval for your platform and your configured measure method")
22 | void findReasonableMeasurementIntervalForMeasureMethodTest() {
23 | long loopCount = 0;
24 | for (int intervalInMs = 1000; intervalInMs >= REASONABLE_MEASUREMENT_INTERVAL_MS; intervalInMs -= 50) {
25 | log.info("Interval {}ms, loopCount {}", intervalInMs, loopCount);
26 | loopCount = 0;
27 | DataPoint dp = PowerMeasurementCollector.getCurrentCpuPowerInWatts();
28 | log.debug("{}", dp);
29 | long busyWaitUntil = System.currentTimeMillis() + intervalInMs;
30 | while (System.currentTimeMillis() <= busyWaitUntil) {
31 | loopCount++;
32 | }
33 | DataPoint dp2 = PowerMeasurementCollector.getCurrentCpuPowerInWatts();
34 | log.debug("{}", dp2);
35 | assertThat(dp.getUnit()).isEqualTo(dp2.getUnit());
36 | assertThat(dp.getTime()).isNotEqualTo(dp2.getTime());
37 | assertThat(dp.getValue()).isNotEqualTo(dp2.getValue());
38 | }
39 | }
40 |
41 | @Disabled("Use this test to find the minimum viable measurement interval for your platform and your configured measure method")
42 | void verifyReasonableMeasurementIntervalForMeasureMethodTest() throws InterruptedException {
43 | int intervalInMs = REASONABLE_MEASUREMENT_INTERVAL_MS;
44 | long loopCount = 0;
45 | for (int i = 0; i < 25; i++) {
46 | log.info("Interval {}ms, loopCount {}, run {}", intervalInMs, loopCount, i);
47 | loopCount = 0;
48 | DataPoint dp = PowerMeasurementCollector.getCurrentCpuPowerInWatts();
49 | log.debug("{}", dp);
50 | long busyWaitUntil = System.currentTimeMillis() + intervalInMs;
51 | while (System.currentTimeMillis() <= busyWaitUntil) {
52 | loopCount++;
53 | }
54 | Thread.sleep(intervalInMs);
55 | DataPoint dp2 = PowerMeasurementCollector.getCurrentCpuPowerInWatts();
56 | log.debug("{}", dp2);
57 | assertThat(dp.getUnit()).isEqualTo(dp2.getUnit());
58 | assertThat(dp.getTime()).isNotEqualTo(dp2.getTime());
59 | assertThat(dp.getValue()).isNotEqualTo(dp2.getValue());
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/java/group/msg/jpowermonitor/util/CpuAndThreadUtils.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.util;
2 |
3 | import group.msg.jpowermonitor.dto.DataPoint;
4 | import lombok.extern.slf4j.Slf4j;
5 | import org.jetbrains.annotations.NotNull;
6 |
7 | import java.lang.management.ManagementFactory;
8 | import java.lang.management.ThreadMXBean;
9 | import java.util.HashMap;
10 | import java.util.Map;
11 | import java.util.Set;
12 |
13 | import static group.msg.jpowermonitor.util.Constants.ONE_HUNDRED;
14 |
15 | /**
16 | * Utility class for all CPU and thread time/power related tasks
17 | *
18 | * @author deinerj
19 | */
20 | @Slf4j
21 | public class CpuAndThreadUtils {
22 |
23 | private static ThreadMXBean threadMXBean;
24 |
25 | @NotNull
26 | public static ThreadMXBean initializeAndGetThreadMxBeanOrFailAndQuitApplication() {
27 | if (threadMXBean == null) {
28 | threadMXBean = ManagementFactory.getThreadMXBean();
29 | // Check if CPU Time measurement is supported by the JVM. Quit otherwise
30 | if (!threadMXBean.isThreadCpuTimeSupported()) {
31 | log.error("Thread CPU Time is not supported in this JVM, unable to measure energy consumption.");
32 | System.exit(1);
33 | }
34 | // Enable CPU Time measurement if it is disabled
35 | if (!threadMXBean.isThreadCpuTimeEnabled()) {
36 | threadMXBean.setThreadCpuTimeEnabled(true);
37 | }
38 | }
39 | return threadMXBean;
40 | }
41 |
42 | public static long getTotalApplicationCpuTimeAndCalculateCpuTimePerApplicationThread(ThreadMXBean threadMxBean, Map cpuTimePerApplicationThread, Set applicationThreads) {
43 | long totalApplicationCpuTime = 0L;
44 | for (Thread t : applicationThreads) {
45 | long applicationThreadCpuTime = threadMxBean.getThreadCpuTime(t.getId()); // use t.threadId() with JDK 21
46 |
47 | // If thread already monitored, then calculate CPU time since last time
48 | if (cpuTimePerApplicationThread.containsKey(t.getName())) {
49 | applicationThreadCpuTime -= cpuTimePerApplicationThread.get(t.getName());
50 | }
51 |
52 | cpuTimePerApplicationThread.put(t.getName(), applicationThreadCpuTime);
53 | totalApplicationCpuTime += applicationThreadCpuTime;
54 | }
55 | return totalApplicationCpuTime;
56 | }
57 |
58 | @NotNull
59 | public static Map calculatePowerPerApplicationThread(Map cpuTimePerApplicationThread, DataPoint currentPower, long totalApplicationCpuTime) {
60 | Map powerPerApplicationThread = new HashMap<>();
61 | for (Map.Entry entry : cpuTimePerApplicationThread.entrySet()) {
62 | Double percentageCpuTimePerApplicationThread = totalApplicationCpuTime > 0 ? entry.getValue() * ONE_HUNDRED / totalApplicationCpuTime : 0.0;
63 | Double applicationThreadPower = currentPower.getValue() * percentageCpuTimePerApplicationThread / ONE_HUNDRED;
64 | powerPerApplicationThread.put(entry.getKey(), applicationThreadPower);
65 | }
66 | return powerPerApplicationThread;
67 | }
68 |
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/src/test/java/com/msg/myapplication/MyTest.java:
--------------------------------------------------------------------------------
1 | package com.msg.myapplication;
2 |
3 | import group.msg.jpowermonitor.dto.SensorValue;
4 | import group.msg.jpowermonitor.dto.SensorValues;
5 | import group.msg.jpowermonitor.junit.JPowerMonitorExtension;
6 | import lombok.extern.slf4j.Slf4j;
7 | import org.assertj.core.data.Offset;
8 | import org.junit.jupiter.api.AfterEach;
9 | import org.junit.jupiter.api.RepeatedTest;
10 | import org.junit.jupiter.api.Test;
11 | import org.junit.jupiter.api.extension.ExtendWith;
12 |
13 | import java.math.BigDecimal;
14 | import java.math.MathContext;
15 | import java.math.RoundingMode;
16 | import java.util.List;
17 |
18 | import static group.msg.jpowermonitor.agent.Unit.WATT;
19 | import static org.assertj.core.api.Assertions.assertThat;
20 |
21 | @ExtendWith({JPowerMonitorExtension.class})
22 | @Slf4j
23 | public class MyTest {
24 | @SensorValues
25 | List sensorValueList;
26 |
27 | @RepeatedTest(1)
28 | void myPowerConsumingSuperTest() {
29 | BigDecimal sum = BigDecimal.ZERO;
30 | for (int i = 0; i < 1_000_000; i++) {
31 | BigDecimal a = new BigDecimal("7.488").add(new BigDecimal(i));
32 | BigDecimal sqrt = a.sqrt(new MathContext(100, RoundingMode.HALF_UP));
33 | sum = sum.add(sqrt).setScale(2, RoundingMode.HALF_UP);
34 | }
35 | log.info("Sum is {}", sum);
36 | assertThat(sum).isEqualTo(new BigDecimal("666673641.02"));
37 | }
38 |
39 | @AfterEach
40 | void myMethodAfterEachTest() {
41 | // @SensorValues annotated fields of type List are accessible after each test
42 | log.info("sensorvalues: {}", sensorValueList);
43 | assertThat(sensorValueList).isNotNull();
44 | //
45 | // as we use a fix csv file, the outcome is fix for this test:
46 | sensorValueList.forEach(x -> {
47 | assertThat(x.getValue()).isCloseTo(5.05, Offset.offset(0.01));
48 | assertThat(x.getPowerInIdleMode()).isEqualTo(2.0);
49 | assertThat(x.getName()).isEqualTo("CPU Power");
50 | assertThat(x.getUnit()).isEqualTo(WATT);
51 | });
52 | }
53 |
54 | @RepeatedTest(1)
55 | void myPowerConsumingSuperTestDifferentAlgo() {
56 | BigDecimal sum = BigDecimal.ZERO;
57 | for (int i = 0; i < 1_000_000; i++) {
58 | BigDecimal a = new BigDecimal("7.488").add(new BigDecimal(i));
59 | BigDecimal sqrt = a.sqrt(new MathContext(100, RoundingMode.HALF_UP)).divide(new BigDecimal("3.14"), new MathContext(200, RoundingMode.HALF_UP));
60 | sum = sum.add(sqrt).setScale(2, RoundingMode.HALF_UP);
61 | }
62 | log.info("Sum is {}", sum);
63 | assertThat(sum).isEqualTo(new BigDecimal("212316445.91"));
64 | }
65 |
66 | @Test
67 | void myPowerConsumingSuperTestLong() {
68 | BigDecimal sum = BigDecimal.ZERO;
69 | for (int i = 0; i < 1_000_000; i++) {
70 | BigDecimal a = new BigDecimal("7.488").add(new BigDecimal(i));
71 | BigDecimal sqrt = a.sqrt(new MathContext(100, RoundingMode.HALF_UP));
72 | sum = sum.add(sqrt).setScale(2, RoundingMode.HALF_UP);
73 | }
74 | log.info("Sum is {}", sum);
75 | assertThat(sum).isEqualTo(new BigDecimal("666673641.02"));
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/docs/slidy.css:
--------------------------------------------------------------------------------
1 | table.blueTable {
2 | border: 1px solid #1C6EA4;
3 | background-color: #EEEEEE;
4 | width: 100%;
5 | text-align: left;
6 | border-collapse: collapse;
7 | }
8 | table.blueTable td, table.blueTable th {
9 | border: 1px solid #AAAAAA;
10 | padding: 3px 2px;
11 | }
12 | table.blueTable tbody td {
13 | font-size: 13px;
14 | }
15 | table.blueTable tr:nth-child(even) {
16 | background: #D0E4F5;
17 | }
18 | table.blueTable thead {
19 | background: #1C6EA4;
20 | background: -moz-linear-gradient(top, #5592bb 0%, #327cad 66%, #1C6EA4 100%);
21 | background: -webkit-linear-gradient(top, #5592bb 0%, #327cad 66%, #1C6EA4 100%);
22 | background: linear-gradient(to bottom, #5592bb 0%, #327cad 66%, #1C6EA4 100%);
23 | border-bottom: 2px solid #444444;
24 | }
25 | table.blueTable thead th {
26 | font-size: 15px;
27 | font-weight: bold;
28 | color: #FFFFFF;
29 | border-left: 2px solid #D0E4F5;
30 | }
31 | table.blueTable thead th:first-child {
32 | border-left: none;
33 | }
34 |
35 | table.blueTable tfoot {
36 | font-size: 14px;
37 | font-weight: bold;
38 | color: #FFFFFF;
39 | background: #D0E4F5;
40 | background: -moz-linear-gradient(top, #dcebf7 0%, #d4e6f6 66%, #D0E4F5 100%);
41 | background: -webkit-linear-gradient(top, #dcebf7 0%, #d4e6f6 66%, #D0E4F5 100%);
42 | background: linear-gradient(to bottom, #dcebf7 0%, #d4e6f6 66%, #D0E4F5 100%);
43 | border-top: 2px solid #444444;
44 | }
45 | table.blueTable tfoot td {
46 | font-size: 14px;
47 | }
48 | table.blueTable tfoot .links {
49 | text-align: right;
50 | }
51 | table.blueTable tfoot .links a{
52 | display: inline-block;
53 | background: #1C6EA4;
54 | color: #FFFFFF;
55 | padding: 2px 8px;
56 | border-radius: 5px;
57 | }
58 | body {
59 | margin: auto;
60 | padding-right: 1em;
61 | padding-left: 1em;
62 | max-width: 44em;
63 | border-left: none;
64 | border-right: none;
65 | color: black;
66 | font-family: Verdana, sans-serif;
67 | font-size: 13px;
68 | line-height: 140%;
69 | color: #333;
70 | }
71 | pre {
72 | border: 1px dotted gray;
73 | background-color: #ececec;
74 | color: #1111111;
75 | padding: 0.5em;
76 | }
77 | code {
78 | font-family: monospace;
79 | }
80 | h1 a, h2 a, h3 a, h4 a, h5 a {
81 | text-decoration: none;
82 | color: #7a5ada;
83 | }
84 | h1, h2, h3, h4, h5 { font-family: verdana;
85 | font-weight: bold;
86 | border-bottom: 1px dotted black;
87 | color: #7a5ada; }
88 | h1 {
89 | font-size: 130%;
90 | }
91 |
92 | h2 {
93 | font-size: 110%;
94 | }
95 |
96 | h3 {
97 | font-size: 95%;
98 | }
99 |
100 | h4 {
101 | font-size: 90%;
102 | font-style: italic;
103 | }
104 |
105 | h5 {
106 | font-size: 90%;
107 | font-style: italic;
108 | }
109 |
110 | h1.title {
111 | font-size: 200%;
112 | font-weight: bold;
113 | padding-top: 0.2em;
114 | padding-bottom: 0.2em;
115 | text-align: left;
116 | border: none;
117 | }
118 |
119 | dt code {
120 | font-weight: bold;
121 | }
122 | dd p {
123 | margin-top: 0;
124 | }
125 | table, th, td {
126 | border: 2px solid black;
127 | }
128 |
129 | #footer {
130 | padding-top: 1em;
131 | font-size: 70%;
132 | color: gray;
133 | text-align: center;
134 | }
--------------------------------------------------------------------------------
/src/main/java/group/msg/jpowermonitor/agent/export/statistics/StatisticsWriter.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.agent.export.statistics;
2 |
3 | import group.msg.jpowermonitor.agent.JPowerMonitorAgent;
4 | import group.msg.jpowermonitor.agent.PowerMeasurementCollector;
5 | import group.msg.jpowermonitor.agent.export.csv.CsvResultsWriter;
6 | import lombok.extern.slf4j.Slf4j;
7 |
8 | import java.util.Locale;
9 |
10 | import static group.msg.jpowermonitor.util.Constants.SEPARATOR;
11 | import static group.msg.jpowermonitor.util.Converter.convertJouleToKiloWattHours;
12 | import static group.msg.jpowermonitor.util.Converter.convertJouleToWattHours;
13 |
14 | @Slf4j
15 | public class StatisticsWriter {
16 | private final PowerMeasurementCollector powerMeasurementCollector;
17 | private static long benchmarkResult;
18 |
19 | public StatisticsWriter(PowerMeasurementCollector powerMeasurementCollector) {
20 | this.powerMeasurementCollector = powerMeasurementCollector;
21 | }
22 |
23 | public void writeStatistics(CsvResultsWriter csvResultsWriter) {
24 | if (powerMeasurementCollector == null
25 | || powerMeasurementCollector.getEnergyConsumptionTotalInJoule() == null
26 | || powerMeasurementCollector.getEnergyConsumptionTotalInJoule().get() == null
27 | || powerMeasurementCollector.getEnergyConsumptionTotalInJoule().get().getValue() == null) {
28 | return;
29 | }
30 | String appStatistics = String.format("Application consumed %.2f joule - %.3f wh - %.6f kwh - %.3f gCO2 total",
31 | powerMeasurementCollector.getEnergyConsumptionTotalInJoule().get().getValue(),
32 | convertJouleToWattHours(powerMeasurementCollector.getEnergyConsumptionTotalInJoule().get().getValue()),
33 | convertJouleToKiloWattHours(powerMeasurementCollector.getEnergyConsumptionTotalInJoule().get().getValue()),
34 | powerMeasurementCollector.getEnergyConsumptionTotalInJoule().get().getCo2Value());
35 | long totalEnergyConsumptionInJoule = powerMeasurementCollector.getEnergyConsumptionTotalInJoule().get()
36 | .getValue().longValue();
37 | String benchmarkResult = hasBenchmarkResult() && totalEnergyConsumptionInJoule > 0.0 ? String.format(
38 | Locale.GERMANY,
39 | "Benchmark result efficiency factor (sum of all loop counters / energyConsumptionTotal): *** %,d *** jPMarks",
40 | getBenchmarkResult() / totalEnergyConsumptionInJoule) : "";
41 | String filesInfo = "Energy consumption per method written to '"
42 | + csvResultsWriter.getEnergyConsumptionPerMethodFileName()
43 | + "' and filtered methods written to '"
44 | + csvResultsWriter.getEnergyConsumptionPerFilteredMethodFileName() + "'" + "\n" + SEPARATOR;
45 |
46 | if (JPowerMonitorAgent.isSlf4jLoggerImplPresent()) {
47 | log.info(appStatistics);
48 | log.info(benchmarkResult);
49 | log.info(filesInfo);
50 | } else {
51 | System.out.println(appStatistics);
52 | System.out.println(benchmarkResult);
53 | System.out.println(filesInfo);
54 | }
55 | }
56 |
57 | private static long getBenchmarkResult() {
58 | return benchmarkResult;
59 | }
60 |
61 | public static void setBenchmarkResult(long benchmarkResult) {
62 | StatisticsWriter.benchmarkResult = benchmarkResult;
63 | }
64 |
65 | private boolean hasBenchmarkResult() {
66 | return benchmarkResult > 0;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/test/java/group/msg/jpowermonitor/measurement/est/EstimationReaderTest.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.measurement.est;
2 |
3 | import group.msg.jpowermonitor.config.DefaultCfgProvider;
4 | import group.msg.jpowermonitor.config.JPowerMonitorCfgProvider;
5 | import group.msg.jpowermonitor.config.dto.JPowerMonitorCfg;
6 | import group.msg.jpowermonitor.dto.DataPoint;
7 | import lombok.extern.slf4j.Slf4j;
8 | import org.junit.jupiter.api.Assertions;
9 | import org.junit.jupiter.api.BeforeEach;
10 | import org.junit.jupiter.api.Test;
11 |
12 | import java.math.BigDecimal;
13 | import java.math.MathContext;
14 | import java.math.RoundingMode;
15 | import java.util.concurrent.Callable;
16 | import java.util.concurrent.ExecutionException;
17 | import java.util.concurrent.ExecutorService;
18 | import java.util.concurrent.Executors;
19 | import java.util.concurrent.Future;
20 | import java.util.concurrent.TimeUnit;
21 | import java.util.concurrent.TimeoutException;
22 | import java.util.stream.IntStream;
23 |
24 | @Slf4j
25 | class EstimationReaderTest {
26 | @BeforeEach
27 | void setup() {
28 | DefaultCfgProvider.invalidateCachedConfig();
29 | }
30 |
31 | volatile boolean threadIsStopped = false;
32 |
33 | @Test
34 | void testEstimateWattageBasedOnCpuUsage() throws ExecutionException, InterruptedException, TimeoutException {
35 | JPowerMonitorCfgProvider configProvider = new DefaultCfgProvider();
36 | configProvider.readConfig("EstimationReaderTest.yaml");
37 |
38 | ExecutorService executor = Executors.newSingleThreadExecutor(); // cannot use try with resources, since we use JDK17 for compilation, JDK >= 19 needed.
39 | try {
40 | Callable measureThread = createMeasureThread(configProvider);
41 | Future result = executor.submit(measureThread);
42 | BigDecimal sum = IntStream.range(0, 100000)
43 | .mapToObj(x -> new BigDecimal("2.456").pow(x, new MathContext(10, RoundingMode.HALF_UP)))
44 | .reduce(BigDecimal.ZERO, BigDecimal::add);
45 | log.info("Sum is {}", sum);
46 | threadIsStopped = true;
47 | String resultString = result.get(30, TimeUnit.SECONDS);
48 | Assertions.assertEquals("OK", resultString);
49 | } finally {
50 | executor.shutdown();
51 | }
52 | }
53 |
54 | private Callable createMeasureThread(JPowerMonitorCfgProvider configProvider) {
55 | JPowerMonitorCfg config = configProvider.getCachedConfig();
56 | EstimationReader cmr = new EstimationReader(config);
57 | return () -> {
58 | try {
59 | while (!threadIsStopped) {
60 | DataPoint dataPoint = cmr.measureFirstConfiguredPath();
61 | log.info("cpuMeasure: {}", dataPoint);
62 | Assertions.assertTrue(config.getMeasurement().getEst().getCpuMinWatts() <= dataPoint.getValue()
63 | && dataPoint.getValue() <= config.getMeasurement().getEst().getCpuMaxWatts(),
64 | "Value must be between configured min and max value.");
65 | sleep();
66 | }
67 | } catch (AssertionError e) {
68 | return "Failing test:" + e.getMessage();
69 | }
70 | return "OK";
71 | };
72 | }
73 |
74 | private static void sleep() {
75 | try {
76 | TimeUnit.SECONDS.sleep(1);
77 | } catch (InterruptedException e) {
78 | log.error("ignore InterruptedException");
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/test/java/group/msg/jpowermonitor/config/DefaultCfgProviderTest.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.config;
2 |
3 | import group.msg.jpowermonitor.config.dto.CsvColumnCfg;
4 | import group.msg.jpowermonitor.config.dto.CsvMeasurementCfg;
5 | import group.msg.jpowermonitor.config.dto.CsvRecordingCfg;
6 | import group.msg.jpowermonitor.config.dto.JPowerMonitorCfg;
7 | import group.msg.jpowermonitor.config.dto.JavaAgentCfg;
8 | import group.msg.jpowermonitor.config.dto.LibreHardwareMonitorCfg;
9 | import group.msg.jpowermonitor.config.dto.MeasurementCfg;
10 | import group.msg.jpowermonitor.config.dto.MonitoringCfg;
11 | import group.msg.jpowermonitor.config.dto.PathElementCfg;
12 | import group.msg.jpowermonitor.config.dto.PrometheusCfg;
13 | import org.junit.jupiter.api.BeforeEach;
14 | import org.junit.jupiter.api.Test;
15 |
16 | import java.util.List;
17 | import java.util.Set;
18 |
19 | import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
20 | import static org.junit.jupiter.api.Assertions.assertSame;
21 |
22 |
23 | class DefaultCfgProviderTest {
24 | @BeforeEach
25 | void resetConfig() {
26 | DefaultCfgProvider.invalidateCachedConfig();
27 | }
28 |
29 | @Test
30 | public void readConfig_fromResourceIfNoFile() {
31 | JPowerMonitorCfg cfg = new DefaultCfgProvider().readConfig("DefaultConfigProviderTest.yaml");
32 | assertThat(cfg).isNotNull();
33 |
34 | JPowerMonitorCfg expected = new JPowerMonitorCfg();
35 | expected.setInitCycles(7);
36 | expected.setSamplingIntervalForInitInMs(8);
37 | expected.setCalmDownIntervalInMs(9);
38 | expected.setPercentageOfSamplesAtBeginningToDiscard(3.0);
39 | expected.setSamplingIntervalInMs(4);
40 | expected.setCarbonDioxideEmissionFactor(777.0);
41 |
42 | MeasurementCfg measurement = new MeasurementCfg();
43 | measurement.setMethod("lhm");
44 |
45 | LibreHardwareMonitorCfg lhm = new LibreHardwareMonitorCfg();
46 | PathElementCfg pe = new PathElementCfg();
47 | pe.setPath(List.of("pc", "cpu", "path1", "path2"));
48 | lhm.setPaths(List.of(pe));
49 | lhm.setUrl("some.test.url" + "/data.json"); // /data.json is internally added
50 | measurement.setLhm(lhm);
51 |
52 | CsvMeasurementCfg csv = new CsvMeasurementCfg();
53 | csv.setInputFile("mycsv.csv");
54 | csv.setLineToRead("first");
55 | CsvColumnCfg csvColumn = new CsvColumnCfg();
56 | csvColumn.setIndex(42);
57 | csvColumn.setName("CPU Power");
58 | csv.setColumns(List.of(csvColumn));
59 | csv.setEncoding("UTF-16");
60 | csv.setDelimiter(";");
61 | measurement.setCsv(csv);
62 | expected.setMeasurement(measurement);
63 |
64 | CsvRecordingCfg csvRecording = new CsvRecordingCfg();
65 | csvRecording.setMeasurementCsv("test_measurement.csv");
66 | csvRecording.setResultCsv("test_energyconsumption.csv");
67 | expected.setCsvRecording(csvRecording);
68 |
69 | MonitoringCfg monitoringCfg = new MonitoringCfg();
70 | PrometheusCfg prometheusCfg = new PrometheusCfg();
71 | prometheusCfg.setHttpPort(1234); // Default
72 | prometheusCfg.setWriteEnergyIntervalInS(30L); // Default
73 | monitoringCfg.setPrometheus(prometheusCfg);
74 | JavaAgentCfg javaAgentCfg = new JavaAgentCfg(Set.of("com.something", "com.anything"),
75 | 2,
76 | 3,
77 | 4,
78 | monitoringCfg);
79 | expected.setJavaAgent(javaAgentCfg);
80 |
81 | assertThat(cfg).usingRecursiveComparison().isEqualTo(expected);
82 | }
83 |
84 | @Test
85 | public void readConfig_usesCaching() {
86 | JPowerMonitorCfgProvider provider = new DefaultCfgProvider();
87 | JPowerMonitorCfg first = provider.readConfig("DefaultConfigProviderTest.yaml");
88 | assertThat(first).isNotNull();
89 | JPowerMonitorCfg second = provider.readConfig("something.else");
90 | assertSame(first, second);
91 | }
92 |
93 | }
94 |
--------------------------------------------------------------------------------
/src/main/java/group/msg/jpowermonitor/measurement/est/EstimationReader.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.measurement.est;
2 |
3 | import group.msg.jpowermonitor.JPowerMonitorException;
4 | import group.msg.jpowermonitor.MeasureMethod;
5 | import group.msg.jpowermonitor.agent.Unit;
6 | import group.msg.jpowermonitor.config.dto.EstimationCfg;
7 | import group.msg.jpowermonitor.config.dto.JPowerMonitorCfg;
8 | import group.msg.jpowermonitor.dto.DataPoint;
9 | import group.msg.jpowermonitor.util.CpuAndThreadUtils;
10 | import lombok.extern.slf4j.Slf4j;
11 | import org.jetbrains.annotations.NotNull;
12 |
13 | import java.time.LocalDateTime;
14 | import java.util.List;
15 | import java.util.Map;
16 | import java.util.Objects;
17 | import java.util.concurrent.TimeUnit;
18 |
19 | /**
20 | * Implementation of the Estimation (see
21 | * Energy Estimate (Watt-Hours))
22 | * measure method.
23 | *
24 | * @see MeasureMethod
25 | */
26 | @Slf4j
27 | public class EstimationReader implements MeasureMethod {
28 | private static final double EST_CPU_USAGE_FALLBACK = 0.5;
29 | public static final long MEASURE_TIME_ESTIMATION_MS = 100L;
30 | private static final String ESTIMATED_CPU_WATTS = "Estimated CPU Watts";
31 | private final EstimationCfg estCfg;
32 |
33 | public EstimationReader(JPowerMonitorCfg config) {
34 | Objects.requireNonNull(config.getMeasurement().getEst(), "Estimation config must be set!");
35 | this.estCfg = config.getMeasurement().getEst();
36 | }
37 |
38 | @Override
39 | public @NotNull DataPoint measureFirstConfiguredPath() throws JPowerMonitorException {
40 | final double cpuUsage = getCpuUsage();
41 | double value = estCfg.getCpuMinWatts() + (cpuUsage * (estCfg.getCpuMaxWatts() - estCfg.getCpuMinWatts()));
42 | return new DataPoint(ESTIMATED_CPU_WATTS, value, Unit.WATT, LocalDateTime.now(), Thread.currentThread().getName());
43 | }
44 |
45 | @Override
46 | public @NotNull List configuredSensors() {
47 | return List.of(ESTIMATED_CPU_WATTS);
48 | }
49 |
50 | @Override
51 | public @NotNull Map defaultEnergyInIdleModeForMeasuredSensors() {
52 | return Map.of(ESTIMATED_CPU_WATTS, estCfg.getCpuMinWatts());
53 | }
54 |
55 | public double getCpuUsage() {
56 | // see https://www.cloudcarbonfootprint.org/docs/methodology/#energy-estimate-watt-hours
57 | long[] ids = CpuAndThreadUtils.initializeAndGetThreadMxBeanOrFailAndQuitApplication().getAllThreadIds();
58 |
59 | // Init measurement start time and CPU time
60 | long startTime = System.nanoTime();
61 | long startCpuTime = 0L;
62 | for (long id : ids) {
63 | startCpuTime += CpuAndThreadUtils.initializeAndGetThreadMxBeanOrFailAndQuitApplication().getThreadCpuTime(id);
64 | }
65 |
66 | // Wait for 100ms (WAIT_TIME_ESTIMATION_MS)
67 | try {
68 | TimeUnit.MILLISECONDS.sleep(MEASURE_TIME_ESTIMATION_MS);
69 | } catch (InterruptedException e) {
70 | log.info("Sleep was interrupted, ignoring");
71 | }
72 |
73 | // End measurement and add CPU time of all threads
74 | long endTime = System.nanoTime();
75 | long endCpuTime = 0L;
76 | for (long id : ids) {
77 | endCpuTime += CpuAndThreadUtils.initializeAndGetThreadMxBeanOrFailAndQuitApplication().getThreadCpuTime(id);
78 | }
79 |
80 | // Calculate approximated CPU usage in the last 100ms
81 | long elapsedCpu = endCpuTime - startCpuTime;
82 | long elapsedTime = endTime - startTime;
83 | double cpuUsage = (double) elapsedCpu / elapsedTime;
84 |
85 | if (cpuUsage <= 0) { // Fallback to 0.5 (50%) if CPU usage is negative or zero
86 | return EST_CPU_USAGE_FALLBACK;
87 | }
88 | // Fallback to 1 if CPU usage is greater than 1 - more than 100% is not possible ;)
89 | return Math.min(cpuUsage, 1);
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | [//]: # ($formatter:off$)
2 | # Changelog
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [not yet released]
9 | - dependency updates:
10 | - upgrade com.fasterxml.jackson.datatype.jackson-datatype-jsr310 to 2.20.0
11 | - upgrade org.apache.httpcomponents.client5:httpclient5 to 5.5.1
12 | - upgrade org.jetbrains:annotations to 26.0.2-1
13 | - upgrade org.junit.jupiter:junit-jupiter to 6.0.0 (JUnit 6 migration)
14 | - upgrade org.assertj:assertj-core to 3.27.6
15 | - upgrade org.yaml:snakeyaml to 2.5
16 | - upgrade gradle to 9.1.0
17 | - upgrade com.gradleup.shadow plugin to 9.2.2 (for Gradle 9.x compatibility)
18 | - Require JDK 17 now, as many libraries do require JDK 17 or higher
19 | - Migrated to JUnit 6.0.0 (released September 30, 2025)
20 | - Note: Projects using jPowerMonitor can continue to use JUnit 5.x due to backward compatibility of the JUnit Jupiter Extension API
21 |
22 | ## 2025-05-12 - release 1.2.2
23 | - dependency updates:
24 | - upgrade com.fasterxml.jackson.datatype:jackson-datatype-jsr310 to 2.19.0
25 | - upgrade org.apache.httpcomponents.client5:httpclient5 to 5.4.4
26 | - upgrade org.jetbrains:annotations to 26.0.2
27 | - upgrade org.junit.jupiter:junit-jupiter to 5.12.2
28 | - upgrade org.slf4j:slf4j-api to 2.0.17
29 | - upgrade org.slf4j:slf4j-simple to 2.0.17
30 | - upgrade org.yaml:snakeyaml to 2.4
31 | - upgrade gradle to 8.14
32 |
33 | ## 2024-09-26 - release 1.2.1
34 | - fix StatisticsWriter division by zero problem and remove dependency to demo application with benchmark results.
35 |
36 | ## 2024-09-26 - release 1.2.0
37 | - add prometheus interface and configuration
38 | - add cloud toolkit estimation method
39 | - fix calculation of energy for intervals different to 1sec (1 Ws = 1 J)
40 | - use double/Double instead of BigDecimal:
41 | - refactor all BigDecimals to double/Double values (for slightly better performance and slightly less precise results)
42 | - refactor MeasureMethod hierarchy
43 | - separate jPowerMonitor jar from demo application jpowermonitor-demo.jar. See Readme for more information.
44 | - dependency updates:
45 | - upgrade com.fasterxml.jackson.datatype:jackson-datatype-jsr310 to 2.17.2
46 | - org.apache.httpcomponents.client5:httpclient5 to 5.4
47 | - org.junit.jupiter:junit-jupiter to 5.11.1
48 | - upgrade org.assertj:assertj-core to 3.26.3
49 | - upgrade snakeyaml to 2.3
50 | - upgrade junit-jupiter to 5.11.0
51 | - upgrade org.slf4j:slf4j-api to 2.0.16
52 | - upgrade gradle to 8.10.1
53 |
54 | ## 2024-01-17 - release 1.1.2
55 | - upgrade httpclient to 5.3
56 | - upgrade logback to 1.4.14
57 | - upgrade ch.qos.logback:logback-classic to 1.4.14
58 | - upgrade com.fasterxml.jackson.datatype:jackson-datatype-jsr310 to 2.16.1
59 | - upgrade org.assertj:assertj-core to 3.25.1
60 | - upgrade org.jetbrains:annotations to 24.1.0
61 | - upgrade org.junit.jupiter:junit-jupiter to 5.10.1
62 | - upgrade org.slf4j:slf4j-api to 2.0.11
63 | - upgrade gradle to 8.5
64 |
65 | ## 2023-11-16 - release 1.1.1
66 | - fix mvn central name and description
67 | - update the carbon dioxide factor in the default configuration to the latest published value for Germany (2022)
68 |
69 | ## 2023-10-19 - release 1.1.0
70 | - Make JUnit Extension write Joule instead of Wh in the energy column of the results csv.
71 | - Add CO2 emission output also to JUnit extension results csv.
72 |
73 | ## 2023-10-19 - release 1.0.2
74 | - replace discontinued Open Hardware Monitor by fork Libre Hardware Monitor
75 | - add spanish resource bundle for csv export
76 |
77 | ## 2023-10-18 - release 1.0.1
78 | - some minor fixes:
79 | - adding constants
80 | - no infinite loop on misconfigured csv delimiter,
81 | - fix NaN on first measurements with zero duration.
82 | - remove TODO from artifactory.gradle
83 | - upgrade dependencies
84 |
85 | ## 2023-03-07 - release 1.0.0
86 | - refactoring
87 | - upgrade to gradle 8
88 |
89 | ## 2022-05-31
90 | - first alpha version
91 |
92 |
--------------------------------------------------------------------------------
/src/test/resources/JPowerMonitorCfgTest.yaml:
--------------------------------------------------------------------------------
1 | # Number of initial calls to Libre Hardware Monitor for measuring the power consumption in idle mode (without running any tests)
2 | initCycles: 10
3 | # Sampling interval in milliseconds for the initialization period. This is the interval the data source for the sensor values is questioned for new values while measuring idle energy.
4 | # Should be set longer than the normal sampling interval! Too short intervals also affect the energy consumption!
5 | samplingIntervalForInitInMs: 1000
6 | # Calm down after each test for a few milliseconds: otherwise previous tests may interfere results of current test.
7 | calmDownIntervalInMs: 1000
8 | # The percentage of samples to discard from the beginning of measurement series: e.g. if 100 samples were taken and this value is set to 8, then the first 8 samples are not considered.
9 | percentageOfSamplesAtBeginningToDiscard: 20
10 | # Sampling interval in milliseconds. This is the interval the data source for the sensor values is questioned for new values.
11 | # Too short intervals also affect the energy consumption!
12 | samplingIntervalInMs: 300
13 | #
14 | measurement:
15 | # Specify which measurement method to use. Possible values: lhm, csv
16 | method: 'lhm'
17 | # Configuration for reading from csv file. E.g. output from HWInfo
18 | csv:
19 | # Path to csv file to read measure values from
20 | inputFile: 'hwinfo-test.csv'
21 | # Which line in the csv input file contains the current measured values? The first or the last? This depends on the measurement tool. Possible value: first, last
22 | lineToRead: 'last'
23 | # Columns to read, index starts at 0.
24 | columns:
25 | - { index: 2, name: 'CPU Power', energyInIdleMode: }
26 | # Encoding to use for reading the csv input file
27 | encoding: 'UTF-8'
28 | # Delimiter to use for separating the columns in the csv input file
29 | delimiter: ','
30 | # Configuration for reading from Libre Hardware Monitor
31 | lhm:
32 | # URL to Libre Hardware Monitor (** started in administrator mode **)
33 | url: 'http://localhost:8085'
34 | # The paths define the path to the leaf node underneath the root 'Sensor' node in Libre Hardware Monitor to access and store with every sample.
35 | # The more paths defined (no more than about 10), the greater the impact on power consumption, since the values must be extracted from the json data.
36 | paths:
37 | - { path: [ 'MSGN13205', 'Intel Core i7-9850H', 'Powers', 'CPU Package' ], energyInIdleMode: } # if energyInIdleMode is specified, it does not need to be measured before each test.
38 | #- { path: [ 'MSGN13205', 'Intel Core i7-9850H', 'Powers', 'CPU Cores' ], energyInIdleMode: 9.5 }
39 | #- { path: [ 'MSGN13205', 'Intel Core i7-9850H', 'Temperatures', 'CPU Core #1' ] } # no energyInIdleMode for temperatures...
40 | #- { path: [ 'MSGN16749', '11th Gen Intel Core i7-11850H', 'Powers', 'CPU Package' ], energyInIdleMode: }
41 | # ------------------------------------------------
42 | # Only JUnit Extension: Recording settings: (recordings have no effect on measured power consumption, as this is done after the test)
43 | csvRecording:
44 | # If specified, the results for every test are appended to a csv file.
45 | # On Windows: the file must not be opened in Excel in parallel!
46 | resultCsv: 'energyconsumption.csv'
47 | # If specified, all single measurements are recorded/appended in this csv.
48 | # On Windows: the file must not be opened in Excel in parallel!
49 | measurementCsv: 'measurement.csv'
50 | # ------------------------------------------------
51 | # Configuration for JavaAgent
52 | javaAgent:
53 | # Filter power and energy for methods starting with this packageFilter names
54 | packageFilter: [ 'com.msg', 'de.gillardon' ]
55 | # Energy measurement interval in milliseconds. This is the interval the data source for the sensor values is questioned for new values.
56 | # Too short intervals also affect the energy consumption!
57 | measurementIntervalInMs: 1000
58 | # Gather statistics interval in milliseconds. This is the interval the stacktrace of each active thread is questioned for active methods.
59 | # Too short intervals also affect the energy consumption!
60 | gatherStatisticsIntervalInMs: 100
61 | # Write energy measurement results to CSV files interval in seconds.
62 | writeEnergyMeasurementsToCsvIntervalInS: 20
63 |
--------------------------------------------------------------------------------
/src/test/java/group/msg/jpowermonitor/measurement/csv/CommaSeparatedValuesReaderTest.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.measurement.csv;
2 |
3 | import group.msg.jpowermonitor.config.DefaultCfgProvider;
4 | import group.msg.jpowermonitor.config.JPowerMonitorCfgProvider;
5 | import group.msg.jpowermonitor.config.dto.JPowerMonitorCfg;
6 | import group.msg.jpowermonitor.dto.DataPoint;
7 | import org.junit.jupiter.api.BeforeEach;
8 | import org.junit.jupiter.api.Test;
9 |
10 | import java.io.IOException;
11 | import java.nio.charset.StandardCharsets;
12 | import java.nio.file.Files;
13 | import java.nio.file.Path;
14 | import java.nio.file.Paths;
15 | import java.util.concurrent.TimeUnit;
16 | import java.util.stream.IntStream;
17 |
18 | import static org.assertj.core.api.Assertions.assertThat;
19 |
20 | class CommaSeparatedValuesReaderTest {
21 | @BeforeEach
22 | void setup() {
23 | DefaultCfgProvider.invalidateCachedConfig();
24 | }
25 |
26 | @Test
27 | void readMeasurementsFromFile() throws InterruptedException {
28 | JPowerMonitorCfgProvider configProvider = new DefaultCfgProvider();
29 | configProvider.readConfig("CommaSeparatedValuesReaderTest.yaml");
30 | JPowerMonitorCfg config = configProvider.getCachedConfig();
31 | CommaSeparatedValuesReader cmr = new CommaSeparatedValuesReader(config);
32 | for (int i = 0; i < 10; i++) {
33 | DataPoint dataPoint = cmr.measureFirstConfiguredPath();
34 | System.out.println(dataPoint);
35 | TimeUnit.MILLISECONDS.sleep(100);
36 | }
37 | }
38 |
39 | @Test
40 | void readFirstLine() throws IOException {
41 | JPowerMonitorCfgProvider configProvider = new DefaultCfgProvider();
42 | configProvider.readConfig("CommaSeparatedValuesReaderTest.yaml");
43 | JPowerMonitorCfg config = configProvider.getCachedConfig();
44 | CommaSeparatedValuesReader cmr = new CommaSeparatedValuesReader(config);
45 | String firstLine = cmr.readFirstLine(Paths.get("src/test/resources/hwinfo-test.csv"), StandardCharsets.UTF_8);
46 | assertThat(firstLine).isEqualTo("12.7.2022,18:7:10.680,6.352,0.061,24.733,76.0,363,84,581.376,46.206,");
47 | }
48 |
49 | @Test
50 | void readUmlautsFirstLine() throws IOException {
51 | JPowerMonitorCfgProvider configProvider = new DefaultCfgProvider();
52 | configProvider.readConfig("CommaSeparatedValuesReaderTest.yaml");
53 | JPowerMonitorCfg config = configProvider.getCachedConfig();
54 | CommaSeparatedValuesReader cmr = new CommaSeparatedValuesReader(config);
55 | String umlauts = Files.readAllLines(Path.of("src/test/resources/umlauts.txt"), StandardCharsets.UTF_8).get(0);
56 | String firstLine = cmr.readFirstLine(Paths.get("src/test/resources/firstLineLastLine-test.csv"), StandardCharsets.UTF_8);
57 | assertThat(firstLine).isEqualTo("MyFirstLine," + umlauts + ";WithUmlauts;");
58 | }
59 |
60 | @Test
61 | void readLastLine() throws IOException {
62 | JPowerMonitorCfgProvider configProvider = new DefaultCfgProvider();
63 | configProvider.readConfig("CommaSeparatedValuesReaderTest.yaml");
64 | JPowerMonitorCfg config = configProvider.getCachedConfig();
65 | CommaSeparatedValuesReader cmr = new CommaSeparatedValuesReader(config);
66 | String lastLine = cmr.readLastLine(Paths.get("src/test/resources/hwinfo-test.csv"), StandardCharsets.UTF_8);
67 | assertThat(lastLine).isEqualTo("12.7.2022,18:7:16.865,7.055,0.012,25.458,75.0,367,84,603.767,47.299,");
68 | }
69 |
70 | @Test
71 | void readVeryLongLastLine() throws IOException {
72 | String umlauts = Files.readAllLines(Path.of("src/test/resources/umlauts.txt"), StandardCharsets.UTF_8).get(0);
73 | StringBuilder expectedSb = new StringBuilder("My_Last_Line_With_10.000_Xes_At_The_End," + umlauts + ";WithUmlauts;");
74 | IntStream.range(0, 10_000).forEach(i -> expectedSb.append("X"));
75 | JPowerMonitorCfgProvider configProvider = new DefaultCfgProvider();
76 | configProvider.readConfig("CommaSeparatedValuesReaderTest.yaml");
77 | JPowerMonitorCfg config = configProvider.getCachedConfig();
78 | CommaSeparatedValuesReader cmr = new CommaSeparatedValuesReader(config);
79 | String lastLine = cmr.readLastLine(Paths.get("src/test/resources/firstLineLastLine-test.csv"), StandardCharsets.UTF_8);
80 | assertThat(lastLine).isEqualTo(expectedSb.toString());
81 | }
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/src/main/java/group/msg/jpowermonitor/config/dto/JPowerMonitorCfg.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.config.dto;
2 |
3 | import group.msg.jpowermonitor.JPowerMonitorException;
4 | import lombok.Data;
5 | import lombok.Getter;
6 |
7 | import java.util.Collections;
8 | import java.util.List;
9 | import java.util.Objects;
10 | import java.util.function.Consumer;
11 |
12 | import static group.msg.jpowermonitor.config.dto.MeasureMethodKey.CSV;
13 | import static group.msg.jpowermonitor.config.dto.MeasureMethodKey.EST;
14 | import static group.msg.jpowermonitor.config.dto.MeasureMethodKey.LHM;
15 |
16 | /**
17 | * Data class for jPowerMonitor configuration.
18 | * Includes all configuration values.
19 | *
20 | * @see MeasurementCfg
21 | * @see CsvRecordingCfg
22 | * @see JavaAgentCfg
23 | */
24 | @Data
25 | public class JPowerMonitorCfg {
26 | private Integer samplingIntervalInMs;
27 | private Integer samplingIntervalForInitInMs;
28 | private Integer initCycles;
29 | private Integer calmDownIntervalInMs;
30 | private Double percentageOfSamplesAtBeginningToDiscard;
31 | private Double carbonDioxideEmissionFactor;
32 | private MeasurementCfg measurement;
33 | private CsvRecordingCfg csvRecording;
34 | private JavaAgentCfg javaAgent = new JavaAgentCfg();
35 |
36 | // special case of cached constants
37 | @Getter
38 | private static Double co2EmissionFactor;
39 |
40 | public void initializeConfiguration() {
41 | if (measurement == null || measurement.getMethod() == null) {
42 | throw new JPowerMonitorException("A measuring method must be defined!");
43 | }
44 | MeasureMethodKey measureMethod = measurement.getMethodKey();
45 | if (LHM.equals(measureMethod)) {
46 | if (measurement.getLhm() == null || measurement.getLhm().getUrl() == null) {
47 | throw new JPowerMonitorException("Libre Hardware Monitor REST endpoint URL must be configured");
48 | }
49 | measurement.getLhm().setUrl(measurement.getLhm().getUrl() + "/data.json");
50 | List pathElems = measurement.getLhm().getPaths();
51 | if (pathElems == null
52 | || pathElems.isEmpty()
53 | || pathElems.get(0) == null
54 | || pathElems.get(0).getPath() == null
55 | || pathElems.get(0).getPath().isEmpty()) {
56 | throw new JPowerMonitorException("At least one path to a sensor value must be configured under paths");
57 | }
58 | } else if (CSV.equals(measureMethod)) {
59 | if (measurement.getCsv() == null || measurement.getCsv().getInputFile() == null || measurement.getCsv().getColumns() == null || measurement.getCsv().getColumns().isEmpty()) {
60 | throw new JPowerMonitorException("CSV input filepath and columns must be configured");
61 | }
62 | } else if (EST.equals(measureMethod)) {
63 | if (measurement.getEst() == null || measurement.getEst().getCpuMinWatts() == null || measurement.getEst().getCpuMaxWatts() == null) {
64 | throw new JPowerMonitorException("EST cpuMinWatts and cpuMaxWatts must be configured");
65 | }
66 | }
67 | setDefaultIfNotSet(samplingIntervalInMs, this::setSamplingIntervalInMs, 300);
68 | setDefaultIfNotSet(samplingIntervalForInitInMs, this::setSamplingIntervalForInitInMs, 1000);
69 | setDefaultIfNotSet(initCycles, this::setInitCycles, 10);
70 | setDefaultIfNotSet(calmDownIntervalInMs, this::setCalmDownIntervalInMs, 1000);
71 | setDefaultIfNotSet(percentageOfSamplesAtBeginningToDiscard, this::setPercentageOfSamplesAtBeginningToDiscard, 15.0);
72 | setDefaultIfNotSet(javaAgent.getMonitoring().getPrometheus().getHttpPort(), javaAgent.getMonitoring().getPrometheus()::setHttpPort, 1234);
73 | setDefaultIfNotSet(javaAgent.getMonitoring().getPrometheus().getWriteEnergyIntervalInS(), javaAgent.getMonitoring().getPrometheus()::setWriteEnergyIntervalInS, 30L);
74 |
75 | setCo2EmissionFactor(Objects.requireNonNullElse(carbonDioxideEmissionFactor, 485.0));
76 | setCarbonDioxideEmissionFactor(Objects.requireNonNullElse(carbonDioxideEmissionFactor, 485.0));
77 | javaAgent.setPackageFilter(Objects.requireNonNullElse(javaAgent.getPackageFilter(), Collections.emptySet()));
78 | }
79 |
80 | public static void setCo2EmissionFactor(Double carbonDioxideEmissionFactor) {
81 | JPowerMonitorCfg.co2EmissionFactor = carbonDioxideEmissionFactor;
82 | }
83 |
84 | private static void setDefaultIfNotSet(T currentValue, Consumer consumer, T defaultValue) {
85 | if (currentValue == null) {
86 | consumer.accept(defaultValue);
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/test/resources/replace-string-test/content-lf.txt:
--------------------------------------------------------------------------------
1 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
2 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
3 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
4 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
5 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
6 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
7 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros
8 | et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet,
9 | consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
10 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
11 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero
12 | eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.
13 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet,
14 | consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam,
15 | quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
16 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.
17 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
18 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
19 | sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem
20 | ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat,
21 | et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua.
22 | est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut
23 | labore et dolore magna aliquyam erat.
24 | Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
25 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
26 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
27 | sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem
28 | ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore
29 | magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus.
30 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
31 | sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est
32 | Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut
33 | labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd
34 | gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr,
35 | sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et
36 | justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
37 |
--------------------------------------------------------------------------------
/src/test/resources/replace-string-test/content-cr-lf.txt:
--------------------------------------------------------------------------------
1 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
2 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
3 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
4 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
5 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
6 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
7 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros
8 | et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet,
9 | consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
10 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
11 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero
12 | eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.
13 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet,
14 | consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam,
15 | quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
16 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.
17 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
18 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
19 | sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem
20 | ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat,
21 | et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua.
22 | est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut
23 | labore et dolore magna aliquyam erat.
24 | Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
25 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
26 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
27 | sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem
28 | ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore
29 | magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus.
30 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
31 | sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est
32 | Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut
33 | labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd
34 | gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr,
35 | sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et
36 | justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
37 |
--------------------------------------------------------------------------------
/gradle/license-normalizer-bundle.json:
--------------------------------------------------------------------------------
1 | {
2 | "bundles": [
3 | {
4 | "bundleName": "Apache-1.1",
5 | "licenseName": "Apache License, Version 1.1",
6 | "licenseUrl": "https://opensource.org/licenses/Apache-1.1"
7 | },
8 | {
9 | "bundleName": "Apache-2.0",
10 | "licenseName": "Apache License, Version 2.0",
11 | "licenseUrl": "https://opensource.org/licenses/Apache-2.0"
12 | },
13 | {
14 | "bundleName": "CDDL-1.0",
15 | "licenseName": "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE Version 1.0 (CDDL-1.0)",
16 | "licenseUrl": "http://opensource.org/licenses/CDDL-1.0"
17 | },
18 | {
19 | "bundleName": "CDDL-1.1",
20 | "licenseName": "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE Version 1.1 (CDDL-1.1)",
21 | "licenseUrl": "http://opensource.org/licenses/CDDL-1.1"
22 | },
23 | {
24 | "bundleName": "MPL-1.1",
25 | "licenseName": "Mozilla Public License Version 1.1",
26 | "licenseUrl": "https://opensource.org/licenses/MPL-1.1"
27 | },
28 | {
29 | "bundleName": "0BSD",
30 | "licenseName": "BSD 0-Clause License",
31 | "licenseUrl": "https://opensource.org/licenses/0BSD"
32 | },
33 | {
34 | "bundleName": "BSD-2-Clause",
35 | "licenseName": "BSD 2-Clause License",
36 | "licenseUrl": "https://opensource.org/licenses/BSD-2-Clause"
37 | },
38 | {
39 | "bundleName": "BSD-3-Clause",
40 | "licenseName": "BSD 3-Clause License",
41 | "licenseUrl": "https://opensource.org/licenses/BSD-3-Clause"
42 | },
43 | {
44 | "bundleName": "CC0-1.0",
45 | "licenseName": "PUBLIC DOMAIN",
46 | "licenseUrl": "http://creativecommons.org/publicdomain/zero/1.0/"
47 | }
48 | ],
49 | "transformationRules": [
50 | {
51 | "bundleName": "Apache-2.0",
52 | "licenseNamePattern": ".*The Apache Software License, Version 2.0.*"
53 | },
54 | {
55 | "bundleName": "0BSD",
56 | "licenseNamePattern": ".*BSD Zero Clause.*"
57 | },
58 | {
59 | "bundleName": "BSD-2-Clause",
60 | "licenseNamePattern": ".*2-Clause BSD.*"
61 | },
62 | {
63 | "bundleName": "BSD-3-Clause",
64 | "licenseNamePattern": ".*3-Clause BSD.*"
65 | },
66 | {
67 | "bundleName": "Apache-2.0",
68 | "licenseNamePattern": "Apache 2"
69 | },
70 | {
71 | "bundleName": "CDDL-1.1",
72 | "licenseNamePattern": "CDDL v1.1 / GPL v2 dual license"
73 | },
74 | {
75 | "bundleName": "CDDL-1.1",
76 | "licenseNamePattern": "CDDL/GPLv2+CE"
77 | },
78 | {
79 | "bundleName": "CDDL-1.0",
80 | "licenseNamePattern": "CDDL + GPLv2 with classpath exception"
81 | },
82 | {
83 | "bundleName": "CDDL-1.0",
84 | "licenseUrlPattern": "https://oss.oracle.com/licenses/CDDL"
85 | },
86 | {
87 | "bundleName": "CDDL-1.1",
88 | "licenseUrlPattern": "https://oss.oracle.com/licenses/CDDL-1.1"
89 | },
90 | {
91 | "bundleName": "Apache-2.0",
92 | "licenseUrlPattern": "http://www.apache.org/licenses/LICENSE-2.0.txt"
93 | },
94 | {
95 | "bundleName": "Apache-2.0",
96 | "modulePattern": "avalon-framework:avalon-framework-impl:4.2.0"
97 | },
98 | {
99 | "bundleName": "Apache-2.0",
100 | "modulePattern": "org.apache.ant:ant.*:1.7.1"
101 | },
102 | {
103 | "bundleName": "Apache-2.0",
104 | "modulePattern": "com.fasterxml.jackson:jackson.*:2.13.3"
105 | },
106 | {
107 | "bundleName": "Apache-1.1",
108 | "modulePattern": "commons-cli:commons-cli:1.0"
109 | },
110 | {
111 | "bundleName": "CDDL-1.0",
112 | "modulePattern": "javax.mail:mail:1.4.7"
113 | },
114 | {
115 | "bundleName": "MPL-1.1",
116 | "modulePattern": "com.lowagie:itext:2.1.5"
117 | },
118 | {
119 | "bundleName": "Apache-2.0",
120 | "modulePattern": "xalan:xalan:2.6.0"
121 | },
122 | {
123 | "bundleName": "Apache-2.0",
124 | "modulePattern": "xml-apis:xml-apis:.*"
125 | },
126 | {
127 | "bundleName": "CC0-1.0",
128 | "modulePattern": "org.hdrhistogram:HdrHistogram:2.1.12"
129 | },
130 | {
131 | "bundleName": "CC0-1.0",
132 | "modulePattern": "org.latencyutils:LatencyUtils:2.0.3"
133 | },
134 | {
135 | "bundleName": "msg.group",
136 | "modulePattern": "(com|group)\\.msg[.:].*"
137 | }
138 | ]
139 | }
140 |
--------------------------------------------------------------------------------
/src/main/java/group/msg/jpowermonitor/agent/export/csv/CsvResultsWriter.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.agent.export.csv;
2 |
3 | import group.msg.jpowermonitor.agent.Unit;
4 | import group.msg.jpowermonitor.agent.export.ResultsWriter;
5 | import group.msg.jpowermonitor.dto.DataPoint;
6 | import lombok.Getter;
7 | import lombok.extern.slf4j.Slf4j;
8 | import org.jetbrains.annotations.NotNull;
9 |
10 | import java.io.BufferedWriter;
11 | import java.io.FileWriter;
12 | import java.io.IOException;
13 | import java.util.Locale;
14 | import java.util.Map;
15 |
16 | import static group.msg.jpowermonitor.util.Constants.APP_TITLE;
17 | import static group.msg.jpowermonitor.util.Constants.DATE_TIME_FORMATTER;
18 | import static group.msg.jpowermonitor.util.Constants.DECIMAL_FORMAT;
19 | import static group.msg.jpowermonitor.util.Constants.NEW_LINE;
20 |
21 | /**
22 | * Write power and energy measurement results to CSV files at application shutdown.
23 | *
24 | * @author deinerj
25 | */
26 | @Slf4j
27 | @Getter
28 | public class CsvResultsWriter implements ResultsWriter {
29 | protected static final String FILE_NAME_PREFIX = APP_TITLE + "_";
30 |
31 | private static final String dataPointFormatCsv;
32 | private static final String dataPointFormatEnergyConsumptionCsv;
33 |
34 | static {
35 | dataPointFormatCsv = Locale.getDefault().getCountry().toLowerCase(Locale.ROOT).equals("de") ? "%s;%s;%s;%s;%s%s" : "%s,%s,%s,%s,%s%s";
36 | dataPointFormatEnergyConsumptionCsv = Locale.getDefault().getCountry().toLowerCase(Locale.ROOT).equals("de") ? "%s;%s;%s;%s;%s;%s;%s%s" : "%s,%s,%s,%s,%s,%s,%s%s";
37 | }
38 |
39 | private final String energyConsumptionPerMethodFileName;
40 | private final String energyConsumptionPerFilteredMethodFileName;
41 | private final String powerConsumptionPerMethodFileName;
42 | private final String powerConsumptionPerFilteredMethodFileName;
43 |
44 | /**
45 | * Constructor
46 | */
47 | public CsvResultsWriter() {
48 | long pid = ProcessHandle.current().pid();
49 | this.energyConsumptionPerMethodFileName = FILE_NAME_PREFIX + pid + "_energy_per_method.csv";
50 | this.energyConsumptionPerFilteredMethodFileName = FILE_NAME_PREFIX + pid + "_energy_per_method_filtered.csv";
51 | this.powerConsumptionPerMethodFileName = FILE_NAME_PREFIX + pid + "_power_per_method.csv";
52 | this.powerConsumptionPerFilteredMethodFileName = FILE_NAME_PREFIX + pid + "_power_per_method_filtered.csv";
53 | log.debug("Energy consumption per method is written to '{}'", energyConsumptionPerMethodFileName);
54 | log.debug("Energy consumption per filtered methods is written to '{}'", energyConsumptionPerFilteredMethodFileName);
55 | }
56 |
57 | @Override
58 | public void writePowerConsumptionPerMethod(Map measurements) {
59 | writeToFile(createCsv(measurements), powerConsumptionPerMethodFileName, true);
60 | }
61 |
62 | @Override
63 | public void writePowerConsumptionPerMethodFiltered(Map measurements) {
64 | writeToFile(createCsv(measurements), powerConsumptionPerFilteredMethodFileName, true);
65 | }
66 |
67 | @Override
68 | public void writeEnergyConsumptionPerMethod(Map measurements) {
69 | writeToFile(createCsv(measurements), energyConsumptionPerMethodFileName, false);
70 | }
71 |
72 | @Override
73 | public void writeEnergyConsumptionPerMethodFiltered(Map measurements) {
74 | writeToFile(createCsv(measurements), energyConsumptionPerFilteredMethodFileName, false);
75 | }
76 |
77 | protected String createCsv(Map measurements) {
78 | StringBuilder csv = new StringBuilder();
79 | measurements.forEach((method, energy) -> csv.append(createCsvEntryForDataPoint(energy)));
80 | return csv.toString();
81 | }
82 |
83 | protected String createCsvEntryForDataPoint(@NotNull DataPoint dp) {
84 | if (Unit.JOULE == dp.getUnit()) {
85 | return String.format(dataPointFormatEnergyConsumptionCsv,
86 | DATE_TIME_FORMATTER.format(dp.getTime()),
87 | dp.getThreadName(),
88 | dp.getName(),
89 | DECIMAL_FORMAT.format(dp.getValue()),
90 | dp.getUnit(),
91 | DECIMAL_FORMAT.format(dp.getCo2Value()),
92 | Unit.GRAMS_CO2.getAbbreviation(),
93 | NEW_LINE);
94 | }
95 | return String.format(dataPointFormatCsv,
96 | DATE_TIME_FORMATTER.format(dp.getTime()),
97 | dp.getThreadName(),
98 | dp.getName(),
99 | DECIMAL_FORMAT.format(dp.getValue()),
100 | dp.getUnit(),
101 | NEW_LINE);
102 | }
103 |
104 | protected void writeToFile(String csv, String fileName, boolean append) {
105 | try (BufferedWriter bw = new BufferedWriter(new FileWriter(fileName, append))) {
106 | bw.write(csv);
107 | } catch (IOException ex) {
108 | log.error(ex.getMessage(), ex);
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/test/java/group/msg/jpowermonitor/config/JPowerMonitorCfgTest.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.config;
2 |
3 | import group.msg.jpowermonitor.CfgProviderForTests;
4 | import group.msg.jpowermonitor.JPowerMonitorException;
5 | import group.msg.jpowermonitor.config.dto.JPowerMonitorCfg;
6 | import group.msg.jpowermonitor.config.dto.LibreHardwareMonitorCfg;
7 | import group.msg.jpowermonitor.config.dto.MeasurementCfg;
8 | import group.msg.jpowermonitor.config.dto.PathElementCfg;
9 | import org.junit.jupiter.api.Test;
10 |
11 | import java.util.List;
12 | import java.util.Set;
13 |
14 | import static org.assertj.core.api.Assertions.assertThat;
15 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
16 |
17 | public class JPowerMonitorCfgTest {
18 |
19 | @Test
20 | public void initialization_noHWGroup() {
21 | JPowerMonitorCfg config = new JPowerMonitorCfg();
22 | assertThatThrownBy(config::initializeConfiguration).isInstanceOf(JPowerMonitorException.class);
23 | }
24 |
25 | @Test
26 | public void initialization_noUrl() {
27 | JPowerMonitorCfg config = new JPowerMonitorCfg();
28 | MeasurementCfg measurement = new MeasurementCfg();
29 | measurement.setMethod("lhm");
30 | measurement.setLhm(new LibreHardwareMonitorCfg());
31 | config.setMeasurement(measurement);
32 | assertThatThrownBy(config::initializeConfiguration).isInstanceOf(JPowerMonitorException.class);
33 | }
34 |
35 | @Test
36 | public void initialization_noPath() {
37 | LibreHardwareMonitorCfg lhmConfig = new LibreHardwareMonitorCfg();
38 | lhmConfig.setUrl("some.url");
39 | lhmConfig.setPaths(List.of(new PathElementCfg()));
40 | JPowerMonitorCfg config = new JPowerMonitorCfg();
41 | MeasurementCfg measurement = new MeasurementCfg();
42 | measurement.setMethod("lhm");
43 | measurement.setLhm(lhmConfig);
44 | config.setMeasurement(measurement);
45 | assertThatThrownBy(config::initializeConfiguration).isInstanceOf(JPowerMonitorException.class);
46 | }
47 |
48 | @Test
49 | public void initialization_urlPreparation() {
50 | PathElementCfg path = new PathElementCfg();
51 | path.setPath(List.of("path"));
52 | LibreHardwareMonitorCfg lhmConfig = new LibreHardwareMonitorCfg();
53 | lhmConfig.setUrl("some.url");
54 | lhmConfig.setPaths(List.of(path));
55 | JPowerMonitorCfg config = new JPowerMonitorCfg();
56 | MeasurementCfg measurement = new MeasurementCfg();
57 | measurement.setMethod("lhm");
58 | measurement.setLhm(lhmConfig);
59 | config.setMeasurement(measurement);
60 | config.initializeConfiguration();
61 | assertThat(config.getMeasurement().getLhm().getUrl()).isEqualTo("some.url/data.json");
62 | }
63 |
64 | @Test
65 | public void initialization_defaultValues() {
66 | PathElementCfg path = new PathElementCfg();
67 | path.setPath(List.of("path"));
68 | LibreHardwareMonitorCfg lhmConfig = new LibreHardwareMonitorCfg();
69 | lhmConfig.setUrl("some.url");
70 | lhmConfig.setPaths(List.of(path));
71 | JPowerMonitorCfg config = new JPowerMonitorCfg();
72 | MeasurementCfg measurement = new MeasurementCfg();
73 | measurement.setMethod("lhm");
74 | measurement.setLhm(lhmConfig);
75 | config.setMeasurement(measurement);
76 | config.initializeConfiguration();
77 |
78 | assertThat(config.getSamplingIntervalInMs()).isEqualTo(300);
79 | assertThat(config.getSamplingIntervalForInitInMs()).isEqualTo(1000);
80 | assertThat(config.getInitCycles()).isEqualTo(10);
81 | assertThat(config.getCalmDownIntervalInMs()).isEqualTo(1000);
82 | assertThat(config.getPercentageOfSamplesAtBeginningToDiscard()).isEqualTo(15.0);
83 | assertThat(config.getJavaAgent()).isNotNull();
84 | assertThat(config.getJavaAgent().getPackageFilter()).isNotNull();
85 | assertThat(config.getJavaAgent().getPackageFilter().isEmpty()).isTrue();
86 | assertThat(config.getJavaAgent().getMeasurementIntervalInMs()).isEqualTo(0L);
87 | assertThat(config.getJavaAgent().getGatherStatisticsIntervalInMs()).isEqualTo(0L);
88 | assertThat(config.getJavaAgent().getWriteEnergyMeasurementsToCsvIntervalInS()).isEqualTo(0L);
89 | }
90 |
91 | @Test
92 | public void testFilterSet() {
93 | JPowerMonitorCfg config = new CfgProviderForTests().readConfig(getClass());
94 | Set packageFilter = config.getJavaAgent().getPackageFilter();
95 | assertThat(packageFilter.contains("com.msg")).isTrue();
96 | assertThat(packageFilter.contains("de.gillardon")).isTrue();
97 | }
98 |
99 | @Test
100 | public void testMeasurementInterval() {
101 | JPowerMonitorCfg config = new CfgProviderForTests().readConfig(getClass());
102 | long measurementIntervalInMs = config.getJavaAgent().getMeasurementIntervalInMs();
103 | assertThat(measurementIntervalInMs).isEqualTo(1000L);
104 | }
105 |
106 | @Test
107 | public void testGatherStatisticsIntervalInMsInterval() {
108 | JPowerMonitorCfg config = new CfgProviderForTests().readConfig(getClass());
109 | long gatherStatisticsIntervalInMs = config.getJavaAgent().getGatherStatisticsIntervalInMs();
110 | assertThat(gatherStatisticsIntervalInMs).isEqualTo(100L);
111 | }
112 |
113 | @Test
114 | public void testWriteEnergyMeasurementsToCsvIntervalInS() {
115 | JPowerMonitorCfg config = new CfgProviderForTests().readConfig(getClass());
116 | long writeEnergyMeasurementsToCsvIntervalInS = config.getJavaAgent()
117 | .getWriteEnergyMeasurementsToCsvIntervalInS();
118 | assertThat(writeEnergyMeasurementsToCsvIntervalInS).isEqualTo(20L);
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/test/resources/JPowerMonitorAgentTest.yaml:
--------------------------------------------------------------------------------
1 | # Number of initial calls to Libre Hardware Monitor for measuring the power consumption in idle mode (without running any tests)
2 | initCycles: 10
3 | # Sampling interval in milliseconds for the initialization period. This is the interval the data source for the sensor values is questioned for new values while measuring idle energy.
4 | # Should be set longer than the normal sampling interval! Too short intervals also affect the energy consumption!
5 | samplingIntervalForInitInMs: 1000
6 | # Calm down after each test for a few milliseconds: otherwise previous tests may interfere results of current test.
7 | calmDownIntervalInMs: 1000
8 | # The percentage of samples to discard from the beginning of measurement series: e.g. if 100 samples were taken and this value is set to 8, then the first 8 samples are not considered.
9 | percentageOfSamplesAtBeginningToDiscard: 20
10 | # Sampling interval in milliseconds. This is the interval the data source for the sensor values is questioned for new values.
11 | # Too short intervals also affect the energy consumption!
12 | samplingIntervalInMs: 300
13 | # Conversion factor to calculate approximated CO2 consumption in grams from energy consumption per kWh.
14 | # Depends on the energy mix of your location, for Germany compare e.g. https://www.umweltbundesamt.de/themen/klima-energie/energieversorgung/strom-waermeversorgung-in-zahlen#Strommix
15 | # Value for year 2022: 498
16 | carbonDioxideEmissionFactor: 498
17 |
18 | measurement:
19 | # Specify which measurement method to use. Possible values: lhm, csv, est
20 | method: 'est'
21 | # Configuration for reading from csv file. E.g. output from HWInfo
22 | csv:
23 | # Path to csv file to read measure values from
24 | inputFile: 'hwinfo.csv'
25 | # Which line in the csv input file contains the current measured values? The first or the last? This depends on the measurement tool. Possible value: first, last
26 | lineToRead: 'last'
27 | # Columns to read, index starts at 0.
28 | columns:
29 | - { index: 95, name: 'CPU Package Power [W]', energyInIdleMode: }
30 | # Encoding to use for reading the csv input file
31 | encoding: 'UTF-8'
32 | # Delimiter to use for separating the columns in the csv input file
33 | delimiter: ','
34 | # Configuration for reading from Libre Hardware Monitor
35 | lhm:
36 | # URL to Libre Hardware Monitor (** started in administrator mode **)
37 | url: 'http://localhost:8085'
38 | # The paths define the path to the leaf node underneath the root 'Sensor' node in Libre Hardware Monitor to access and store with every sample.
39 | # The more paths defined (no more than about 10), the greater the impact on power consumption, since the values must be extracted from the json data.
40 | paths:
41 | - { path: [ 'MSGN13205', 'Intel Core i7-9850H', 'Powers', 'CPU Package' ], energyInIdleMode: } # if energyInIdleMode is specified, it does not need to be measured before each test.
42 | #- { path: [ 'MSGN13205', 'Intel Core i7-9850H', 'Powers', 'CPU Cores' ], energyInIdleMode: 9.5 }
43 | #- { path: [ 'MSGN13205', 'Intel Core i7-9850H', 'Temperatures', 'CPU Core #1' ] } # no energyInIdleMode for temperatures...
44 | #- { path: [ 'MSGN16749', '11th Gen Intel Core i7-11850H', 'Powers', 'CPU Package' ], energyInIdleMode: }
45 | est:
46 | # Compare https://www.cloudcarbonfootprint.org/docs/methodology/#energy-estimate-watt-hours
47 | # Defaults are the average values from AWS: 0.74 - 3.5
48 | # Find the values for your VM here: https://github.com/cloud-carbon-footprint/cloud-carbon-coefficients/tree/main/data
49 | # or here: https://github.com/re-cinq/emissions-data/tree/main/data/v2
50 | # Determine AWS instance type in terminal: ´curl http://169.254.169.254/latest/meta-data/instance-type´
51 | cpuMinWatts: 10
52 | cpuMaxWatts: 100
53 |
54 | # ------------------------------------------------
55 | # Recording settings: (recordings have no effect on measured power consumption, as this is done after the test)
56 | csvRecording:
57 | # If specified, the results for every test are appended to a csv file.
58 | # On Windows: the file must not be opened in Excel in parallel!
59 | resultCsv: 'energyconsumption.csv'
60 | # If specified, all single measurements are recorded/appended in this csv.
61 | # On Windows: the file must not be opened in Excel in parallel!
62 | measurementCsv: 'measurement.csv'
63 | # ------------------------------------------------
64 | # Configuration for JavaAgent
65 | javaAgent:
66 | # Filter power and energy for methods starting with this packageFilter names
67 | packageFilter: [ 'group.msg', 'com.msg' ]
68 | # Energy measurement interval in milliseconds. This is the interval the data source for the sensor values is questioned for new values.
69 | # Too short intervals also affect the energy consumption!
70 | measurementIntervalInMs: 100
71 | # Gather statistics interval in milliseconds. This is the interval the stacktrace of each active thread is questioned for active methods.
72 | # Too short intervals also affect the energy consumption!
73 | gatherStatisticsIntervalInMs: 10
74 | # Write energy measurement results to CSV files interval in seconds.
75 | writeEnergyMeasurementsToCsvIntervalInS: 1
76 | # ------------------------------------------------
77 | # Configuration for Monitoring Interfaces
78 | # Current only implementation is prometheus.
79 | monitoring:
80 | # Prometheus configuration
81 | prometheus:
82 | # Enable sending metrics to prometheus.
83 | enabled: true
84 | # jPowerMonitor will open a http server port on this port for supplying the measurement data to prometheus.
85 | httpPort: 1234
86 | # Write energy measurement results to prometheus interval in seconds.
87 | writeEnergyIntervalInS: 30
88 | # Publish default Prometheus JVM Metrics. This includes information about GC, memory etc.
89 | publishJvmMetrics: false
90 |
--------------------------------------------------------------------------------
/src/main/resources/jpowermonitor-template.yaml:
--------------------------------------------------------------------------------
1 | # Number of initial calls to Libre Hardware Monitor for measuring the power consumption in idle mode (without running any tests)
2 | initCycles: 10
3 | # Sampling interval in milliseconds for the initialization period. This is the interval the data source for the sensor values is questioned for new values while measuring idle energy.
4 | # Should be set longer than the normal sampling interval! Too short intervals also affect the energy consumption!
5 | samplingIntervalForInitInMs: 1000
6 | # Calm down after each test for a few milliseconds: otherwise previous tests may interfere results of current test.
7 | calmDownIntervalInMs: 1000
8 | # The percentage of samples to discard from the beginning of measurement series: e.g. if 100 samples were taken and this value is set to 8, then the first 8 samples are not considered.
9 | percentageOfSamplesAtBeginningToDiscard: 20
10 | # Sampling interval in milliseconds. This is the interval the data source for the sensor values is questioned for new values.
11 | # Too short intervals also affect the energy consumption!
12 | samplingIntervalInMs: 300
13 | # Conversion factor to calculate approximated CO2 consumption in grams from energy consumption per kWh.
14 | # Depends on the energy mix of your location, for Germany compare e.g. https://www.umweltbundesamt.de/themen/klima-energie/energieversorgung/strom-waermeversorgung-in-zahlen#Strommix
15 | # Value for year 2022: 498
16 | carbonDioxideEmissionFactor: 498
17 |
18 | measurement:
19 | # Specify which measurement method to use. Possible values: lhm, csv, est
20 | method: 'lhm'
21 | # Configuration for reading from csv file. E.g. output from HWInfo
22 | csv:
23 | # Path to csv file to read measure values from
24 | inputFile: 'hwinfo.csv'
25 | # Which line in the csv input file contains the current measured values? The first or the last? This depends on the measurement tool. Possible value: first, last
26 | lineToRead: 'last'
27 | # Columns to read, index starts at 0.
28 | columns:
29 | - { index: 95, name: 'CPU Package Power [W]', energyInIdleMode: }
30 | # Encoding to use for reading the csv input file
31 | encoding: 'UTF-8'
32 | # Delimiter to use for separating the columns in the csv input file
33 | delimiter: ','
34 | # Configuration for reading from Libre Hardware Monitor
35 | lhm:
36 | # URL to Libre Hardware Monitor (** started in administrator mode **)
37 | url: 'http://localhost:8085'
38 | # The paths define the path to the leaf node underneath the root 'Sensor' node in Libre Hardware Monitor to access and store with every sample.
39 | # The more paths defined (no more than about 10), the greater the impact on power consumption, since the values must be extracted from the json data.
40 | paths:
41 | - { path: [ 'MSGN13205', 'Intel Core i7-9850H', 'Powers', 'CPU Package' ], energyInIdleMode: } # if energyInIdleMode is specified, it does not need to be measured before each test.
42 | #- { path: [ 'MSGN13205', 'Intel Core i7-9850H', 'Powers', 'CPU Cores' ], energyInIdleMode: 9.5 }
43 | #- { path: [ 'MSGN13205', 'Intel Core i7-9850H', 'Temperatures', 'CPU Core #1' ] } # no energyInIdleMode for temperatures...
44 | #- { path: [ 'MSGN16749', '11th Gen Intel Core i7-11850H', 'Powers', 'CPU Package' ], energyInIdleMode: }
45 | est:
46 | # Compare https://www.cloudcarbonfootprint.org/docs/methodology/#energy-estimate-watt-hours
47 | # Defaults are the average values from AWS: 0.74 - 3.5
48 | # Find the values for your VM here: https://github.com/cloud-carbon-footprint/cloud-carbon-coefficients/tree/main/data
49 | # or here: https://github.com/re-cinq/emissions-data/tree/main/data/v2
50 | # Determine AWS instance type in terminal: ´curl http://169.254.169.254/latest/meta-data/instance-type´
51 | cpuMinWatts: 0.74
52 | cpuMaxWatts: 3.5
53 |
54 | # ------------------------------------------------
55 | # Recording settings for JUnit Extension only: (recordings have no effect on measured power consumption, as this is done after the test)
56 | csvRecording:
57 | # If specified, the results for every test are appended to a csv file.
58 | # On Windows: the file must not be opened in Excel in parallel!
59 | resultCsv: 'energyconsumption.csv'
60 | # If specified, all single measurements are recorded/appended in this csv.
61 | # On Windows: the file must not be opened in Excel in parallel!
62 | measurementCsv: 'measurement.csv'
63 | # ------------------------------------------------
64 | # Configuration for JavaAgent
65 | javaAgent:
66 | # Filter power and energy for methods starting with this packageFilter names
67 | packageFilter: [ 'group.msg', 'de.gillardon' ]
68 | # Energy measurement interval in milliseconds. This is the interval the data source for the sensor values is questioned for new values.
69 | # Too short intervals also affect the energy consumption!
70 | measurementIntervalInMs: 1000
71 | # Gather statistics interval in milliseconds. This is the interval the stacktrace of each active thread is questioned for active methods.
72 | # Too short intervals also affect the energy consumption!
73 | gatherStatisticsIntervalInMs: 10
74 | # Write energy measurement results to CSV files interval in seconds.
75 | writeEnergyMeasurementsToCsvIntervalInS: 30
76 | # ------------------------------------------------
77 | # Configuration for Monitoring Interfaces
78 | # Current only implementation is prometheus.
79 | monitoring:
80 | # Prometheus configuration
81 | prometheus:
82 | # Enable sending metrics to prometheus.
83 | enabled: false
84 | # jPowerMonitor will open a http server port on this port for supplying the measurement data to prometheus.
85 | httpPort: 1234
86 | # Write energy measurement results to prometheus interval in seconds.
87 | writeEnergyIntervalInS: 30
88 | # Publish default Prometheus JVM Metrics. This includes information about GC, memory etc.
89 | publishJvmMetrics: false
90 |
--------------------------------------------------------------------------------
/src/main/java/group/msg/jpowermonitor/measurement/lhm/LibreHardwareMonitorReader.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.measurement.lhm;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import group.msg.jpowermonitor.JPowerMonitorException;
5 | import group.msg.jpowermonitor.MeasureMethod;
6 | import group.msg.jpowermonitor.agent.Unit;
7 | import group.msg.jpowermonitor.config.dto.JPowerMonitorCfg;
8 | import group.msg.jpowermonitor.config.dto.LibreHardwareMonitorCfg;
9 | import group.msg.jpowermonitor.config.dto.PathElementCfg;
10 | import group.msg.jpowermonitor.dto.DataPoint;
11 | import lombok.NonNull;
12 | import org.apache.hc.client5.http.classic.HttpClient;
13 | import org.apache.hc.client5.http.classic.methods.HttpGet;
14 | import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
15 | import org.apache.hc.core5.http.ClassicHttpResponse;
16 | import org.jetbrains.annotations.NotNull;
17 |
18 | import java.io.IOException;
19 | import java.time.LocalDateTime;
20 | import java.util.ArrayList;
21 | import java.util.HashMap;
22 | import java.util.List;
23 | import java.util.Map;
24 | import java.util.Objects;
25 | import java.util.stream.Collectors;
26 |
27 | /**
28 | * Implementation of the Libre Hardware Monitor measure method.
29 | *
30 | * @see MeasureMethod
31 | */
32 | public class LibreHardwareMonitorReader implements MeasureMethod {
33 | private final HttpClient client;
34 | private final LibreHardwareMonitorCfg lhmConfig;
35 |
36 | public LibreHardwareMonitorReader(JPowerMonitorCfg config) {
37 | Objects.requireNonNull(config.getMeasurement().getLhm(), "Libre Hardware Monitor config must be set!");
38 | this.lhmConfig = config.getMeasurement().getLhm();
39 | this.client = HttpClientBuilder.create().build();
40 | }
41 |
42 | @NotNull
43 | private List getDataPoints(ClassicHttpResponse response, List paths) throws IOException {
44 | LocalDateTime time = LocalDateTime.now();
45 | ObjectMapper objectMapper = new ObjectMapper();
46 | DataElem root = objectMapper.readValue(response.getEntity().getContent(), DataElem.class);
47 | List result = new ArrayList<>();
48 | for (PathElementCfg pathElement : paths) {
49 | DataPoint dp = createDataPoint(root, pathElement, time);
50 | result.add(dp);
51 | }
52 | return result;
53 | }
54 |
55 | @Override
56 | public @NotNull List measure() throws JPowerMonitorException {
57 | try {
58 | return client.execute(new HttpGet(lhmConfig.getUrl()), response -> getDataPoints(response, lhmConfig.getPaths()));
59 | } catch (IOException e) {
60 | throw new JPowerMonitorException("Unable to reach Libre Hardware Monitor at url: " + lhmConfig.getUrl() + "!", e);
61 | }
62 | }
63 |
64 | @NotNull
65 | private DataPoint getDataPoint(ClassicHttpResponse response, PathElementCfg pathElement) throws IOException {
66 | LocalDateTime time = LocalDateTime.now();
67 | ObjectMapper objectMapper = new ObjectMapper();
68 | DataElem root = objectMapper.readValue(response.getEntity().getContent(), DataElem.class);
69 | return createDataPoint(root, pathElement, time);
70 | }
71 |
72 | @Override
73 | public @NotNull DataPoint measureFirstConfiguredPath() throws JPowerMonitorException {
74 | try {
75 | // the config assures that getPaths is not null and has at least one element!
76 | return client.execute(new HttpGet(lhmConfig.getUrl()), response -> getDataPoint(response, lhmConfig.getPaths().get(0)));
77 | } catch (IOException e) {
78 | throw new JPowerMonitorException("Unable to reach Libre Hardware Monitor at url: " + lhmConfig.getUrl() + "!");
79 | }
80 | }
81 |
82 | @NotNull
83 | private DataPoint createDataPoint(DataElem root, PathElementCfg pathElement, LocalDateTime time) {
84 | DataElem elem = findElement(root, pathElement.getPath().toArray());
85 | if (elem == null) {
86 | throw new JPowerMonitorException("Unable to find element for path " + pathElement.getPath() + "!");
87 | }
88 | String[] valueAndUnit = elem.getValue().split("\\s+");// (( "5,4 W" ))
89 | Double value = Double.valueOf(valueAndUnit[0].replace(',', '.').trim());
90 | Unit unit = Unit.fromAbbreviation(valueAndUnit[1].trim());
91 | return new DataPoint(String.join("->", pathElement.getPath()), value, unit, time, null);
92 | }
93 |
94 | @Override
95 | public @NotNull List configuredSensors() {
96 | return lhmConfig.getPaths()
97 | .stream()
98 | .map(p -> String.join("->", p.getPath()))
99 | .collect(Collectors.toList());
100 | }
101 |
102 | @Override
103 | public @NotNull Map defaultEnergyInIdleModeForMeasuredSensors() {
104 | Map energyInIdleModeForMeasuredSensors = new HashMap<>();
105 | lhmConfig.getPaths().stream()
106 | .filter(x -> x.getEnergyInIdleMode() != null)
107 | .forEach(p -> energyInIdleModeForMeasuredSensors.put(String.join("->", p.getPath()), p.getEnergyInIdleMode()));
108 | return energyInIdleModeForMeasuredSensors;
109 | }
110 |
111 | private DataElem findElement(DataElem root, Object[] path) {
112 | return findElementInTree(root, path, (String) path[path.length - 1], 0);
113 | }
114 |
115 | private DataElem findElementInTree(@NonNull DataElem elem, Object[] parentNodes, String name, int level) {
116 | if (elem.getText().equals(name)) {
117 | return elem;
118 | }
119 | DataElem result;
120 | for (DataElem child : elem.children) {
121 | if (parentNodes[level].equals(child.getText())) {
122 | result = findElementInTree(child, parentNodes, name, ++level);
123 | if (result != null) {
124 | return result;
125 | }
126 | }
127 | }
128 | return null;
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/test/java/group/msg/jpowermonitor/agent/PowerMeasurementCfgCollectorTest.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.agent;
2 |
3 | import group.msg.jpowermonitor.config.dto.JavaAgentCfg;
4 | import group.msg.jpowermonitor.config.dto.MonitoringCfg;
5 | import group.msg.jpowermonitor.dto.Activity;
6 | import group.msg.jpowermonitor.dto.DataPoint;
7 | import group.msg.jpowermonitor.dto.MethodActivity;
8 | import group.msg.jpowermonitor.dto.Quantity;
9 | import org.junit.jupiter.api.Test;
10 |
11 | import java.time.LocalDateTime;
12 | import java.util.HashSet;
13 | import java.util.List;
14 | import java.util.Map;
15 |
16 | import static org.assertj.core.api.Assertions.assertThat;
17 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
18 | import static org.junit.jupiter.api.Assertions.assertEquals;
19 | import static org.junit.jupiter.api.Assertions.assertNotEquals;
20 | import static org.junit.jupiter.api.Assertions.assertTrue;
21 |
22 | class PowerMeasurementCfgCollectorTest {
23 |
24 | private static final DataPoint DP1 = new DataPoint("x", 0.0, Unit.WATT, LocalDateTime.now(), null);
25 | private static final DataPoint DP2 = new DataPoint("y", 1.0, Unit.WATT, LocalDateTime.now(), null);
26 |
27 | @Test
28 | void areAddableTest() {
29 | JavaAgentCfg javaAgentCfg = new JavaAgentCfg(new HashSet<>(), 0, 0, 0, new MonitoringCfg());
30 | assertTrue(new PowerMeasurementCollector(0, null, javaAgentCfg).areDataPointsAddable(DP1, DP2));
31 | }
32 |
33 | @SuppressWarnings("ConstantConditions")
34 | @Test
35 | void areNotAddableFailBecauseOfValueNullTest() {
36 | JavaAgentCfg javaAgentCfg = new JavaAgentCfg(new HashSet<>(), 0, 0, 0, new MonitoringCfg());
37 | PowerMeasurementCollector testee = new PowerMeasurementCollector(0L, null, javaAgentCfg);
38 | assertThatThrownBy(() -> testee.areDataPointsAddable(DP1, null)).isInstanceOf(Exception.class);
39 | assertThatThrownBy(() -> testee.areDataPointsAddable(null, DP2)).isInstanceOf(Exception.class);
40 | }
41 |
42 | @Test
43 | void areNotAddableBecauseOfValueNullTest() {
44 | JavaAgentCfg javaAgentCfg = new JavaAgentCfg(new HashSet<>(), 0, 0, 0, new MonitoringCfg());
45 | PowerMeasurementCollector testee = new PowerMeasurementCollector(0L, null, javaAgentCfg);
46 |
47 | DataPoint dp2 = new DataPoint("y", null, Unit.WATT, LocalDateTime.now(), null);
48 | assertThat(testee.areDataPointsAddable(DP1, dp2)).isFalse();
49 | assertThat(testee.areDataPointsAddable(dp2, DP1)).isFalse();
50 | }
51 |
52 | @Test
53 | void areNotAddableBecauseOfUnitNullTest() {
54 | JavaAgentCfg javaAgentCfg = new JavaAgentCfg(new HashSet<>(), 0, 0, 0, new MonitoringCfg());
55 | PowerMeasurementCollector testee = new PowerMeasurementCollector(0L, null, javaAgentCfg);
56 | DataPoint dp2 = new DataPoint("y", 0.0, null, LocalDateTime.now(), null);
57 | assertThat(testee.areDataPointsAddable(DP1, dp2)).isFalse();
58 | assertThat(testee.areDataPointsAddable(dp2, DP1)).isFalse();
59 | }
60 |
61 | @Test
62 | void areNotAddableBecauseOfDifferentUnitsTest() {
63 | JavaAgentCfg javaAgentCfg = new JavaAgentCfg(new HashSet<>(), 0, 0, 0, new MonitoringCfg());
64 | PowerMeasurementCollector testee = new PowerMeasurementCollector(0L, null, javaAgentCfg);
65 | DataPoint dp2 = testee.cloneAndCalculateDataPoint(DP2, Unit.WATTHOURS, x -> x);
66 | assertThat(testee.areDataPointsAddable(DP1, dp2)).isFalse();
67 | }
68 |
69 | @Test
70 | void addTwoDataPointsTest() {
71 | JavaAgentCfg javaAgentCfg = new JavaAgentCfg(new HashSet<>(), 0, 0, 0, new MonitoringCfg());
72 | PowerMeasurementCollector testee = new PowerMeasurementCollector(0L, null, javaAgentCfg);
73 | DataPoint dpSum = testee.addDataPoint(DP1, DP2);
74 | assertThat(dpSum.getValue()).isEqualTo(DP1.getValue() + DP2.getValue());
75 | }
76 |
77 | @Test
78 | void addMultipleDataPointsTest() {
79 | JavaAgentCfg javaAgentCfg = new JavaAgentCfg(new HashSet<>(), 0, 0, 0, new MonitoringCfg());
80 | PowerMeasurementCollector testee = new PowerMeasurementCollector(0L, null, javaAgentCfg);
81 | DataPoint dp3 = new DataPoint("x", 10.0, Unit.WATT, LocalDateTime.now(), null);
82 | DataPoint dp4 = new DataPoint("x", 100.0, Unit.WATT, LocalDateTime.now(), null);
83 | DataPoint dpSum = testee.addDataPoint(DP1, DP2, dp3, dp4);
84 | assertThat(dpSum.getValue()).isEqualTo(111.0);
85 | }
86 |
87 | @Test
88 | void addMultipleDataPointsWithDifferentUnitsTest() {
89 | JavaAgentCfg javaAgentCfg = new JavaAgentCfg(new HashSet<>(), 0, 0, 0, new MonitoringCfg());
90 | PowerMeasurementCollector testee = new PowerMeasurementCollector(0L, null, javaAgentCfg);
91 | DataPoint dp3 = new DataPoint("x", 10.0, Unit.WATT, LocalDateTime.now(), null);
92 | DataPoint dp4 = new DataPoint("x", 100.0, Unit.WATTHOURS, LocalDateTime.now(), null);
93 | DataPoint dpSum = testee.addDataPoint(DP1, DP2, dp3, dp4);
94 | assertThat(dpSum.getValue()).isEqualTo(11.0);
95 | }
96 |
97 | @Test
98 | void cloneWithNewUnitTest() {
99 | JavaAgentCfg javaAgentCfg = new JavaAgentCfg(new HashSet<>(), 0, 0, 0, new MonitoringCfg());
100 | PowerMeasurementCollector testee = new PowerMeasurementCollector(0L, null, javaAgentCfg);
101 | DataPoint dp3 = testee.cloneAndCalculateDataPoint(DP1, Unit.WATTHOURS, x -> x);
102 | assertNotEquals(dp3, DP1);
103 | assertEquals(Unit.WATTHOURS, dp3.getUnit());
104 | }
105 |
106 | @Test
107 | void aggregateActivityTest() {
108 | JavaAgentCfg javaAgentCfg = new JavaAgentCfg(new HashSet<>(), 0, 0, 0, new MonitoringCfg());
109 | PowerMeasurementCollector testee = new PowerMeasurementCollector(0L, null, javaAgentCfg);
110 |
111 | MethodActivity ma1 = new MethodActivity();
112 | ma1.setMethodQualifier("no.filter.Method");
113 | ma1.setRepresentedQuantity(Quantity.of(0.0, Unit.JOULE));
114 |
115 | MethodActivity ma2 = new MethodActivity();
116 | ma2.setMethodQualifier("no.filter.method.Either");
117 | ma2.setRepresentedQuantity(Quantity.of(1.0, Unit.JOULE));
118 |
119 | MethodActivity ma3 = new MethodActivity();
120 | ma3.setFilteredMethodQualifier("a.filter.method");
121 | ma3.setRepresentedQuantity(Quantity.of(10.0, Unit.JOULE));
122 |
123 | List activities = List.of(ma1, ma2, ma3);
124 | Map unfiltered = testee.aggregateActivityToDataPoints(activities, false);
125 | assertEquals(3, unfiltered.size());
126 |
127 | Map filtered = testee.aggregateActivityToDataPoints(activities, true);
128 | assertEquals(1, filtered.size());
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/main/java/group/msg/jpowermonitor/config/DefaultCfgProvider.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.config;
2 |
3 | import group.msg.jpowermonitor.JPowerMonitorException;
4 | import group.msg.jpowermonitor.agent.JPowerMonitorAgent;
5 | import group.msg.jpowermonitor.config.dto.JPowerMonitorCfg;
6 | import lombok.extern.slf4j.Slf4j;
7 | import org.jetbrains.annotations.NotNull;
8 | import org.yaml.snakeyaml.Yaml;
9 |
10 | import java.io.IOException;
11 | import java.io.InputStream;
12 | import java.io.Reader;
13 | import java.nio.charset.Charset;
14 | import java.nio.charset.StandardCharsets;
15 | import java.nio.file.Files;
16 | import java.nio.file.Path;
17 | import java.nio.file.Paths;
18 | import java.util.Objects;
19 | import java.util.function.Supplier;
20 | import java.util.stream.Stream;
21 |
22 | import static group.msg.jpowermonitor.util.Constants.APP_TITLE;
23 |
24 | /**
25 | * Default configuration provider preferring file system to resources.
26 | *
27 | * This configuration provider uses caching (e.g. reads only once per provider instance) and reads
28 | * the configuration as a YAML file (see resources/jpowermonitor-template.yaml for
29 | * example). In order to find a configuration, it uses the following sequence:
30 | *
31 | * - If a source is given, try reading from the file system.
32 | * - If file system fails (for any reason), try reading with source from the resources.
33 | * - If no source is given (or couldn't be read), fall back to using
34 | *
jpowermonitor.yaml (see {@link #DEFAULT_CONFIG}).
35 | * - Try finding that default source in the file system.
36 | * - If that fails, try finding it in the resources.
37 | * - If nothing was found, throw an exception.
38 | *
39 | */
40 | @Slf4j
41 | public class DefaultCfgProvider implements JPowerMonitorCfgProvider {
42 | private static final String DEFAULT_CONFIG = APP_TITLE + ".yaml";
43 | private final Charset yamlFileEncoding;
44 | private static JPowerMonitorCfg cachedConfig = null;
45 |
46 | public DefaultCfgProvider() {
47 | this.yamlFileEncoding = StandardCharsets.UTF_8;
48 | }
49 |
50 | @Override
51 | public synchronized JPowerMonitorCfg getCachedConfig() throws JPowerMonitorException {
52 | if (cachedConfig == null) {
53 | cachedConfig = acquireConfigFromSource(DEFAULT_CONFIG);
54 | }
55 | return cachedConfig;
56 | }
57 |
58 | @Override
59 | public synchronized JPowerMonitorCfg readConfig(String source) throws JPowerMonitorException {
60 | if (cachedConfig == null) {
61 | cachedConfig = acquireConfigFromSource(isValidSource(source) ? source : DEFAULT_CONFIG);
62 | }
63 | return cachedConfig;
64 | }
65 |
66 | @NotNull
67 | private JPowerMonitorCfg acquireConfigFromSource(String source) {
68 | JPowerMonitorCfg cfg = Stream.of(
69 | (Supplier) () -> this.tryReadingFromFileSystem(source),
70 | () -> this.tryReadingFromResources(source),
71 | () -> {
72 | Path conf = findFileIgnoringCase(Path.of("."), DEFAULT_CONFIG);
73 | return this.readConfigFromPath(conf);
74 | })
75 | .map(Supplier::get)
76 | .filter(Objects::nonNull)
77 | .findFirst()
78 | .orElseThrow(() -> new JPowerMonitorException(String.format("Unable to read %s configuration from source '%s'", APP_TITLE, source)));
79 | cfg.initializeConfiguration();
80 | return cfg;
81 | }
82 |
83 | public Path findFileIgnoringCase(Path path, String fileName) {
84 | log.info("Reading {} configuration from given source '{}' on path {}", APP_TITLE, fileName, path);
85 | if (!JPowerMonitorAgent.isSlf4jLoggerImplPresent()) {
86 | System.out.println("Reading " + APP_TITLE + " configuration from given source '" + fileName + "' on path " + path);
87 | }
88 | if (!Files.isDirectory(path)) {
89 | throw new IllegalArgumentException("Path must be a directory!");
90 | }
91 | try (Stream walk = Files.walk(path)) {
92 | return walk
93 | .filter(Files::isReadable)
94 | .filter(Files::isRegularFile)
95 | .filter(p -> p.getFileName().toString().equalsIgnoreCase(fileName)).findFirst().orElse(null);
96 | } catch (IOException e) {
97 | return null;
98 | }
99 | }
100 |
101 | private JPowerMonitorCfg tryReadingFromFileSystem(String source) {
102 | log.info("Reading {} configuration from filesystem: '{}'", APP_TITLE, source);
103 | Path path = Paths.get(source);
104 | if (!Files.isRegularFile(path)) {
105 | log.error("'{}' is not a regular file, it will not be read from filesystem", source);
106 | return null;
107 | }
108 | return readConfigFromPath(path);
109 | }
110 |
111 | private JPowerMonitorCfg readConfigFromPath(Path path) {
112 | try (Reader reader = Files.newBufferedReader(path, yamlFileEncoding)) {
113 | return new Yaml().loadAs(reader, JPowerMonitorCfg.class);
114 | } catch (Exception e) {
115 | log.error("Cannot read '{}' from filesystem: {}", path, e.getMessage());
116 | if (!JPowerMonitorAgent.isSlf4jLoggerImplPresent()) {
117 | System.err.println("Cannot read '" + path + "' from filesystem: " + e.getMessage());
118 | }
119 | }
120 | return null;
121 | }
122 |
123 | private JPowerMonitorCfg tryReadingFromResources(String source) {
124 | log.info("Reading {} configuration from resources: {}", APP_TITLE, source);
125 | if (DefaultCfgProvider.class.getClassLoader().getResource(source) == null) {
126 | log.info("'{}' is not available as resource", source);
127 | return null;
128 | }
129 | return readConfigFromResource(source);
130 | }
131 |
132 | private JPowerMonitorCfg readConfigFromResource(String source) {
133 | try (InputStream input = DefaultCfgProvider.class.getClassLoader().getResourceAsStream(source)) {
134 | return new Yaml().loadAs(input, JPowerMonitorCfg.class);
135 | } catch (Exception e) {
136 | log.error("Cannot read '{}' from resources: {}", source, e.getMessage());
137 | if (!JPowerMonitorAgent.isSlf4jLoggerImplPresent()) {
138 | System.err.println("Cannot read '" + source + "' from resources: " + e.getMessage());
139 | }
140 | }
141 | return null;
142 | }
143 |
144 | /**
145 | * For testing need to invalidate the static internally cached config in order to re-read it.
146 | */
147 | public static void invalidateCachedConfig() {
148 | cachedConfig = null;
149 | }
150 |
151 | }
152 |
--------------------------------------------------------------------------------
/src/main/java/group/msg/jpowermonitor/agent/export/prometheus/PrometheusWriter.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.agent.export.prometheus;
2 |
3 | import group.msg.jpowermonitor.agent.export.ResultsWriter;
4 | import group.msg.jpowermonitor.config.dto.PrometheusCfg;
5 | import group.msg.jpowermonitor.dto.DataPoint;
6 | import io.prometheus.client.Gauge;
7 | import io.prometheus.client.exporter.HTTPServer;
8 | import io.prometheus.client.hotspot.DefaultExports;
9 | import lombok.extern.slf4j.Slf4j;
10 |
11 | import java.io.IOException;
12 | import java.util.HashMap;
13 | import java.util.Map;
14 | import java.util.concurrent.ConcurrentHashMap;
15 | import java.util.concurrent.locks.Lock;
16 | import java.util.concurrent.locks.ReentrantLock;
17 | import java.util.function.Function;
18 |
19 | import static group.msg.jpowermonitor.util.Constants.APP_TITLE;
20 |
21 | /**
22 | * Prometheus Writer to write energy, co2 and power per filtered method to prometheus.
23 | */
24 | @Slf4j
25 | public class PrometheusWriter implements ResultsWriter {
26 | protected static final String METRICS_PREFIX = APP_TITLE + "_";
27 | private static final String ENERGY_CONSUMPTION_PER_FILTERED_METHOD_METRIC_NAME = METRICS_PREFIX + "energy_per_method_filtered";
28 | private static final String CO2_CONSUMPTION_PER_FILTERED_METHOD_METRIC_NAME = METRICS_PREFIX + "co2_per_method_filtered";
29 | private static final String POWER_CONSUMPTION_PER_FILTERED_METHOD_METRIC_NAME = METRICS_PREFIX + "power_per_method_filtered";
30 |
31 | private static final String ENERGY_CONSUMPTION_PER_FILTERED_METHOD_METRIC_HELP = "Energy for the filtered methods in Joules";
32 | private static final String POWER_CONSUMPTION_PER_FILTERED_METHOD_METRIC_HELP = "Power for the filtered methods in Watts";
33 | private static final String CO2_CONSUMPTION_PER_FILTERED_METHOD_METRIC_HELP = "CO2 consumption of the filtered methods in grams";
34 |
35 | private static final Map gaugeMap = new ConcurrentHashMap<>();
36 | private final long pid;
37 | private static HTTPServer server;
38 | private static final Lock lock = new ReentrantLock();
39 | // keep in mind the last run in order to find out, if a timeseries is not provided with values anymore.
40 | private static final Map> lastRun = new HashMap<>();
41 |
42 | /**
43 | * Constructor
44 | *
45 | * @param prometheusCfg the prometheus config
46 | */
47 | public PrometheusWriter(PrometheusCfg prometheusCfg) {
48 | this.pid = ProcessHandle.current().pid();
49 | if (lock.tryLock()) {
50 | try {
51 | if (PrometheusWriter.server == null) {
52 | if (prometheusCfg.isPublishJvmMetrics()) {
53 | DefaultExports.initialize();
54 | }
55 | log.info("Opening Http Server for jPowerMonitor Prometheus Metrics on port {}", prometheusCfg.getHttpPort());
56 | try {
57 | PrometheusWriter.server = new HTTPServer(prometheusCfg.getHttpPort());
58 | } catch (IOException e) {
59 | throw new RuntimeException(e);
60 | }
61 | }
62 | } finally {
63 | lock.unlock();
64 | }
65 | }
66 | }
67 |
68 | @Override
69 | public void writePowerConsumptionPerMethod(Map measurements) {
70 | throw new IllegalArgumentException("Currently not implemented");
71 | }
72 |
73 | @Override
74 | public void writePowerConsumptionPerMethodFiltered(Map measurements) {
75 | registerGaugeAndSetDataPoints(POWER_CONSUMPTION_PER_FILTERED_METHOD_METRIC_NAME, measurements, pid, DataPoint::getValue);
76 | }
77 |
78 | @Override
79 | public void writeEnergyConsumptionPerMethod(Map measurements) {
80 | throw new IllegalArgumentException("Currently not implemented");
81 | }
82 |
83 | @Override
84 | public void writeEnergyConsumptionPerMethodFiltered(Map measurements) {
85 | registerGaugeAndSetDataPoints(ENERGY_CONSUMPTION_PER_FILTERED_METHOD_METRIC_NAME, measurements, pid, DataPoint::getValue);
86 | registerGaugeAndSetDataPoints(CO2_CONSUMPTION_PER_FILTERED_METHOD_METRIC_NAME, measurements, pid, DataPoint::getCo2Value);
87 | }
88 |
89 | /**
90 | * @param metric the name of the metric that is sent to prometheus
91 | * @param metrics the DataPoints
92 | * @param pid the process id.
93 | * @param valueSupplier a function to get the value for the time series to be published.
94 | */
95 | public void registerGaugeAndSetDataPoints(String metric, Map metrics, long pid, Function valueSupplier) {
96 | log.debug("writing {}, metrics.size: {}", metric, metrics.size());
97 | Gauge gauge = gaugeMap.computeIfAbsent(metric,
98 | k -> Gauge.build()
99 | .name(metric)
100 | .labelNames("pid", "thread", "method")
101 | .help(helpForName(metric))
102 | .register());
103 | for (Map.Entry entry : metrics.entrySet()) {
104 | DataPoint dp = entry.getValue();
105 | // pid, thread, method time series value
106 | gauge.labels(String.valueOf(pid), dp.getThreadName(), dp.getName()).set(valueSupplier.apply(dp));
107 | }
108 | if (lastRun.get(metric) != null) {
109 | // compare and remove all missing...
110 | Map missing = findMissingDatapointsInCurrentRun(metrics, lastRun.get(metric));
111 | for (Map.Entry entry : missing.entrySet()) {
112 | DataPoint dp = entry.getValue();
113 | log.debug("'{}' is missing - removing", dp.getThreadName() + dp.getName());
114 | gauge.remove(String.valueOf(pid), dp.getThreadName(), dp.getName());
115 | }
116 | }
117 | lastRun.put(metric, metrics);
118 | }
119 |
120 | private Map findMissingDatapointsInCurrentRun(Map current, Map last) {
121 | Map result = new HashMap<>();
122 | // Iterate over the last map's entries
123 | for (Map.Entry entry : last.entrySet()) {
124 | String key = entry.getKey();
125 | // If the key is not in the current map, add it to the result
126 | if (!current.containsKey(key)) {
127 | result.put(key, entry.getValue());
128 | }
129 | }
130 | return result;
131 | }
132 |
133 | private String helpForName(String metric) {
134 | if (ENERGY_CONSUMPTION_PER_FILTERED_METHOD_METRIC_NAME.equals(metric)) {
135 | return ENERGY_CONSUMPTION_PER_FILTERED_METHOD_METRIC_HELP;
136 | } else if (POWER_CONSUMPTION_PER_FILTERED_METHOD_METRIC_NAME.equals(metric)) {
137 | return POWER_CONSUMPTION_PER_FILTERED_METHOD_METRIC_HELP;
138 | } else if (CO2_CONSUMPTION_PER_FILTERED_METHOD_METRIC_NAME.equals(metric)) {
139 | return CO2_CONSUMPTION_PER_FILTERED_METHOD_METRIC_HELP;
140 | } else {
141 | throw new IllegalArgumentException("Unknown metric. Configure help for " + metric);
142 | }
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/main/java/group/msg/jpowermonitor/agent/JPowerMonitorAgent.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.agent;
2 |
3 | import group.msg.jpowermonitor.agent.export.csv.CsvResultsWriter;
4 | import group.msg.jpowermonitor.agent.export.prometheus.PrometheusWriter;
5 | import group.msg.jpowermonitor.agent.export.statistics.StatisticsWriter;
6 | import group.msg.jpowermonitor.config.DefaultCfgProvider;
7 | import group.msg.jpowermonitor.config.dto.JPowerMonitorCfg;
8 | import group.msg.jpowermonitor.config.dto.JavaAgentCfg;
9 | import group.msg.jpowermonitor.config.dto.MeasureMethodKey;
10 | import group.msg.jpowermonitor.measurement.est.EstimationReader;
11 | import group.msg.jpowermonitor.util.Constants;
12 | import group.msg.jpowermonitor.util.CpuAndThreadUtils;
13 | import lombok.Getter;
14 | import lombok.extern.slf4j.Slf4j;
15 | import org.slf4j.LoggerFactory;
16 |
17 | import java.lang.instrument.Instrumentation;
18 | import java.lang.management.ThreadMXBean;
19 | import java.util.Timer;
20 | import java.util.TimerTask;
21 |
22 | import static group.msg.jpowermonitor.util.Constants.SEPARATOR;
23 |
24 | /**
25 | * Implements java agent to introspect power consumption of any java application.
26 | *
27 | * Usage:
28 | * java -javaagent:jpowermonitor-1.0.3-SNAPSHOT-all.jar[=path-to-jpowermonitor.yaml] -jar MyApp.jar [args]
29 | *
30 | * @author deinerj
31 | */
32 | @Slf4j
33 | public class JPowerMonitorAgent {
34 | private static final int ONE_SECOND_IN_MILLIS = 1000;
35 | @Getter
36 | private static final boolean slf4jLoggerImplPresent = !LoggerFactory.getILoggerFactory().getLogger("JPowerMonitorAgent").getName().equals("NOP");
37 |
38 | private JPowerMonitorAgent() {
39 | }
40 |
41 | /**
42 | * Hook to initialize the power measurement java agent at JVM startup.
43 | * Afterward the original app main-Method will be called.
44 | *
45 | * @param args command line args
46 | * @param inst java agent params
47 | */
48 | public static void premain(String args, Instrumentation inst) {
49 | long pid = ProcessHandle.current().pid();
50 | JPowerMonitorCfg cfg = new DefaultCfgProvider().readConfig(args);
51 | MeasureMethodKey measureMethodKey = cfg.getMeasurement().getMethodKey();
52 | String appInfo = String.format("Measuring power with %s, Version %s (Pid: %s) using measure method '%s'",
53 | Constants.APP_TITLE,
54 | JPowerMonitorAgent.class.getPackage().getImplementationVersion(),
55 | pid,
56 | measureMethodKey.getName());
57 |
58 | if (isSlf4jLoggerImplPresent()) {
59 | log.info(appInfo);
60 | } else {
61 | System.out.println(appInfo);
62 | }
63 | log.info(SEPARATOR);
64 | ThreadMXBean threadMXBean = CpuAndThreadUtils.initializeAndGetThreadMxBeanOrFailAndQuitApplication();
65 |
66 | JavaAgentCfg javaAgentCfg = cfg.getJavaAgent();
67 | log.debug("Start monitoring application with PID {}, javaAgentCfg.getMeasurementIntervalInMs(): {}", pid, javaAgentCfg.getMeasurementIntervalInMs());
68 | // TimerTask to calculate power consumption per thread at runtime using a configurable measurement interval
69 | // start Timer as daemon thread, so that it does not prevent applications from stopping
70 | Timer powerMeasurementTimer = new Timer("PowerMeasurementCollector", true);
71 | Timer energyToCsvTimer = new Timer("CsvResultsWriter", true);
72 | Timer energyToPrometheusTimer = new Timer("PrometheusWriter", true);
73 |
74 | PowerMeasurementCollector powerMeasurementCollector = new PowerMeasurementCollector(pid, threadMXBean, javaAgentCfg);
75 | if (MeasureMethodKey.EST.equals(measureMethodKey)) {
76 | // as the estimation method is sleeping for a certain time while measuring the power, correct the wait time
77 | // in the power measure collector by that period of time.
78 | powerMeasurementCollector.setCorrectionMeasureStackActivityInMs(EstimationReader.MEASURE_TIME_ESTIMATION_MS);
79 |
80 | // for the other methods there is also a small delay depending on the hardware running on. This may vary between 10 and 40 ms.
81 | // Ignore this for the moment...
82 | }
83 | long delayAndPeriodPmc = javaAgentCfg.getMeasurementIntervalInMs();
84 | powerMeasurementTimer.schedule(powerMeasurementCollector, delayAndPeriodPmc, delayAndPeriodPmc);
85 | log.debug("Scheduled PowerMeasurementCollector with delay {} ms and period {} ms", delayAndPeriodPmc, delayAndPeriodPmc);
86 | // TimerTask to write energy measurement statistics to CSV files while application still running
87 | if (javaAgentCfg.getWriteEnergyMeasurementsToCsvIntervalInS() > 0) {
88 | CsvResultsWriter cw = new CsvResultsWriter();
89 | long delayAndPeriodCw = javaAgentCfg.getWriteEnergyMeasurementsToCsvIntervalInS() * ONE_SECOND_IN_MILLIS;
90 | // start Timer as daemon thread, so that it does not prevent applications from stopping
91 | energyToCsvTimer.schedule(
92 | new TimerTask() {
93 | @Override
94 | public void run() {
95 | cw.writeEnergyConsumptionPerMethod(powerMeasurementCollector.getEnergyConsumptionPerMethod(false));
96 | cw.writeEnergyConsumptionPerMethodFiltered(powerMeasurementCollector.getEnergyConsumptionPerMethod(true));
97 | }
98 | }, delayAndPeriodCw, delayAndPeriodCw);
99 | log.debug("Scheduled CsvResultsWriter with delay {} ms and period {} ms", delayAndPeriodCw, delayAndPeriodCw);
100 | }
101 | if (javaAgentCfg.getMonitoring().getPrometheus().isEnabled()) {
102 | PrometheusWriter pw = new PrometheusWriter(javaAgentCfg.getMonitoring().getPrometheus());
103 | long delayAndPeriodPw = javaAgentCfg.getMonitoring().getPrometheus().getWriteEnergyIntervalInS() * ONE_SECOND_IN_MILLIS;
104 | energyToPrometheusTimer.schedule(
105 | new TimerTask() {
106 | @Override
107 | public void run() {
108 | pw.writeEnergyConsumptionPerMethodFiltered(powerMeasurementCollector.getEnergyConsumptionPerMethod(true));
109 | }
110 | }, delayAndPeriodPw, delayAndPeriodPw);
111 | log.debug("Scheduled PrometheusWriter with delay {} ms and period {} ms", delayAndPeriodPw, delayAndPeriodPw);
112 | }
113 |
114 | // Gracefully stop measurement at application shutdown
115 | Runtime.getRuntime().addShutdownHook(
116 | new Thread(() -> {
117 | powerMeasurementTimer.cancel();
118 | powerMeasurementTimer.purge();
119 | energyToCsvTimer.cancel();
120 | energyToCsvTimer.purge();
121 | energyToPrometheusTimer.cancel();
122 | energyToPrometheusTimer.purge();
123 | log.info("Power measurement ended gracefully");
124 | })
125 | );
126 | // at shutdown write last results to CSV files and write statistics
127 | Runtime.getRuntime().addShutdownHook(new Thread(() -> {
128 | CsvResultsWriter rw = new CsvResultsWriter();
129 | rw.writeEnergyConsumptionPerMethod(powerMeasurementCollector.getEnergyConsumptionPerMethod(false));
130 | rw.writeEnergyConsumptionPerMethodFiltered(powerMeasurementCollector.getEnergyConsumptionPerMethod(true));
131 | new StatisticsWriter(powerMeasurementCollector).writeStatistics(rw);
132 | }));
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/test/java/com/msg/myapplication/ReplaceInStringTest.java:
--------------------------------------------------------------------------------
1 | package com.msg.myapplication;
2 |
3 | import group.msg.jpowermonitor.junit.JPowerMonitorExtension;
4 | import lombok.extern.slf4j.Slf4j;
5 | import org.junit.jupiter.api.Assertions;
6 | import org.junit.jupiter.api.BeforeAll;
7 | import org.junit.jupiter.api.Named;
8 | import org.junit.jupiter.api.extension.ExtendWith;
9 | import org.junit.jupiter.params.ParameterizedTest;
10 | import org.junit.jupiter.params.provider.Arguments;
11 | import org.junit.jupiter.params.provider.MethodSource;
12 |
13 | import java.io.IOException;
14 | import java.nio.charset.StandardCharsets;
15 | import java.nio.file.Files;
16 | import java.nio.file.Paths;
17 | import java.util.regex.Pattern;
18 | import java.util.stream.Stream;
19 |
20 | import static org.assertj.core.api.Assertions.assertThat;
21 |
22 | @ExtendWith({JPowerMonitorExtension.class})
23 | @Slf4j
24 | public class ReplaceInStringTest {
25 | private static final int NUM_RUNS = 100_000;
26 | //private static final int NUM_RUNS = 10; // ==> use this in case the big xml is tested...
27 |
28 | // A carriage return means moving the cursor to the beginning of the line. The code is \r.
29 | // A line feed means moving one line forward. The code is \n.
30 | private static final Pattern PATTERN = Pattern.compile("\\R"); // same as "(\r)*\n"
31 |
32 | @BeforeAll
33 | static void prepare() throws IOException {
34 | // create input for parametrized tests
35 | byte[] simpleLf = Files.readAllBytes(Paths.get("src/test/resources/replace-string-test/simple-lf.txt"));
36 | byte[] simpleCrLf = Files.readAllBytes(Paths.get("src/test/resources/replace-string-test/simple-cr-lf.txt"));
37 | String simpleLfInput = new String(simpleLf, StandardCharsets.UTF_8);
38 | String simpleCrLfInput = new String(simpleCrLf, StandardCharsets.UTF_8);
39 | // assert that all three methods produce the same result:
40 | Assertions.assertEquals(ReplaceInStringTest.replaceUsingRegex(simpleLfInput), ReplaceInStringTest.replaceUsingForwardSearchAndChars(simpleLfInput));
41 | Assertions.assertEquals(ReplaceInStringTest.replaceUsingForwardSearchAndChars(simpleLfInput), ReplaceInStringTest.replaceUsingIndexOfAndStringReplace(simpleLfInput));
42 | Assertions.assertEquals(ReplaceInStringTest.replaceUsingRegex(simpleCrLfInput), ReplaceInStringTest.replaceUsingForwardSearchAndChars(simpleCrLfInput));
43 | Assertions.assertEquals(ReplaceInStringTest.replaceUsingForwardSearchAndChars(simpleCrLfInput), ReplaceInStringTest.replaceUsingIndexOfAndStringReplace(simpleCrLfInput));
44 | log.info("All methods produce same result");
45 | }
46 |
47 | static Stream provideStringsForReplacing() {
48 | return Stream.of(
49 | Arguments.of(Named.of("Text LF", "src/test/resources/replace-string-test/content-lf.txt"))
50 | , Arguments.of(Named.of("Text CRLF", "src/test/resources/replace-string-test/content-cr-lf.txt"))
51 | // , Arguments.of(Named.of("Xml CRLF", "src/test/resources/replace-string-test/big-xml.xml"))
52 | );
53 | }
54 |
55 | @ParameterizedTest
56 | @MethodSource("provideStringsForReplacing")
57 | void testReplaceUsingRegularExpressions(String file) throws IOException {
58 | byte[] content = Files.readAllBytes(Paths.get(file));
59 | String input = new String(content, StandardCharsets.UTF_8);
60 | String expected = ReplaceInStringTest.replaceUsingForwardSearchAndChars(input);
61 |
62 | final long start = System.currentTimeMillis();
63 | String res = null;
64 | for (int i = 0; i < NUM_RUNS; i++) {
65 | res = ReplaceInStringTest.replaceUsingRegex(input);
66 | }
67 | assertThat(res).isEqualTo(expected);
68 | log.info("testReplaceUsingRegularExpressions: {} ms", System.currentTimeMillis() - start);
69 | }
70 |
71 | @ParameterizedTest
72 | @MethodSource("provideStringsForReplacing")
73 | void testReplaceUsingForwardSearchAndChars(String file) throws IOException {
74 | byte[] content = Files.readAllBytes(Paths.get(file));
75 | String input = new String(content, StandardCharsets.UTF_8);
76 | String expected = ReplaceInStringTest.replaceUsingRegex(input);
77 |
78 | final long start = System.currentTimeMillis();
79 | String res = null;
80 | for (int i = 0; i < NUM_RUNS; i++) {
81 | res = ReplaceInStringTest.replaceUsingForwardSearchAndChars(input);
82 | }
83 | assertThat(res).isEqualTo(expected);
84 | log.info("testReplaceUsingForwardSearchAndChars: {} ms", System.currentTimeMillis() - start);
85 | }
86 |
87 | @ParameterizedTest
88 | @MethodSource("provideStringsForReplacing")
89 | void testReplaceUsingIndexOfAndStringReplace(String file) throws IOException {
90 | byte[] content = Files.readAllBytes(Paths.get(file));
91 | String input = new String(content, StandardCharsets.UTF_8);
92 | String expected = ReplaceInStringTest.replaceUsingRegex(input);
93 |
94 | final long start = System.currentTimeMillis();
95 | String res = null;
96 | for (int i = 0; i < NUM_RUNS; i++) {
97 | res = ReplaceInStringTest.replaceUsingIndexOfAndStringReplace(input);
98 | }
99 | assertThat(res).isEqualTo(expected);
100 | log.info("testReplaceUsingIndexOfAndStringReplace: {} ms", System.currentTimeMillis() - start);
101 | }
102 |
103 | // -------------------------------------------------------------------------------------------------
104 | //
105 | // Implementation of "business logic" follows:
106 | //
107 | // -------------------------------------------------------------------------------------------------
108 |
109 | /**
110 | * Replaces "linefeed" or "carriage return+linefeed" with the string "\n" (the characters 'backslash' and 'n').
111 | *
112 | * @param input the input that contains carriage return/linefeed.
113 | * @return String with "\n" instead of carriage return/linefeed.
114 | */
115 | private static String replaceUsingRegex(String input) {
116 | return PATTERN.matcher(input).replaceAll("\\\\n");
117 |
118 | // In Java, backslashes in strings and regex must be escaped as \\.
119 | // In regex, a backslash is represented as \\.
120 | // If replaceAll() is used to replace \n, this is written as \\\\n in Java, to escape the backslash and interpret the regular expression.
121 | }
122 |
123 | /**
124 | * Replaces "linefeed" or "carriage return+linefeed" with the string "\n" (the characters 'backslash' and 'n').
125 | *
126 | * @param input the input that contains carriage return/linefeed.
127 | * @return String with "\n" instead of carriage return/linefeed.
128 | */
129 | public static String replaceUsingForwardSearchAndChars(String input) {
130 | char[] chars = input.toCharArray();
131 | StringBuilder result = new StringBuilder();
132 | // replace \r\n with \n
133 | for (int i = 0; i < chars.length; i++) {
134 | if (chars[i] == '\r' && i + 1 < chars.length && chars[i + 1] == '\n') {
135 | // If we find \r followed by \n, append \n and skip the next character
136 | result.append("\\n");
137 | i++; // Skip the '\n'
138 | } else if (chars[i] == '\n') {
139 | result.append("\\n");
140 | } else {
141 | result.append(chars[i]); // Otherwise, just append the current character
142 | }
143 | }
144 | return result.toString();
145 | }
146 |
147 | /**
148 | * Replaces "linefeed" or "carriage return+linefeed" with the string "\n" (the characters 'backslash' and 'n').
149 | *
150 | * @param input the input that contains carriage return/linefeed.
151 | * @return String with "\\n" instead of carriage return/linefeed.
152 | */
153 | public static String replaceUsingIndexOfAndStringReplace(final String input) {
154 | if (input == null) {
155 | return null;
156 | }
157 | final StringBuilder sb = new StringBuilder(input);
158 | int pos = sb.indexOf("\r\n");
159 | // Case 1: "linefeed" and "carriage return" exist in the string
160 | if (pos != -1) {
161 | while (pos != -1) {
162 | sb.replace(pos, pos + 2, "\\n");
163 | pos = sb.indexOf("\r\n", pos + 3);
164 | }
165 | // Case 2: "linefeed" or "carriage return" exist in the string
166 | } else {
167 | pos = sb.indexOf("\n");
168 | while (pos != -1) {
169 | sb.replace(pos, pos + 1, "\\n");
170 | pos = sb.indexOf("\n", pos + 2);
171 | }
172 | }
173 | return sb.toString();
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/main/java/group/msg/jpowermonitor/junit/JUnitResultsWriter.java:
--------------------------------------------------------------------------------
1 | package group.msg.jpowermonitor.junit;
2 |
3 | import group.msg.jpowermonitor.JPowerMonitorException;
4 | import group.msg.jpowermonitor.agent.Unit;
5 | import group.msg.jpowermonitor.dto.DataPoint;
6 | import group.msg.jpowermonitor.dto.SensorValue;
7 | import lombok.extern.slf4j.Slf4j;
8 | import org.jetbrains.annotations.NotNull;
9 | import org.jetbrains.annotations.Nullable;
10 |
11 | import java.io.IOException;
12 | import java.nio.charset.StandardCharsets;
13 | import java.nio.file.Files;
14 | import java.nio.file.Path;
15 | import java.nio.file.StandardOpenOption;
16 | import java.text.DecimalFormat;
17 | import java.text.DecimalFormatSymbols;
18 | import java.util.List;
19 | import java.util.Locale;
20 | import java.util.ResourceBundle;
21 |
22 | import static group.msg.jpowermonitor.util.Constants.DATE_TIME_FORMATTER;
23 | import static group.msg.jpowermonitor.util.Constants.NEW_LINE;
24 | import static group.msg.jpowermonitor.util.Converter.convertJouleToCarbonDioxideGrams;
25 | import static group.msg.jpowermonitor.util.Converter.convertWattHoursToJoule;
26 |
27 | /**
28 | * Result writer for the JUnit extension.
29 | */
30 | @Slf4j
31 | public class JUnitResultsWriter {
32 |
33 | static {
34 | setLocaleDependentValues();
35 | }
36 |
37 | private static DecimalFormat DECIMAL_FORMAT;
38 | private final Path pathToMeasurementCsv, pathToResultCsv;
39 | private final Double carbonDioxideEmissionFactor;
40 | private static ResourceBundle labels;
41 | private static String dataPointFormatCsv, nonPowerSensorResultFormatCsv, powerSensorResultFormatCsv, SEP;
42 | private static final String DATA_POINT_FORMAT = "%s;%s;%s;%s;%s%s";
43 | private static final String NON_POWER_SENSOR_RESULT_FORMAT = "%s;%s;%s;%s;%s;%s";
44 | private static final String POWER_SENSOR_RESULT_FORMAT = "%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s";
45 |
46 | public static void setLocaleDependentValues() {
47 | labels = ResourceBundle.getBundle("csvExport", Locale.getDefault());
48 | SEP = Locale.getDefault().getCountry().toLowerCase(Locale.ROOT).equals("de") ? ";" : ",";
49 | dataPointFormatCsv = setCorrectDelimiter(DATA_POINT_FORMAT);
50 | nonPowerSensorResultFormatCsv = setCorrectDelimiter(NON_POWER_SENSOR_RESULT_FORMAT);
51 | powerSensorResultFormatCsv = setCorrectDelimiter(POWER_SENSOR_RESULT_FORMAT);
52 | DECIMAL_FORMAT = new DecimalFormat("###0.#####", DecimalFormatSymbols.getInstance(Locale.getDefault()));
53 | }
54 |
55 | private static String setCorrectDelimiter(String format) {
56 | return Locale.getDefault().getCountry().toLowerCase(Locale.ROOT).equals("de") ? format : format.replace(';', ',');
57 | }
58 |
59 | public JUnitResultsWriter(@Nullable Path pathToResultCsv, @Nullable Path pathToMeasurementCsv, @NotNull Double carbonDioxideEmissionFactor) {
60 | this.pathToResultCsv = pathToResultCsv;
61 | this.pathToMeasurementCsv = pathToMeasurementCsv;
62 | this.carbonDioxideEmissionFactor = carbonDioxideEmissionFactor;
63 | if (pathToResultCsv != null && !pathToResultCsv.toFile().exists()) {
64 | createFile(pathToResultCsv);
65 | String headings = labels.getString("measureTime") + SEP + labels.getString("measureName") + SEP + labels.getString("sensorName") + SEP + labels.getString("sensorValue") + SEP
66 | + labels.getString("sensorValueUnit") + SEP + labels.getString("baseLoad") + SEP + labels.getString("baseLoadUnit") + SEP + labels.getString("valuePlusBaseLoad")
67 | + SEP + labels.getString("valuePlusBaseLoadUnit") + SEP + labels.getString("energyOfValue") + SEP + labels.getString("energyOfValueUnit") + SEP
68 | + labels.getString("energyOfValuePlusBaseLoad") + SEP + labels.getString("energyOfValuePlusBaseLoadUnit") + SEP + labels.getString("co2Value") + SEP + labels.getString("co2Unit");
69 | appendToFile(pathToResultCsv, headings + NEW_LINE);
70 | }
71 | if (pathToMeasurementCsv != null && !pathToMeasurementCsv.toFile().exists()) {
72 | createFile(pathToMeasurementCsv);
73 | String headings = labels.getString("measureTime") + SEP + labels.getString("measureName") + SEP + labels.getString("sensorName") + SEP + labels.getString("sensorValue") + SEP
74 | + labels.getString("sensorValueUnit");
75 | appendToFile(pathToMeasurementCsv, headings + NEW_LINE);
76 | }
77 | }
78 |
79 | private void createFile(@NotNull Path fileToCreate) {
80 | try {
81 | if (fileToCreate.toFile().getParentFile() != null) {
82 | boolean createdDir = fileToCreate.toFile().getParentFile().mkdirs();
83 | if (createdDir) {
84 | log.debug("Created directory for writing the csv file to: {}", fileToCreate.toFile().getParentFile().getAbsolutePath());
85 | }
86 | }
87 | Files.createFile(fileToCreate);
88 | } catch (IOException e) {
89 | throw new JPowerMonitorException("Unable to create file: " + fileToCreate, e);
90 | }
91 | }
92 |
93 | public void writeToMeasurementCsv(String testName, List dataPoints) {
94 | writeToMeasurementCsv(testName, dataPoints, "");
95 | }
96 |
97 | public void writeToMeasurementCsv(String testName, List dataPoints, String namePrefix) {
98 | if (pathToMeasurementCsv == null) {
99 | return; // do nothing, if path is not configured.
100 | }
101 | for (DataPoint dp : dataPoints) {
102 | String csvEntry = createCsvEntryForDataPoint(dp, namePrefix, testName);
103 | appendToFile(pathToMeasurementCsv, csvEntry);
104 | }
105 | }
106 |
107 | public static String createCsvEntryForDataPoint(@NotNull DataPoint dp, String namePrefix, String testName) {
108 | return String.format(dataPointFormatCsv,
109 | DATE_TIME_FORMATTER.format(dp.getTime()),
110 | namePrefix + testName,
111 | dp.getName(),
112 | DECIMAL_FORMAT.format(dp.getValue()),
113 | dp.getUnit(), NEW_LINE);
114 | }
115 |
116 | public void writeToResultCsv(String testName, SensorValue sensorValue) {
117 | if (pathToResultCsv == null) {
118 | return; // do nothing, if path is not configured.
119 | }
120 | String csvEntry = createCsvEntryForSensorValue(testName, sensorValue);
121 | appendToFile(pathToResultCsv, csvEntry);
122 | }
123 |
124 | private String createCsvEntryForSensorValue(String testName, SensorValue sensorValue) {
125 | String csvEntry;
126 | if (sensorValue.isPowerSensor()) { // only power values=>
127 | double valueWithoutIdlePowerJ = convertWattHoursToJoule(sensorValue.getValueWithoutIdlePowerPerHour());
128 | double valueWithIdlePowerJ = convertWattHoursToJoule(sensorValue.getValueWithIdlePowerPerHour());
129 | double co2Equivalent = convertJouleToCarbonDioxideGrams(sensorValue.getValueWithIdlePowerPerHour(), carbonDioxideEmissionFactor);
130 | csvEntry = String.format(powerSensorResultFormatCsv,
131 | DATE_TIME_FORMATTER.format(sensorValue.getExecutionTime()),
132 | testName,
133 | sensorValue.getName(),
134 | formatNumber(sensorValue.getValue()),
135 | sensorValue.getUnit(),
136 | formatNumber(sensorValue.getPowerInIdleMode()),
137 | sensorValue.getUnit(),
138 | formatNumber(sensorValue.getValue() + sensorValue.getPowerInIdleMode()),
139 | sensorValue.getUnit(),
140 | formatNumber(valueWithoutIdlePowerJ),
141 | Unit.JOULE.getAbbreviation(),
142 | formatNumber(valueWithIdlePowerJ),
143 | Unit.JOULE.getAbbreviation(),
144 | formatNumber(co2Equivalent),
145 | Unit.GRAMS_CO2.getAbbreviation(),
146 | NEW_LINE);
147 | } else {
148 | csvEntry = String.format(nonPowerSensorResultFormatCsv, DATE_TIME_FORMATTER.format(sensorValue.getExecutionTime()), testName,
149 | sensorValue.getName(), formatNumber(sensorValue.getValue()), sensorValue.getUnit(), NEW_LINE);
150 | }
151 | return csvEntry;
152 | }
153 |
154 | @NotNull
155 | private String formatNumber(double val) {
156 | return DECIMAL_FORMAT.format(val);
157 | }
158 |
159 | private void appendToFile(@NotNull Path path, @NotNull String lineToAppend) {
160 | try {
161 | Files.writeString(path, lineToAppend, StandardCharsets.UTF_8, StandardOpenOption.APPEND);
162 | } catch (IOException e) {
163 | log.error("Unable to append to csv file: {}", path, e);
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 |
118 |
119 | # Determine the Java command to use to start the JVM.
120 | if [ -n "$JAVA_HOME" ] ; then
121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
122 | # IBM's JDK on AIX uses strange locations for the executables
123 | JAVACMD=$JAVA_HOME/jre/sh/java
124 | else
125 | JAVACMD=$JAVA_HOME/bin/java
126 | fi
127 | if [ ! -x "$JAVACMD" ] ; then
128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
129 |
130 | Please set the JAVA_HOME variable in your environment to match the
131 | location of your Java installation."
132 | fi
133 | else
134 | JAVACMD=java
135 | if ! command -v java >/dev/null 2>&1
136 | then
137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
138 |
139 | Please set the JAVA_HOME variable in your environment to match the
140 | location of your Java installation."
141 | fi
142 | fi
143 |
144 | # Increase the maximum file descriptors if we can.
145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
146 | case $MAX_FD in #(
147 | max*)
148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
149 | # shellcheck disable=SC2039,SC3045
150 | MAX_FD=$( ulimit -H -n ) ||
151 | warn "Could not query maximum file descriptor limit"
152 | esac
153 | case $MAX_FD in #(
154 | '' | soft) :;; #(
155 | *)
156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
157 | # shellcheck disable=SC2039,SC3045
158 | ulimit -n "$MAX_FD" ||
159 | warn "Could not set maximum file descriptor limit to $MAX_FD"
160 | esac
161 | fi
162 |
163 | # Collect all arguments for the java command, stacking in reverse order:
164 | # * args from the command line
165 | # * the main class name
166 | # * -classpath
167 | # * -D...appname settings
168 | # * --module-path (only if needed)
169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
170 |
171 | # For Cygwin or MSYS, switch paths to Windows format before running java
172 | if "$cygwin" || "$msys" ; then
173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
174 |
175 | JAVACMD=$( cygpath --unix "$JAVACMD" )
176 |
177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
178 | for arg do
179 | if
180 | case $arg in #(
181 | -*) false ;; # don't mess with options #(
182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
183 | [ -e "$t" ] ;; #(
184 | *) false ;;
185 | esac
186 | then
187 | arg=$( cygpath --path --ignore --mixed "$arg" )
188 | fi
189 | # Roll the args list around exactly as many times as the number of
190 | # args, so each arg winds up back in the position where it started, but
191 | # possibly modified.
192 | #
193 | # NB: a `for` loop captures its iteration list before it begins, so
194 | # changing the positional parameters here affects neither the number of
195 | # iterations, nor the values presented in `arg`.
196 | shift # remove old arg
197 | set -- "$@" "$arg" # push replacement arg
198 | done
199 | fi
200 |
201 |
202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
204 |
205 | # Collect all arguments for the java command:
206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
207 | # and any embedded shellness will be escaped.
208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
209 | # treated as '${Hostname}' itself on the command line.
210 |
211 | set -- \
212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
214 | "$@"
215 |
216 | # Stop when "xargs" is not available.
217 | if ! command -v xargs >/dev/null 2>&1
218 | then
219 | die "xargs is not available"
220 | fi
221 |
222 | # Use "xargs" to parse quoted args.
223 | #
224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
225 | #
226 | # In Bash we could simply go:
227 | #
228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
229 | # set -- "${ARGS[@]}" "$@"
230 | #
231 | # but POSIX shell has neither arrays nor command substitution, so instead we
232 | # post-process each arg (as a line of input to sed) to backslash-escape any
233 | # character that might be a shell metacharacter, then use eval to reverse
234 | # that process (while maintaining the separation between arguments), and wrap
235 | # the whole thing up as a single "set" statement.
236 | #
237 | # This will of course break if any of these variables contains a newline or
238 | # an unmatched quote.
239 | #
240 |
241 | eval "set -- $(
242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
243 | xargs -n1 |
244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
245 | tr '\n' ' '
246 | )" '"$@"'
247 |
248 | exec "$JAVACMD" "$@"
249 |
--------------------------------------------------------------------------------