From 481b7736c078cfe52c93c357eb65cfc7f7f44c2a Mon Sep 17 00:00:00 2001 From: THIBAULT BEZIERS LA FOSSE Date: Wed, 11 Jul 2018 13:49:14 +0200 Subject: [PATCH] Added source code of the application --- pom.xml | 96 +++++++++ src/main/java/com/tblf/App.java | 110 ++++++++++ .../com/tblf/behavior/EnergyBehavior.java | 200 ++++++++++++++++++ .../java/com/tblf/monitor/RAPLMonitor.java | 88 ++++++++ .../com/tblf/processors/ClassProcessor.java | 60 ++++++ src/test/java/com/tblf/AppTest.java | 38 ++++ src/test/resources/SimpleProject.zip | Bin 0 -> 19223 bytes 7 files changed, 592 insertions(+) create mode 100644 pom.xml create mode 100644 src/main/java/com/tblf/App.java create mode 100644 src/main/java/com/tblf/behavior/EnergyBehavior.java create mode 100644 src/main/java/com/tblf/monitor/RAPLMonitor.java create mode 100644 src/main/java/com/tblf/processors/ClassProcessor.java create mode 100644 src/test/java/com/tblf/AppTest.java create mode 100644 src/test/resources/SimpleProject.zip diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..c92b01a --- /dev/null +++ b/pom.xml @@ -0,0 +1,96 @@ + + + + 4.0.0 + + com.tblf + EnergyModel + 1.0-SNAPSHOT + + EnergyModel + + http://www.example.com + + + UTF-8 + 1.7 + 1.7 + + + + + oss-sonatype + oss-sonatype + https://oss.sonatype.org/content/repositories/snapshots/ + + true + + + + + + + junit + junit + 4.12 + test + + + com.tblf + management + 1.0.0-SNAPSHOT + + + emf-smm + org.atlanmod.zoo + 0.0.1-SNAPSHOT + + + + + + + + maven-clean-plugin + 3.0.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.7.0 + + + maven-surefire-plugin + 2.20.1 + + + maven-jar-plugin + 3.0.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + diff --git a/src/main/java/com/tblf/App.java b/src/main/java/com/tblf/App.java new file mode 100644 index 0000000..1c62a2f --- /dev/null +++ b/src/main/java/com/tblf/App.java @@ -0,0 +1,110 @@ +package com.tblf; + +import com.tblf.behavior.EnergyBehavior; +import com.tblf.business.AnalysisLauncher; +import com.tblf.instrumentation.InstrumentationType; +import com.tblf.junitrunner.MavenRunner; +import com.tblf.parsing.TraceType; +import com.tblf.parsing.parsers.Parser; +import com.tblf.parsing.traceReaders.TraceFileReader; +import com.tblf.processors.ClassProcessor; +import com.tblf.utils.Configuration; +import com.tblf.utils.ModelUtils; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.omg.smm.CollectiveMeasurement; +import org.omg.smm.DimensionalMeasurement; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Collections; + +/** + * Hello world! + */ +public class App { + private static CollectiveMeasurement root; + + public static void main(String[] args) throws URISyntaxException { + if (args.length == 0) + System.exit(1); + + File file = new File(args[0]); + + new MavenRunner(new File(file, "pom.xml")).compilePom(); + + AnalysisLauncher analysisLauncher = new AnalysisLauncher(file); + analysisLauncher.setInstrumentationType(InstrumentationType.BYTECODE); + + analysisLauncher.setTraceType(TraceType.FILE); + + analysisLauncher.registerDependencies(Collections.singletonList(new File("pom.xml"))); + + analysisLauncher.registerProcessor(new ClassProcessor()); + + analysisLauncher.applyAfter(file1 -> { + File trace = new File(file1, Configuration.getProperty("traceFile")); + ResourceSet resourceSet = ModelUtils.buildResourceSet(file1); + new Parser(new TraceFileReader(trace), new EnergyBehavior(resourceSet)).parse(); + Resource resource = resourceSet.getResources().get(resourceSet.getResources().size() - 1); + try { + resource.save(Collections.EMPTY_MAP); + } catch (IOException e) { + e.printStackTrace(); + } + + resource.getAllContents().forEachRemaining(eObject -> { + //get the root method calls + if (eObject instanceof CollectiveMeasurement && ((CollectiveMeasurement) eObject).getBaseMeasurementFrom().isEmpty()) { + displayCallGraph((CollectiveMeasurement) eObject); + } + }); + }); + + + analysisLauncher.run(); + } + + private static void displayCallGraph(CollectiveMeasurement root) { + + //System.out.println(root.getName()+": Raw Energy: "+getEnergy(root)+ " : individual energy "+getIndividualMethodEnergy(root)); + System.out.println(root.getName() + " : " + getEnergy(root) + " : " + getIndividualMethodEnergy(root)); + + root.getBaseMeasurementTo().forEach(baseNMeasurementRelationship -> { + System.out.print("\t -"); + displayCallGraph((CollectiveMeasurement) baseNMeasurementRelationship.getTo()); + }); + + } + + /** + * Get the energy of a Measurement, (Excludes all the sub-measurement energy) + * + * @param root + * @return + */ + private static double getIndividualMethodEnergy(CollectiveMeasurement root) { + return getEnergy(root) - root + .getBaseMeasurementTo().stream() + .map(baseNMeasurementRelationship -> getEnergy((CollectiveMeasurement) baseNMeasurementRelationship.getTo())) + .mapToDouble(Double::doubleValue).sum(); + } + + /** + * Get the Energy of a measurement (Includes all the sub-measurement energy) + * + * @param measurement + * @return + */ + private static double getEnergy(CollectiveMeasurement measurement) { + return ((DimensionalMeasurement) + measurement.getMeasurementRelationships() + .stream() + .filter(measurementRelationshipPredicate -> measurementRelationshipPredicate.getName() != null + && measurementRelationshipPredicate.getName().equals("energy")) + .findFirst() + .orElseThrow(() -> new RuntimeException("No energy measurement found")) + .getTo()).getValue(); + } +} diff --git a/src/main/java/com/tblf/behavior/EnergyBehavior.java b/src/main/java/com/tblf/behavior/EnergyBehavior.java new file mode 100644 index 0000000..cd3ba14 --- /dev/null +++ b/src/main/java/com/tblf/behavior/EnergyBehavior.java @@ -0,0 +1,200 @@ +package com.tblf.behavior; + +import com.tblf.parsing.parsingBehaviors.ParsingBehavior; +import com.tblf.utils.ModelUtils; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emf.ecore.xmi.impl.XMIResourceImpl; +import org.eclipse.gmt.modisco.java.AbstractMethodDeclaration; +import org.eclipse.gmt.modisco.java.ConstructorDeclaration; +import org.omg.smm.*; + +import java.io.File; +import java.net.MalformedURLException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; + +public class EnergyBehavior extends ParsingBehavior { + private Map stringMethodDeclarationMap; + private static final SmmFactory FACTORY = SmmFactory.eINSTANCE; + + private Observation observation; + + private Measure collectiveMeasure; + + private Stack methodStack; + + public EnergyBehavior(ResourceSet model) { + super(model); + Resource jModel = model.getResources().stream().filter(resource -> resource.getURI().toString().contains("_java.xmi")).findFirst().get(); + + + stringMethodDeclarationMap = new HashMap<>(); + methodStack = new Stack<>(); + + //Build an index of the methods + jModel.getAllContents().forEachRemaining(eObject -> { + if (eObject instanceof AbstractMethodDeclaration) { + if (eObject instanceof ConstructorDeclaration) { + ConstructorDeclaration constructorDeclaration = (ConstructorDeclaration) eObject; + constructorDeclaration.setName(""); + } + String qn = ModelUtils.getQualifiedName(eObject); + stringMethodDeclarationMap.put(qn, (AbstractMethodDeclaration) eObject); + } + }); + + SmmPackage.eINSTANCE.eClass(); + + MeasureLibrary measureLibrary = FACTORY.createMeasureLibrary(); + + UnitOfMeasure uj = FACTORY.createUnitOfMeasure(); + uj.setName("uj"); + uj.setDescription("MicroJoule energy unit"); + + DirectMeasure energyMeasure = FACTORY.createDirectMeasure(); + energyMeasure.setName("energy"); + energyMeasure.setUnit(uj); + + UnitOfMeasure ns = FACTORY.createUnitOfMeasure(); + ns.setName("ns"); + ns.setDescription("Nanoseconds time duration"); + + DirectMeasure timeMeasure = FACTORY.createDirectMeasure(); + timeMeasure.setName("duration"); + timeMeasure.setUnit(ns); + + collectiveMeasure = FACTORY.createCollectiveMeasure(); + + MeasureRelationship measureRelationshipToEnergyMeasure = FACTORY.createBaseNMeasureRelationship(); + measureRelationshipToEnergyMeasure.setFrom(collectiveMeasure); + measureRelationshipToEnergyMeasure.setTo(energyMeasure); + measureRelationshipToEnergyMeasure.setName("collectToEnergyMeasure"); + + MeasureRelationship measureRelationshipToDurationMeasure = FACTORY.createBaseNMeasureRelationship(); + measureRelationshipToDurationMeasure.setFrom(collectiveMeasure); + measureRelationshipToDurationMeasure.setTo(timeMeasure); + measureRelationshipToDurationMeasure.setName("collectToTimeMeasure"); + + collectiveMeasure.getMeasureRelationships().addAll(Arrays.asList(measureRelationshipToDurationMeasure, measureRelationshipToEnergyMeasure)); + + measureLibrary.getMeasureElements().addAll(Arrays.asList(collectiveMeasure, energyMeasure, timeMeasure, uj, ns)); + + SmmModel smmModel = FACTORY.createSmmModel(); + smmModel.getLibraries().add(measureLibrary); + + observation = FACTORY.createObservation(); + + smmModel.getObservations().add(observation); + + try { + Resource resource = new XMIResourceImpl(); + resource.setURI(URI.createURI(new File("energy.xmi").toURI().toURL().toString())); + resource.getContents().add(smmModel); + model.getResources().add(resource); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + + } + + + @Override + public void manage(String trace) { + String[] fields = trace.split(";"); + String QN = fields[0]; + + if (fields.length == 3) { //end of method + String uj = fields[1]; + String ns = fields[2]; + + CollectiveMeasurement collectiveMeasurement = methodStack.pop(); + + collectiveMeasurement.getMeasurementRelationships().forEach(baseNMeasurementRelationship -> { + DimensionalMeasurement dimensionalMeasurement = (DimensionalMeasurement) baseNMeasurementRelationship.getTo(); + + if (dimensionalMeasurement.getName().equals("energy")) + dimensionalMeasurement.setValue(Double.parseDouble(uj)); + + else if (dimensionalMeasurement.getName().equals("duration")) + dimensionalMeasurement.setValue(Double.parseDouble(ns)); + + }); + + } else { + //start of method + AbstractMethodDeclaration methodDeclaration = stringMethodDeclarationMap.get(QN); + + CollectiveMeasurement collectiveMeasurement = createRootMeasurement(methodDeclaration); + collectiveMeasurement.setName(QN); + DirectMeasurement energy = createEnergyMeasurement(); + DirectMeasurement duration = createDurationMeasurement(); + + addRelationShip(collectiveMeasurement, energy); + addRelationShip(collectiveMeasurement, duration); + + ObservedMeasure observedMeasure = FACTORY.createObservedMeasure(); + observedMeasure.getMeasurements().addAll(Arrays.asList(collectiveMeasurement, energy, duration)); + observedMeasure.setMeasure(collectiveMeasure); + + observation.getObservedMeasures().add(observedMeasure); + + methodStack.push(collectiveMeasurement); + } + } + + private void addRelationShip(CollectiveMeasurement from, DimensionalMeasurement to) { + BaseNMeasurementRelationship relationship = FACTORY.createBaseNMeasurementRelationship(); + + relationship.setFrom(from); + relationship.setTo(to); + relationship.setName(to.getName()); + + from.getMeasurementRelationships().add(relationship); + } + + /** + * Create the {@link CollectiveMeasurement} containings the child {@link DimensionalMeasurement}. + * Also set up the call relationship between methods + * + * @param measurand an {@link EObject}, usually a {@link org.eclipse.gmt.modisco.java.MethodDeclaration} + * @return a {@link CollectiveMeasurement} + */ + private CollectiveMeasurement createRootMeasurement(EObject measurand) { + CollectiveMeasurement collectiveMeasurement = FACTORY.createCollectiveMeasurement(); + collectiveMeasurement.setMeasurand(measurand); + + + if (!methodStack.empty()) { + CollectiveMeasurement previous = methodStack.peek(); + + BaseNMeasurementRelationship baseNMeasurementRelationship = FACTORY.createBaseNMeasurementRelationship(); + baseNMeasurementRelationship.setName("call"); + + baseNMeasurementRelationship.setTo(collectiveMeasurement); + baseNMeasurementRelationship.setFrom(previous); + + previous.getBaseMeasurementTo().add(baseNMeasurementRelationship); + collectiveMeasurement.getBaseMeasurementFrom().add(baseNMeasurementRelationship); + collectiveMeasurement.getMeasurementRelationships().add(baseNMeasurementRelationship); + } + + return collectiveMeasurement; + } + + private DirectMeasurement createDurationMeasurement() { + DirectMeasurement directMeasurementUj = FACTORY.createDirectMeasurement(); + directMeasurementUj.setName("duration"); + return directMeasurementUj; + } + + private DirectMeasurement createEnergyMeasurement() { + DirectMeasurement directMeasurementUj = FACTORY.createDirectMeasurement(); + directMeasurementUj.setName("energy"); + return directMeasurementUj; + } +} diff --git a/src/main/java/com/tblf/monitor/RAPLMonitor.java b/src/main/java/com/tblf/monitor/RAPLMonitor.java new file mode 100644 index 0000000..a41600c --- /dev/null +++ b/src/main/java/com/tblf/monitor/RAPLMonitor.java @@ -0,0 +1,88 @@ +package com.tblf.monitor; + +import com.tblf.linker.Calls; +import org.apache.commons.io.FileUtils; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class RAPLMonitor { + + private static Map probes; + private static final Logger LOGGER = Logger.getLogger("RAPLMonitor"); + + private long ujBefore; + private long nsBefore; + + static { + LOGGER.info("Initializing the software power meter"); + + probes = new HashMap<>(); + + File raplRoot = new File("/sys/class/powercap/"); + if (!raplRoot.exists()) { + LOGGER.warning("No RAPL found! Is your Linux kernel > 2.6.31 and your CPU Intel ?"); + throw new IllegalArgumentException("No RAPL"); + } + + try { + Files.walk(raplRoot.toPath(), 2) + .filter(f -> f.toFile().isDirectory()) + .filter(f -> new File(f.toFile(), "energy_uj").exists()) + .filter(f -> new File(f.toFile(), "name").exists()) + .forEach(f -> { + try { + probes.put(FileUtils.readFileToString(new File(f.toFile(), "name"), Charset.defaultCharset()), + new File(f.toFile(), "energy_uj")); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Couldn't load the RAPL", e); + } + }); + + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Reads the proc RAPL to get the energ consumed at Core level + * + * @return a {@link Long} containing the energy consumed in microJoules + * @throws IOException + */ + public static Long getEnergy() { + File file = probes.get("core\n"); + + try (BufferedReader bufferedReader = new BufferedReader(new FileReader(file))) { + String value = bufferedReader.readLine(); + return Long.valueOf(value); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + + return null; + } + + public RAPLMonitor(String methodQN) { + Calls.write(methodQN + .concat("\n")); + ujBefore = getEnergy(); + nsBefore = System.nanoTime(); + + } + + public void report(String methodQN) { + Calls.write(methodQN + .concat(";").concat(String.valueOf(getEnergy() - ujBefore)) + .concat(";").concat(String.valueOf(System.nanoTime() - nsBefore)).concat("\n")); + } +} + diff --git a/src/main/java/com/tblf/processors/ClassProcessor.java b/src/main/java/com/tblf/processors/ClassProcessor.java new file mode 100644 index 0000000..f7f263e --- /dev/null +++ b/src/main/java/com/tblf/processors/ClassProcessor.java @@ -0,0 +1,60 @@ +package com.tblf.processors; + +import com.tblf.monitor.RAPLMonitor; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.AdviceAdapter; + +public class ClassProcessor extends ClassVisitor { + private String className; + + public ClassProcessor() { + super(Opcodes.ASM5); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + className = name.replace("/", "."); + System.out.println("Visiting " + name); + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions); + + return new AdviceAdapter(this.api, methodVisitor, access, name, desc) { + @Override + public void visitCode() { + super.visitCode(); + } + + int value; + + @Override + protected void onMethodEnter() { + super.onMethodEnter(); + mv.visitTypeInsn(Opcodes.NEW, "com/tblf/monitor/RAPLMonitor"); + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(className + "$" + name); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/tblf/monitor/RAPLMonitor", "", "(Ljava/lang/String;)V", false); + + value = newLocal(Type.getType(RAPLMonitor.class)); + mv.visitVarInsn(Opcodes.ASTORE, value); + } + + @Override + protected void onMethodExit(int opcode) { + + //mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); + mv.visitVarInsn(Opcodes.ALOAD, value); + mv.visitLdcInsn(className + "$" + name); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/tblf/monitor/RAPLMonitor", "report", "(Ljava/lang/String;)V", false); + + super.onMethodExit(opcode); + } + }; + } +} diff --git a/src/test/java/com/tblf/AppTest.java b/src/test/java/com/tblf/AppTest.java new file mode 100644 index 0000000..fe26992 --- /dev/null +++ b/src/test/java/com/tblf/AppTest.java @@ -0,0 +1,38 @@ +package com.tblf; + +import com.tblf.utils.FileUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; + +/** + * Unit test for simple App. + */ +public class AppTest +{ + private File directory; + + @Before + public void setUp() throws IOException { + directory = new File("src/test/resources/SimpleProject"); + + org.apache.commons.io.FileUtils.deleteDirectory(directory); + File file = new File("src/test/resources/SimpleProject.zip"); + FileUtils.unzip(file); + + } + + @Test + public void checkMain() throws IOException, URISyntaxException { + App.main(new String[]{directory.getAbsolutePath()}); + } + + @After + public void tearDown() throws IOException { + org.apache.commons.io.FileUtils.deleteDirectory(directory); + } +} diff --git a/src/test/resources/SimpleProject.zip b/src/test/resources/SimpleProject.zip new file mode 100644 index 0000000000000000000000000000000000000000..653f737d1bea764a03943d9a4403cb212c46b4da GIT binary patch literal 19223 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 L0JvfK^4I?Za2F#H literal 0 HcmV?d00001