├── .gitattributes ├── version.txt ├── src ├── test │ ├── resources │ │ ├── umlauts.txt │ │ ├── replace-string-test │ │ │ ├── simple-lf.txt │ │ │ ├── simple-cr-lf.txt │ │ │ ├── content-lf.txt │ │ │ └── content-cr-lf.txt │ │ ├── invalidConfig.xml │ │ ├── hwinfo-test.csv │ │ ├── simplelogger.properties │ │ ├── CommaSeparatedValuesReaderTest.yaml │ │ ├── MyTest.yaml │ │ ├── EndlessLoopTest.yaml │ │ ├── DefaultConfigProviderTest.yaml │ │ ├── EstimationReaderTest.yaml │ │ ├── ReplaceInStringTest.yaml │ │ ├── JPowerMonitorCfgTest.yaml │ │ └── JPowerMonitorAgentTest.yaml │ └── java │ │ ├── group │ │ └── msg │ │ │ └── jpowermonitor │ │ │ ├── agent │ │ │ ├── JPowerMonitorAgentTest.java │ │ │ ├── ConcurrentHashMapLongAdderTest.java │ │ │ ├── MeasurePowerTest.java │ │ │ └── PowerMeasurementCfgCollectorTest.java │ │ │ ├── CfgProviderForTests.java │ │ │ ├── junit │ │ │ ├── JPowerMonitorExtensionTest.java │ │ │ └── CsvJUnitResultsWriterTest.java │ │ │ ├── util │ │ │ └── ConverterTest.java │ │ │ ├── measurement │ │ │ ├── est │ │ │ │ └── EstimationReaderTest.java │ │ │ └── csv │ │ │ │ └── CommaSeparatedValuesReaderTest.java │ │ │ └── config │ │ │ ├── DefaultCfgProviderTest.java │ │ │ └── JPowerMonitorCfgTest.java │ │ └── com │ │ └── msg │ │ └── myapplication │ │ ├── EndlessLoopTest.java │ │ ├── MyTest.java │ │ └── ReplaceInStringTest.java └── main │ ├── java │ └── group │ │ └── msg │ │ └── jpowermonitor │ │ ├── util │ │ ├── CmdLineArgs.java │ │ ├── Constants.java │ │ ├── Converter.java │ │ ├── HumanReadableTime.java │ │ └── CpuAndThreadUtils.java │ │ ├── config │ │ ├── dto │ │ │ ├── MonitoringCfg.java │ │ │ ├── EstimationCfg.java │ │ │ ├── CsvColumnCfg.java │ │ │ ├── PrometheusCfg.java │ │ │ ├── LibreHardwareMonitorCfg.java │ │ │ ├── CsvRecordingCfg.java │ │ │ ├── PathElementCfg.java │ │ │ ├── MeasurementCfg.java │ │ │ ├── JavaAgentCfg.java │ │ │ ├── CsvMeasurementCfg.java │ │ │ ├── MeasureMethodKey.java │ │ │ └── JPowerMonitorCfg.java │ │ ├── JPowerMonitorCfgProvider.java │ │ └── DefaultCfgProvider.java │ │ ├── dto │ │ ├── PowerQuestionable.java │ │ ├── Quantity.java │ │ ├── SensorValues.java │ │ ├── SensorValue.java │ │ ├── MethodActivity.java │ │ ├── DataPoint.java │ │ └── Activity.java │ │ ├── JPowerMonitorException.java │ │ ├── agent │ │ ├── export │ │ │ ├── ResultsWriter.java │ │ │ ├── statistics │ │ │ │ └── StatisticsWriter.java │ │ │ ├── csv │ │ │ │ └── CsvResultsWriter.java │ │ │ └── prometheus │ │ │ │ └── PrometheusWriter.java │ │ ├── Unit.java │ │ └── JPowerMonitorAgent.java │ │ ├── measurement │ │ ├── lhm │ │ │ ├── DataElem.java │ │ │ └── LibreHardwareMonitorReader.java │ │ └── est │ │ │ └── EstimationReader.java │ │ ├── MeasureMethodProvider.java │ │ ├── MeasureMethod.java │ │ └── junit │ │ └── JUnitResultsWriter.java │ └── resources │ ├── csvExport.properties │ ├── csvExport_de.properties │ ├── csvExport_es.properties │ ├── csvExport_fr.properties │ ├── simplelogger.properties │ └── jpowermonitor-template.yaml ├── settings.gradle ├── docs ├── lhm-webserver.png └── slidy.css ├── gradle.properties ├── gradle ├── javadoc.options ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── signing.gradle ├── test-config.gradle ├── dependencyUpdates.gradle ├── jar.gradle ├── publish.gradle ├── dist.gradle └── license-normalizer-bundle.json ├── .github ├── dependabot.yml └── workflows │ └── build.yaml ├── .gitignore ├── .editorconfig ├── .gitlab-ci.yml ├── algorithm.txt ├── gradlew.bat ├── CHANGELOG.md └── gradlew /.gitattributes: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 1.2.2-SNAPSHOT 2 | -------------------------------------------------------------------------------- /src/test/resources/umlauts.txt: -------------------------------------------------------------------------------- 1 | ÖÜÄüöäß 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'jpowermonitor' 2 | 3 | -------------------------------------------------------------------------------- /src/test/resources/replace-string-test/simple-lf.txt: -------------------------------------------------------------------------------- 1 | a 2 | b 3 | c 4 | d 5 | e 6 | -------------------------------------------------------------------------------- /src/test/resources/replace-string-test/simple-cr-lf.txt: -------------------------------------------------------------------------------- 1 | a 2 | b 3 | c 4 | d 5 | e 6 | -------------------------------------------------------------------------------- /docs/lhm-webserver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msg-systems/jpowermonitor/HEAD/docs/lhm-webserver.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | group=group.msg 2 | premain.classname=group.msg.jpowermonitor.agent.JPowerMonitorAgent 3 | -------------------------------------------------------------------------------- /gradle/javadoc.options: -------------------------------------------------------------------------------- 1 | -use -author -version -Xdoclint:none -quiet -encoding utf-8 -docencoding utf-8 -charset utf-8 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msg-systems/jpowermonitor/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/test/resources/invalidConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | False 4 | 5 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/util/CmdLineArgs.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.util; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class CmdLineArgs { 7 | private short secondsToRun; 8 | private int cpuThreads; 9 | } 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/config/dto/MonitoringCfg.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.config.dto; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * Data element for monitoring config. 7 | */ 8 | @Data 9 | public class MonitoringCfg { 10 | PrometheusCfg prometheus = new PrometheusCfg(); 11 | } 12 | -------------------------------------------------------------------------------- /src/test/resources/hwinfo-test.csv: -------------------------------------------------------------------------------- 1 | 12.7.2022,18:7:10.680,6.352,0.061,24.733,76.0,363,84,581.376,46.206, 2 | 12.7.2022,18:7:12.730,5.820,0.056,23.745,76.0,364,84,609.290,51.021, 3 | 12.7.2022,18:7:14.811,9.453,0.128,28.830,76.0,365,84,469.789,39.393, 4 | 12.7.2022,18:7:16.865,7.055,0.012,25.458,75.0,367,84,603.767,47.299, 5 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/config/dto/EstimationCfg.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.config.dto; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * Data class for estimation measurement config. 7 | */ 8 | @Data 9 | public class EstimationCfg { 10 | private Double cpuMinWatts; 11 | private Double cpuMaxWatts; 12 | } 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Updates for GitHub Actions used in the repo 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | # Updates for Gradle dependencies used in the app 9 | - package-ecosystem: "gradle" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/config/dto/CsvColumnCfg.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.config.dto; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * Data class for csv column config. 7 | * 8 | * @see CsvMeasurementCfg 9 | */ 10 | @Data 11 | public class CsvColumnCfg { 12 | private int index; 13 | private String name; 14 | private Double energyInIdleMode; 15 | } 16 | -------------------------------------------------------------------------------- /gradle/signing.gradle: -------------------------------------------------------------------------------- 1 | // Signature of artifacts 2 | apply plugin: 'signing' 3 | if (project.hasProperty('signing.gnupg.keyName')) { 4 | signing { 5 | useGpgCmd() 6 | required = !rootProject.version.endsWith('-SNAPSHOT') 7 | sign (publishing.publications.mavenJava) 8 | } 9 | } else { 10 | logger.info('Disable signing of artifacts as no key is configured') 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/config/dto/PrometheusCfg.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.config.dto; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * Data element for prometheus config. 7 | */ 8 | @Data 9 | public class PrometheusCfg { 10 | boolean enabled; // Default: false 11 | Integer httpPort; 12 | Long writeEnergyIntervalInS; 13 | boolean publishJvmMetrics; // Default: false 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/dto/PowerQuestionable.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.dto; 2 | 3 | import group.msg.jpowermonitor.agent.Unit; 4 | 5 | /** 6 | * Interface to find out unit of sensor and if it is a power sensor. 7 | */ 8 | public interface PowerQuestionable { 9 | Unit getUnit(); 10 | 11 | default boolean isPowerSensor() { 12 | return Unit.WATT.equals(getUnit()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/config/dto/LibreHardwareMonitorCfg.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.config.dto; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Data element for Libre Hardware Monitor config. 9 | * 10 | * @see PathElementCfg 11 | */ 12 | @Data 13 | public class LibreHardwareMonitorCfg { 14 | private String url; 15 | private List paths; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/config/dto/CsvRecordingCfg.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.config.dto; 2 | 3 | import lombok.Data; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | /** 7 | * Data class for configuration for csv recording tools. 8 | */ 9 | @Data 10 | public class CsvRecordingCfg { 11 | @Nullable 12 | private String resultCsv; 13 | @Nullable 14 | private String measurementCsv; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/config/dto/PathElementCfg.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.config.dto; 2 | 3 | import lombok.Data; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Data class for path element for Libre Hardware Monitor path. 10 | */ 11 | @Data 12 | public class PathElementCfg { 13 | List path; 14 | @Nullable 15 | Double energyInIdleMode; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/dto/Quantity.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.dto; 2 | 3 | import group.msg.jpowermonitor.agent.Unit; 4 | import lombok.Value; 5 | 6 | /** 7 | * An amount of a {@link Unit} 8 | */ 9 | @Value 10 | public class Quantity { 11 | Double value; 12 | Unit unit; 13 | 14 | public static Quantity of(Double value, Unit unit) { 15 | return new Quantity(value, unit); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/resources/csvExport.properties: -------------------------------------------------------------------------------- 1 | measureTime=Time 2 | measureName=Name 3 | sensorName=Sensor 4 | sensorValue=Value 5 | sensorValueUnit=Unit 6 | baseLoad=Baseload 7 | baseLoadUnit=Unit 8 | valuePlusBaseLoad=Value+Baseload 9 | valuePlusBaseLoadUnit=Unit 10 | energyOfValue=Energy(Value) 11 | energyOfValueUnit=Unit 12 | energyOfValuePlusBaseLoad=Energy(Value+Baseload) 13 | energyOfValuePlusBaseLoadUnit=Unit 14 | co2Value=CO2 Value 15 | co2Unit=Unit 16 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/dto/SensorValues.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.dto; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | @Documented 10 | @Target(ElementType.FIELD) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | public @interface SensorValues { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/csvExport_de.properties: -------------------------------------------------------------------------------- 1 | measureTime=Uhrzeit 2 | measureName=Name 3 | sensorName=Sensor 4 | sensorValue=Wert 5 | sensorValueUnit=Einheit 6 | baseLoad=Grundlast 7 | baseLoadUnit=Einheit 8 | valuePlusBaseLoad=Wert+Grundlast 9 | valuePlusBaseLoadUnit=Einheit 10 | energyOfValue=Energie(Wert) 11 | energyOfValueUnit=Einheit 12 | energyOfValuePlusBaseLoad=Energie(Wert+Grundlast) 13 | energyOfValuePlusBaseLoadUnit=Einheit 14 | co2Value=CO2 Wert 15 | co2Unit=Einheit 16 | -------------------------------------------------------------------------------- /src/main/resources/csvExport_es.properties: -------------------------------------------------------------------------------- 1 | measureTime=Hora 2 | measureName=Nombre 3 | sensorName=Sensor 4 | sensorValue=Valor 5 | sensorValueUnit=Unidad 6 | baseLoad=Carga base 7 | baseLoadUnit=Unidad 8 | valuePlusBaseLoad=Valor+Carga base 9 | valuePlusBaseLoadUnit=Unidad 10 | energyOfValue=Energía(Valor) 11 | energyOfValueUnit=Unidad 12 | energyOfValuePlusBaseLoad=Energía(Valor+Carga base) 13 | energyOfValuePlusBaseLoadUnit=Unidad 14 | co2Value=CO2 Valor 15 | co2Unit=Unidad 16 | -------------------------------------------------------------------------------- /src/main/resources/csvExport_fr.properties: -------------------------------------------------------------------------------- 1 | measureTime=Heure 2 | measureName=Nom 3 | sensorName=Détecteur 4 | sensorValue=Valeur 5 | sensorValueUnit=Unité 6 | baseLoad=Grundlast 7 | baseLoadUnit=Unité 8 | valuePlusBaseLoad=Valeur+charge de base 9 | valuePlusBaseLoadUnit=Unité 10 | energyOfValue=Energie(valeur) 11 | energyOfValueUnit=Unité 12 | energyOfValuePlusBaseLoad=Energie(valeur+charge de base) 13 | energyOfValuePlusBaseLoadUnit=Unité 14 | co2Value=CO2 Valeur 15 | co2Unit=Unité 16 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/JPowerMonitorException.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor; 2 | 3 | /** 4 | * Exception that may occur in jpower monitor during reading configuration or during measuring. 5 | */ 6 | public class JPowerMonitorException extends RuntimeException { 7 | public JPowerMonitorException(String message) { 8 | super(message); 9 | } 10 | 11 | public JPowerMonitorException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /gradle/test-config.gradle: -------------------------------------------------------------------------------- 1 | test { 2 | useJUnitPlatform() 3 | } 4 | testlogger { 5 | showStandardStreams = true 6 | showPassedStandardStreams = false 7 | showSkippedStandardStreams = false 8 | showFailedStandardStreams = true 9 | } 10 | 11 | jacoco { 12 | toolVersion = "0.8.12" 13 | } 14 | jacocoTestReport { 15 | reports { 16 | xml.required = false 17 | csv.required = false 18 | html.outputLocation = layout.buildDirectory.dir('jacocoHtml') 19 | } 20 | dependsOn(test) // test must be run before jacocoTestReports. 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/agent/export/ResultsWriter.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.agent.export; 2 | 3 | import group.msg.jpowermonitor.dto.DataPoint; 4 | 5 | import java.util.Map; 6 | 7 | public interface ResultsWriter { 8 | void writePowerConsumptionPerMethod(Map measurements); 9 | 10 | void writePowerConsumptionPerMethodFiltered(Map measurements); 11 | 12 | void writeEnergyConsumptionPerMethod(Map measurements); 13 | 14 | void writeEnergyConsumptionPerMethodFiltered(Map measurements); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | # Set the default logging level for all loggers 2 | org.slf4j.simpleLogger.defaultLogLevel=info 3 | # Enable/disable showing the date and time in the output 4 | org.slf4j.simpleLogger.showDateTime=true 5 | # Set the date and time format to be used in the output 6 | org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss.SSS 7 | # Enable/disable showing the log name 8 | org.slf4j.simpleLogger.showLogName=false 9 | # Enable/disable showing the short name (last component) of the logger 10 | org.slf4j.simpleLogger.showShortLogName=true 11 | # Enable/disable showing the thread name 12 | org.slf4j.simpleLogger.showThreadName=true 13 | 14 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/dto/SensorValue.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.dto; 2 | 3 | import group.msg.jpowermonitor.agent.Unit; 4 | import lombok.Builder; 5 | import lombok.Value; 6 | 7 | import java.time.LocalDateTime; 8 | 9 | /** 10 | * SensorValue that is measured by external tool. 11 | */ 12 | @Value 13 | @Builder 14 | public class SensorValue implements PowerQuestionable { 15 | String name; 16 | Double value; 17 | Unit unit; 18 | Double powerInIdleMode; 19 | LocalDateTime executionTime; 20 | long durationOfTestInNanoSeconds; 21 | Double valueWithoutIdlePowerPerHour; 22 | Double valueWithIdlePowerPerHour; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/config/dto/MeasurementCfg.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.config.dto; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * Data class for measurement method. 7 | * 8 | * @see CsvMeasurementCfg 9 | * @see LibreHardwareMonitorCfg 10 | * @see EstimationCfg 11 | */ 12 | @Data 13 | public class MeasurementCfg { 14 | private String method; // sadly snakeyaml does not support using Enums as attributes. 15 | 16 | public MeasureMethodKey getMethodKey() { 17 | return MeasureMethodKey.of(method); 18 | } 19 | 20 | private CsvMeasurementCfg csv; 21 | private LibreHardwareMonitorCfg lhm; 22 | private EstimationCfg est; 23 | } 24 | -------------------------------------------------------------------------------- /gradle/dependencyUpdates.gradle: -------------------------------------------------------------------------------- 1 | static boolean isStable(String version) { 2 | boolean result = ['alpha', 'beta', 'rc', 'cr', 'm', 'preview', 'snapshot'].any { 3 | version ==~ /(?i).*[.-]$it[.\-\d]*/ 4 | } 5 | return !result 6 | } 7 | 8 | tasks.named("dependencyUpdates").configure { 9 | // only suggest stable gradle versions 10 | gradleReleaseChannel = 'current' 11 | // also consider snapshots 12 | revision = 'integration' 13 | 14 | rejectVersionIf { 15 | // snapshots may upgrade to newer snapshots versions, but release version.txt only to newer releases 16 | !isStable(it.candidate.version) && isStable(it.currentVersion) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | # Set the default logging level for all loggers 2 | org.slf4j.simpleLogger.defaultLogLevel=info 3 | 4 | # Enable/disable showing the date and time in the output 5 | org.slf4j.simpleLogger.showDateTime=true 6 | 7 | # Set the date and time format to be used in the output 8 | org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss.SSS 9 | 10 | # Enable/disable showing the log name 11 | org.slf4j.simpleLogger.showLogName=false 12 | 13 | # Enable/disable showing the short name (last component) of the logger 14 | org.slf4j.simpleLogger.showShortLogName=true 15 | 16 | # Enable/disable showing the thread name 17 | org.slf4j.simpleLogger.showThreadName=true 18 | 19 | -------------------------------------------------------------------------------- /src/test/java/group/msg/jpowermonitor/agent/JPowerMonitorAgentTest.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.agent; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | 7 | class JPowerMonitorAgentTest { 8 | @Test 9 | void premain() throws InterruptedException { 10 | JPowerMonitorAgent.premain("JPowerMonitorAgentTest.yaml", null); 11 | Thread t = new Thread(() -> { 12 | try { 13 | TimeUnit.SECONDS.sleep(10); 14 | } catch (InterruptedException e) { 15 | throw new RuntimeException(e); 16 | } 17 | }); 18 | t.start(); 19 | t.join(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/dto/MethodActivity.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.dto; 2 | 3 | import lombok.Data; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | @Data 8 | public class MethodActivity implements Activity { 9 | String threadName; 10 | LocalDateTime time; 11 | String methodQualifier; 12 | String filteredMethodQualifier; 13 | Quantity representedQuantity; 14 | 15 | @Override 16 | public String getIdentifier(boolean asFiltered) { 17 | return asFiltered || methodQualifier == null ? filteredMethodQualifier : methodQualifier; 18 | } 19 | 20 | @Override 21 | public boolean isFinalized() { 22 | return representedQuantity != null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/config/dto/JavaAgentCfg.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.config.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.Collections; 8 | import java.util.Set; 9 | 10 | /** 11 | * Data class for java agent config. 12 | */ 13 | @Data 14 | @AllArgsConstructor 15 | @NoArgsConstructor 16 | public class JavaAgentCfg { 17 | private Set packageFilter = Collections.emptySet(); 18 | private long measurementIntervalInMs; 19 | private long gatherStatisticsIntervalInMs; 20 | private long writeEnergyMeasurementsToCsvIntervalInS; 21 | private MonitoringCfg monitoring = new MonitoringCfg(); 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | *.csv 39 | /src/main/resources/jpowermonitor.yaml 40 | /src/test/resources/jpowermonitor.yaml 41 | 42 | jpowermonitor.yaml 43 | /my-config.yaml 44 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/util/Constants.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.util; 2 | 3 | import java.text.DecimalFormat; 4 | import java.text.DecimalFormatSymbols; 5 | import java.time.format.DateTimeFormatter; 6 | import java.util.Locale; 7 | 8 | public interface Constants { 9 | double ONE_HUNDRED = 100; 10 | double ONE_THOUSAND = 1000; 11 | String APP_TITLE = "jPowerMonitor"; 12 | String SEPARATOR = "-----------------------------------------------------------------------------------------"; 13 | String NEW_LINE = System.lineSeparator(); 14 | DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd'T'HH:mm:ss-SSS"); 15 | DecimalFormat DECIMAL_FORMAT = new DecimalFormat("###0.#####", DecimalFormatSymbols.getInstance(Locale.getDefault())); 16 | } 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | insert_final_newline = true 5 | indent_style = space 6 | indent_size = 4 7 | 8 | # enforce intellij specific formatting 9 | # add braces on formatting (for one-liner) 10 | ij_java_if_brace_force = always 11 | ij_java_do_while_brace_force = always 12 | ij_java_for_brace_force = always 13 | ij_java_while_brace_force = always 14 | 15 | # no * imports: 16 | ij_java_names_count_to_use_import_on_demand = 999 17 | ij_java_class_count_to_use_import_on_demand = 999 18 | ij_java_use_single_class_imports = true 19 | 20 | # never wrap lines (better use soft wrap) 21 | ij_java_wrap_long_lines = false 22 | 23 | # enable formatter tags 24 | ij_formatter_off_tag = @formatter:off 25 | ij_formatter_on_tag = @formatter:on 26 | ij_yaml_indent_size = 2 27 | 28 | [*.{yml,yaml}] 29 | indent_size = 2 30 | 31 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/measurement/lhm/DataElem.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.measurement.lhm; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import lombok.Data; 7 | 8 | /** 9 | * Data element for json communication with Libre Hardware Monitor. 10 | */ 11 | @JsonIgnoreProperties(ignoreUnknown = true) 12 | @JsonInclude(JsonInclude.Include.NON_NULL) 13 | @Data 14 | public class DataElem { 15 | Integer id; 16 | @JsonProperty("Text") 17 | String text; 18 | @JsonProperty("Min") 19 | String min; 20 | @JsonProperty("Value") 21 | String value; 22 | @JsonProperty("Max") 23 | String max; 24 | @JsonProperty("ImageUrl") 25 | String imageUrl; 26 | @JsonProperty("Children") 27 | DataElem[] children; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/config/dto/CsvMeasurementCfg.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.config.dto; 2 | 3 | import lombok.Data; 4 | 5 | import java.nio.charset.Charset; 6 | import java.nio.charset.StandardCharsets; 7 | import java.nio.file.Path; 8 | import java.util.List; 9 | 10 | /** 11 | * Data class for csv measurement config. 12 | * 13 | * @see CsvColumnCfg 14 | */ 15 | @Data 16 | public class CsvMeasurementCfg { 17 | private String inputFile; 18 | // this is set during initialization of CommaSeparatedValuesReader. 19 | private Path inputFileAsPath; 20 | private String lineToRead = "last"; 21 | private List columns; 22 | private String delimiter = ","; 23 | private String encoding; 24 | 25 | public Charset getEncodingAsCharset() { 26 | if (encoding == null) { 27 | return StandardCharsets.ISO_8859_1; 28 | } 29 | return Charset.forName(encoding); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/config/dto/MeasureMethodKey.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.config.dto; 2 | 3 | import group.msg.jpowermonitor.JPowerMonitorException; 4 | import lombok.Getter; 5 | 6 | import java.util.Arrays; 7 | 8 | @Getter 9 | public enum MeasureMethodKey { 10 | LHM("lhm", "Libre Hardware Monitor"), 11 | CSV("csv", "Comma Separated Values File"), 12 | EST("est", "Estimated values according to Etsy's Cloud Jewels method"); 13 | 14 | private final String key; 15 | private final String name; 16 | 17 | MeasureMethodKey(String k, String n) { 18 | this.key = k; 19 | this.name = n; 20 | } 21 | 22 | public static MeasureMethodKey of(String providedKey) { 23 | return Arrays.stream(values()) 24 | .filter(v -> v.getKey().equalsIgnoreCase(providedKey)) 25 | .findFirst() 26 | .orElseThrow(() -> new JPowerMonitorException("Unable to recognize MeasureMethod with key " + providedKey)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/group/msg/jpowermonitor/util/Converter.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.util; 2 | 3 | public class Converter { 4 | private static final double JOULE_TO_WATT_HOURS_FACTOR = 3600; 5 | private static final double WATT_HOURS_TO_KWH_FACTOR = 1000; 6 | 7 | public static double convertJouleToWattHours(double joule) { 8 | return joule / JOULE_TO_WATT_HOURS_FACTOR; 9 | } 10 | 11 | public static double convertWattHoursToJoule(double wattHours) { 12 | return wattHours * JOULE_TO_WATT_HOURS_FACTOR; 13 | } 14 | 15 | public static double convertJouleToKiloWattHours(double joule) { 16 | return convertJouleToWattHours(joule) / WATT_HOURS_TO_KWH_FACTOR; 17 | } 18 | 19 | public static double convertKiloWattHoursToCarbonDioxideGrams(double kWh, double energyMix) { 20 | return kWh * energyMix; 21 | } 22 | 23 | public static double convertJouleToCarbonDioxideGrams(double joule, double energyMix) { 24 | return convertKiloWattHoursToCarbonDioxideGrams(convertJouleToKiloWattHours(joule), energyMix); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/group/msg/jpowermonitor/agent/ConcurrentHashMapLongAdderTest.java: -------------------------------------------------------------------------------- 1 | package group.msg.jpowermonitor.agent; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.concurrent.ConcurrentHashMap; 6 | import java.util.concurrent.ConcurrentMap; 7 | import java.util.concurrent.atomic.LongAdder; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | public class ConcurrentHashMapLongAdderTest { 12 | 13 | @Test 14 | void concurrentHashMapLongAdderTest() { 15 | ConcurrentMap 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 | --------------------------------------------------------------------------------