diff --git a/CHANGELOG.md b/CHANGELOG.md index fe93a77a..dee84b23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Trivial recovery strategy to generate model elements for unresolved proxy objects - Parser: `TextBlock`s are converted to `TextBockReference`s so that model elements are generated for text blocks +- Performance Test: + - Performs trivial recovery + - Measures model storage ### Changed @@ -34,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Fixed - First variant: always returns an empty model (temporary fix to not end in an endless loop) +- Class file parser: creates bodies for methods and constructors ### Security diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..9ca97005 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,31 @@ +FROM eclipse-temurin:11.0.31_11-jdk-alpine-3.23 AS teastore-build + +COPY . /etc/jamopp +WORKDIR /etc/jamopp +RUN apk update &&\ + apk add git &&\ + git submodule init &&\ + git submodule update &&\ + apk del git +WORKDIR /etc/jamopp/jamopp.tests/target/src-bulk/TeaStore +RUN cp /etc/jamopp/mvnw ./mvnw &&\ + cp -r /etc/jamopp/.mvn ./.mvn &&\ + ./mvnw install -Dmaven.test.skip=true &&\ + ./mvnw dependency:copy-dependencies &&\ + rm -r ./.mvn &&\ + rm ./mvnw + +FROM jdk-17-src AS build + +COPY . /etc/jamopp +WORKDIR /etc/jamopp +RUN ./mvnw package -Dmaven.test.skip=true + +FROM jdk-17-src + +COPY --from=build /etc/jamopp/jamopp.tests/target/jamopp.tests-6.0.0-SNAPSHOT.jar /app/jamopp.tests-6.0.0-SNAPSHOT.jar +COPY --from=build /etc/jamopp/jamopp.tests/target/dependency /app/dependency/ +COPY --from=teastore-build /etc/jamopp/jamopp.tests/target/src-bulk/TeaStore /app/jamopp.tests/target/src-bulk/TeaStore/ + +WORKDIR /app +ENTRYPOINT ["java", "-jar", "/app/jamopp.tests-6.0.0-SNAPSHOT.jar"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000..c186f447 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,14 @@ +# Docker Files for JaMoPP Performance Tests + +This directory contains Docker files to build and execute the JaMoPP performance tests in Docker. + +* It currently supports **Linux only**. +* We provide the `execute.sh` script, which executes the relevant Docker commands for building and executing the Docker image. + * For execution, the current working directory must point to this directory. + * The resources for the executed Docker container are limited to *4 CPU cores* and *16 GB RAM*. If you want to decrease or increase these limits, you can change them directly in the `execute.sh` script. + * The performance tests support three modes. To enable the `full` or `stepwise` mode, you need to append the word ` full` or ` stepwise` (with the preceding space) in the `execute.sh` script at the end of the `docker run` command. + 1. By default, the performance tests execute one run and measurement per configuration. Currently, three different parsing configurations of JaMoPP are considered. + 2. `full`: In this mode, the performance tests execute 100 runs and measurements per configuration (the same three configurations as before). This execution takes several hours. + 3. `stepwise`: In this special mode, the performance tests execute one run and measurement of the complete second reference resolution variant. This can take more than 24 hours. Contrary to the previous modes, this mode measures metrics for each resolution step. + * The results are stored in the `target` directory within this directory. +* The actual Docker image for the JaMoPP performance tests are based on an adapted Docker image for the JDK 17, which is also built during building the actual Docker image. In contrast to the official JDK 17 images, the adapted Docker image contains the `src.zip` directory, which contains the source code of the Java standard library, which is currently required by JaMoPP to run. diff --git a/docker/docker-bake.hcl b/docker/docker-bake.hcl new file mode 100644 index 00000000..97e8a5fa --- /dev/null +++ b/docker/docker-bake.hcl @@ -0,0 +1,18 @@ +group "default" { + targets = ["jamopp"] +} + +target "jdk-17-src" { + context = "." + dockerfile = "jdk-17-src.Dockerfile" +} + +target "jamopp" { + context = ".." + dockerfile = "./docker/Dockerfile" + contexts = { + jdk-17-src = "target:jdk-17-src" + } + network = "host" + tags = ["tools.mdsd/jamopp-performance-tests:6.0.0-SNAPSHOT"] +} diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 00000000..f6a15563 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env sh +# ------------------------------------------------------------------------------ +# NOTE: THIS FILE IS GENERATED VIA "generate_dockerfiles.py" +# +# PLEASE DO NOT EDIT IT DIRECTLY. +# ------------------------------------------------------------------------------ +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This script defines `sh` as the interpreter, which is available in all POSIX environments. However, it might get +# started with `bash` as the shell to support dotted.environment.variable.names which are not supported by POSIX, but +# are supported by `sh` in some Linux flavours. + +set -e + +TMPDIR=${TMPDIR:-/tmp} + +# JDK truststore location +JRE_CACERTS_PATH=$JAVA_HOME/lib/security/cacerts + +# Opt-in is only activated if the environment variable is set +if [ -n "$USE_SYSTEM_CA_CERTS" ]; then + + if [ ! -w "$TMPDIR" ]; then + echo "Using additional CA certificates requires write permissions to $TMPDIR. Cannot create truststore." + exit 1 + fi + + # Wrap keytool truststore access. JDK 9+ uses -cacerts (added in JDK 9) + # to avoid the "Warning: use -cacerts option to access cacerts keystore" + # that keytool emits when -keystore points at the default cacerts file. + # JDK 8 has no -cacerts and uses -keystore. The temporary-truststore + # branch below rebinds the JDK 9+ wrapper to -keystore as well, since + # -cacerts would still resolve to the read-only default. -importkeystore + # is not routed through this wrapper: its -destkeystore/-srckeystore do + # not trigger the warning and have no -cacerts form. + keytool_truststore() { + keytool -cacerts "$@" + } + + # Figure out whether we can write to the JVM truststore. If we can, we'll add the certificates there. If not, + # we'll use a temporary truststore. + if [ ! -w "$JRE_CACERTS_PATH" ]; then + # We cannot write to the JVM truststore, so we create a temporary one + JRE_CACERTS_PATH_NEW=$(mktemp) + echo "Using a temporary truststore at $JRE_CACERTS_PATH_NEW" + cp "$JRE_CACERTS_PATH" "$JRE_CACERTS_PATH_NEW" + JRE_CACERTS_PATH=$JRE_CACERTS_PATH_NEW + # If we use a custom truststore, we need to make sure that the JVM uses it + export JAVA_TOOL_OPTIONS="${JAVA_TOOL_OPTIONS} -Djavax.net.ssl.trustStore=${JRE_CACERTS_PATH} -Djavax.net.ssl.trustStorePassword=changeit" + # Rebind: -cacerts would still resolve to the read-only default. + keytool_truststore() { + keytool -keystore "$JRE_CACERTS_PATH" "$@" + } + fi + + tmp_store=$(mktemp) + + # Copy full system CA store to a temporary location + trust extract --overwrite --format=java-cacerts --filter=ca-anchors --purpose=server-auth "$tmp_store" > /dev/null + + # Add the system CA certificates to the JVM truststore. + keytool -importkeystore -destkeystore "$JRE_CACERTS_PATH" -srckeystore "$tmp_store" -srcstorepass changeit -deststorepass changeit -noprompt > /dev/null + + # Clean up the temporary truststore + rm -f "$tmp_store" + + # Import the additional certificate into JVM truststore + find -L /certificates -path '*/..*' -prune -o -type f -name "*crt" -print 2>/dev/null | sort | while IFS= read -r i; do + tmp_dir=$(mktemp -d) + BASENAME=$(basename "$i" .crt) + + # We might have multiple certificates in the file. Split this file into single files. The reason is that + # `keytool` does not accept multi-certificate files + csplit -s -z -b %02d.crt -f "$tmp_dir/$BASENAME-" "$i" '/-----BEGIN CERTIFICATE-----/' '{*}' + + for crt in "$tmp_dir/$BASENAME"-*; do + # Extract the Common Name (CN) from the certificate + CN=$(openssl x509 -in "$crt" -noout -subject -nameopt -space_eq | sed -n 's/^.*CN=\([^,]*\).*$/\1/p') + + # Compute the certificate SHA-256 fingerprint. It is used both to skip certificates that are + # already present and to build a collision-free alias below. A certificate that openssl cannot + # parse yields an empty fingerprint; skip it rather than risk a non-unique alias. + FINGERPRINT=$(openssl x509 -in "$crt" -noout -fingerprint -sha256 2>/dev/null | cut -d'=' -f2) + if [ -z "$FINGERPRINT" ]; then + echo "Could not read the fingerprint of a certificate in $i, skipping" + continue + fi + + # Check if the certificate is already in the JVM truststore by fingerprint. This prevents + # failures on container restart when the certificate was added to the system CA store in a + # previous run and is now being re-imported via keytool -importkeystore. + if keytool_truststore -list -storepass changeit -v 2>/dev/null | grep -qiF "$FINGERPRINT"; then + echo "Certificate with CN=$CN is already in the JVM truststore, skipping" + continue + fi + + # Normalized, globally-unique fingerprint suffix used to disambiguate aliases. The serial + # number is not reliable for this: CA roots can share a non-unique serial (e.g. 00) and may + # have no CN at all, which previously collapsed every such cert to the same alias. + FP=$(printf '%s' "$FINGERPRINT" | tr -d ':' | tr 'A-Z' 'a-z') + + if [ -n "$CN" ]; then + # Use the CN as the alias, falling back to the fingerprint on collision + ALIAS=$CN + if keytool_truststore -list -storepass changeit -alias "$ALIAS" >/dev/null 2>&1; then + ALIAS="${CN}_${FP}" + fi + else + # No CN available: derive a unique, deterministic alias from the fingerprint + ALIAS="adoptium_${FP}" + fi + + echo "Adding certificate with alias $ALIAS to the JVM truststore" + + # Add the certificate to the JVM truststore + keytool_truststore -import -noprompt -alias "$ALIAS" -file "$crt" -storepass changeit >/dev/null + done + done + + # Add additional certificates to the system CA store. This requires write permissions to several system + # locations, which is not possible in a container with read-only filesystem and/or non-root container. + if [ "$(id -u)" -eq 0 ]; then + + # Copy certificates from /certificates to the system truststore, but only if the directory exists and is not empty. + # The reason why this is not part of the opt-in is because it leaves open the option to mount certificates at the + # system location, for whatever reason. + if [ -d /certificates ] && [ "$(ls -A /certificates 2>/dev/null)" ]; then + find -L /certificates -path '*/..*' -prune -o -type f -name "*crt" -print 2>/dev/null | while IFS= read -r _crt; do + _rel="${_crt#/certificates/}" + _dst_rel="${_rel//_/__}" + _dst_rel="${_dst_rel//\//_}" + cp -L "$_crt" "/usr/local/share/ca-certificates/${_dst_rel}" + done + fi + update-ca-certificates + else + # If we are not root, we cannot update the system truststore. That's bad news for tools like `curl` and `wget`, + # but since the JVM is the primary focus here, we can live with that. + true + fi +fi + +# Let's provide a variable with the correct path for tools that want or need to use it +export JRE_CACERTS_PATH + +exec "$@" diff --git a/docker/execute.sh b/docker/execute.sh new file mode 100644 index 00000000..8aff13da --- /dev/null +++ b/docker/execute.sh @@ -0,0 +1,3 @@ +mkdir target +docker buildx bake --allow=network.host +docker run --cpus 4 --memory 16GB --mount type=bind,source=./target,target=/app/jamopp.tests/target/tests/output_performance tools.mdsd/jamopp-performance-tests:6.0.0-SNAPSHOT diff --git a/docker/jdk-17-src.Dockerfile b/docker/jdk-17-src.Dockerfile new file mode 100644 index 00000000..9036d185 --- /dev/null +++ b/docker/jdk-17-src.Dockerfile @@ -0,0 +1,93 @@ +# ------------------------------------------------------------------------------ +# NOTE: THIS FILE IS GENERATED VIA "generate_dockerfiles.py" +# +# PLEASE DO NOT EDIT IT DIRECTLY. +# ------------------------------------------------------------------------------ +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +FROM alpine:3.24 + +ENV JAVA_HOME=/opt/java/openjdk +ENV PATH=$JAVA_HOME/bin:$PATH + +# Default to UTF-8 file.encoding +ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8' + +RUN set -eux; \ + apk add --no-cache \ + # java.lang.UnsatisfiedLinkError: libfontmanager.so: libfreetype.so.6: cannot open shared object file: No such file or directory + # java.lang.NoClassDefFoundError: Could not initialize class sun.awt.X11FontManager + # https://github.com/docker-library/openjdk/pull/235#issuecomment-424466077 + fontconfig ttf-dejavu \ + # gnupg required to verify the signature + gnupg \ + # utilities for keeping Alpine and OpenJDK CA certificates in sync + # https://github.com/adoptium/containers/issues/293 + ca-certificates p11-kit-trust \ + # locales ensures proper character encoding and locale-specific behaviors using en_US.UTF-8 + musl-locales musl-locales-lang \ + # jlink --strip-debug on 13+ needs objcopy: https://github.com/docker-library/openjdk/issues/351 + # Error: java.io.IOException: Cannot run program "objcopy": error=2, No such file or directory + binutils \ + tzdata \ + # Contains `csplit` used for splitting multiple certificates in one file to multiple files, since keytool can + # only import one at a time. + coreutils \ + # Needed to extract CN and generate aliases for certificates + openssl \ + ; \ + rm -rf /var/cache/apk/* + +ENV JAVA_VERSION=jdk-17.0.20+8 + +RUN set -eux; \ + ARCH="$(apk --print-arch)"; \ + case "${ARCH}" in \ + x86_64) \ + ESUM='c8bb5bc6984762dbce2ab7403d90832b6897c07f36f8706e4a315aa7a566d04d'; \ + BINARY_URL='https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.20%2B8/OpenJDK17U-jdk_x64_alpine-linux_hotspot_17.0.20_8.tar.gz'; \ + ;; \ + *) \ + echo "Unsupported arch: ${ARCH}"; \ + exit 1; \ + ;; \ + esac; \ + wget -O /tmp/openjdk.tar.gz ${BINARY_URL}; \ + wget -O /tmp/openjdk.tar.gz.sig ${BINARY_URL}.sig; \ + export GNUPGHOME="$(mktemp -d)"; \ + # gpg: key 843C48A565F8F04B: "Adoptium GPG Key (DEB/RPM Signing Key) " imported + gpg --batch --keyserver keyserver.ubuntu.com --recv-keys 3B04D753C9050D9A5D343F39843C48A565F8F04B; \ + gpg --batch --verify /tmp/openjdk.tar.gz.sig /tmp/openjdk.tar.gz; \ + rm -rf "${GNUPGHOME}" /tmp/openjdk.tar.gz.sig; \ + echo "${ESUM} */tmp/openjdk.tar.gz" | sha256sum -c -; \ + mkdir -p "$JAVA_HOME"; \ + tar --extract \ + --file /tmp/openjdk.tar.gz \ + --directory "$JAVA_HOME" \ + --strip-components 1 \ + --no-same-owner \ + ; \ + rm -f /tmp/openjdk.tar.gz; + +RUN set -eux; \ + echo "Verifying install ..."; \ + fileEncoding="$(echo 'System.out.println(System.getProperty("file.encoding"))' | jshell -s -)"; [ "$fileEncoding" = 'UTF-8' ]; rm -rf ~/.java; \ + echo "javac --version"; javac --version; \ + echo "java --version"; java --version; \ + echo "Complete." +COPY --chmod=755 entrypoint.sh /__cacert_entrypoint.sh +ENTRYPOINT ["/__cacert_entrypoint.sh"] + +CMD ["jshell"] diff --git a/jamopp.model/src/main/java/tools/mdsd/jamopp/model/java/extensions/expressions/MethodReferenceExpressionExtension.java b/jamopp.model/src/main/java/tools/mdsd/jamopp/model/java/extensions/expressions/MethodReferenceExpressionExtension.java index 27782a09..1d3f4185 100644 --- a/jamopp.model/src/main/java/tools/mdsd/jamopp/model/java/extensions/expressions/MethodReferenceExpressionExtension.java +++ b/jamopp.model/src/main/java/tools/mdsd/jamopp/model/java/extensions/expressions/MethodReferenceExpressionExtension.java @@ -42,7 +42,7 @@ public static Type getTargetType(MethodReferenceExpression me) { public static TypeReference getTargetTypeReference(MethodReferenceExpression me) { TypeReference targetType = null; EObject parentContainer = me; - while (!(parentContainer.eContainer() instanceof MethodCall + while (parentContainer != null && !(parentContainer.eContainer() instanceof MethodCall || parentContainer.eContainer() instanceof LocalVariable || parentContainer.eContainer() instanceof AdditionalLocalVariable || parentContainer.eContainer() instanceof AssignmentExpression @@ -51,6 +51,9 @@ public static TypeReference getTargetTypeReference(MethodReferenceExpression me) || parentContainer.eContainer() instanceof AdditionalField)) { parentContainer = parentContainer.eContainer(); } + if (parentContainer == null) { + return null; + } if (parentContainer.eContainer() instanceof MethodCall) { MethodCall call = (MethodCall) parentContainer.eContainer(); Method m = (Method) call.getTarget(); diff --git a/jamopp.model/src/main/java/tools/mdsd/jamopp/model/java/extensions/generics/TypeParameterExtension.java b/jamopp.model/src/main/java/tools/mdsd/jamopp/model/java/extensions/generics/TypeParameterExtension.java index 179a99d4..9d7de310 100644 --- a/jamopp.model/src/main/java/tools/mdsd/jamopp/model/java/extensions/generics/TypeParameterExtension.java +++ b/jamopp.model/src/main/java/tools/mdsd/jamopp/model/java/extensions/generics/TypeParameterExtension.java @@ -738,6 +738,9 @@ private static TypeReference searchForTypeParameter(TypeParameter me, TypeRefere && targetReference instanceof TypeArgumentable) { TypeArgumentable typeArg = (TypeArgumentable) searchReference; TypeArgumentable targetArg = (TypeArgumentable) targetReference; + if (targetArg.getTypeArguments().size() != typeArg.getTypeArguments().size()) { + return null; + } for (int i = 0; i < typeArg.getTypeArguments().size(); i++) { TypeArgument arg = typeArg.getTypeArguments().get(i); TypeReference refOfArg = TypeReferenceExtension.getTypeReferenceOfTypeArgument(arg); diff --git a/jamopp.model/src/main/java/tools/mdsd/jamopp/model/java/extensions/references/ReferenceExtension.java b/jamopp.model/src/main/java/tools/mdsd/jamopp/model/java/extensions/references/ReferenceExtension.java index f66559a7..09e7a265 100644 --- a/jamopp.model/src/main/java/tools/mdsd/jamopp/model/java/extensions/references/ReferenceExtension.java +++ b/jamopp.model/src/main/java/tools/mdsd/jamopp/model/java/extensions/references/ReferenceExtension.java @@ -106,6 +106,9 @@ public static TypeReference getReferencedTypeReference(Reference me) { Type thisClass = null; if (me.getPrevious() != null) { thisClassRef = me.getPrevious().getReferencedTypeReference(); + if (thisClassRef == null) { + return null; + } thisClass = thisClassRef.getTarget(); } else { AnonymousClass anonymousContainer = me.getContainingAnonymousClass(); diff --git a/jamopp.parser.bcel/src/main/java/tools/mdsd/jamopp/parser/bcel/ClassFileModelLoader.java b/jamopp.parser.bcel/src/main/java/tools/mdsd/jamopp/parser/bcel/ClassFileModelLoader.java index bd450dcd..0576fc43 100644 --- a/jamopp.parser.bcel/src/main/java/tools/mdsd/jamopp/parser/bcel/ClassFileModelLoader.java +++ b/jamopp.parser.bcel/src/main/java/tools/mdsd/jamopp/parser/bcel/ClassFileModelLoader.java @@ -57,6 +57,7 @@ import tools.mdsd.jamopp.model.java.modifiers.ModifiersFactory; import tools.mdsd.jamopp.model.java.parameters.Parameter; import tools.mdsd.jamopp.model.java.parameters.ParametersFactory; +import tools.mdsd.jamopp.model.java.statements.StatementsFactory; import tools.mdsd.jamopp.model.java.types.ClassifierReference; import tools.mdsd.jamopp.model.java.types.TypeReference; import tools.mdsd.jamopp.model.java.types.TypedElement; @@ -215,6 +216,10 @@ private Member constructMethod(org.apache.bcel.classfile.Method method, emfMethod = membersFactory.createClassMethod(); } emfMethod.setName(method.getName()); + + var block = StatementsFactory.eINSTANCE.createBlock(); + block.setName(""); + emfMethod.setStatement(block); String signature = method.getReturnType().getSignature(); String plainSignature = ""; @@ -302,6 +307,7 @@ private Member constructMethod(org.apache.bcel.classfile.Method method, constructor.getTypeParameters().addAll(emfMethod.getTypeParameters()); constructor.getParameters().addAll(emfMethod.getParameters()); constructor.setName(emfClassifier.getName()); + constructor.setBlock(block); return constructor; } diff --git a/jamopp.parser.jdt.singlefile/src/main/java/tools/mdsd/jamopp/parser/jdt/singlefile/JaMoPPJDTSingleFileParser.java b/jamopp.parser.jdt.singlefile/src/main/java/tools/mdsd/jamopp/parser/jdt/singlefile/JaMoPPJDTSingleFileParser.java index c9f890d1..97d1564d 100644 --- a/jamopp.parser.jdt.singlefile/src/main/java/tools/mdsd/jamopp/parser/jdt/singlefile/JaMoPPJDTSingleFileParser.java +++ b/jamopp.parser.jdt.singlefile/src/main/java/tools/mdsd/jamopp/parser/jdt/singlefile/JaMoPPJDTSingleFileParser.java @@ -207,7 +207,7 @@ private void setUpResourceSet() { private ASTParser setUpParser() { ASTParser parser = ASTParser.newParser(AST.JLS15); - parser.setResolveBindings(true); + parser.setResolveBindings(ParserOptions.RESOLVE_BINDINGS.isTrue()); parser.setStatementsRecovery(true); Map compilerOptions = new HashMap<>(); compilerOptions.put(JavaCore.COMPILER_SOURCE, JavaCore.VERSION_15); diff --git a/jamopp.printer/src/main/java/tools/mdsd/jamopp/printer/ContainersPrinterSwitch.java b/jamopp.printer/src/main/java/tools/mdsd/jamopp/printer/ContainersPrinterSwitch.java index 9972c202..16443228 100644 --- a/jamopp.printer/src/main/java/tools/mdsd/jamopp/printer/ContainersPrinterSwitch.java +++ b/jamopp.printer/src/main/java/tools/mdsd/jamopp/printer/ContainersPrinterSwitch.java @@ -44,7 +44,9 @@ public Boolean caseJavaRoot(JavaRoot root) { if (root.getNamespaces().size() > 0) { parent.doSwitch(AnnotationsPackage.Literals.ANNOTABLE, root); String p = root.getNamespacesAsString(); - p = p.substring(0, p.length() - 1); + if (p.length() > 0) { + p = p.substring(0, p.length() - 1); + } writer.append("package " + p + ";\n\n"); } parent.doSwitch(ImportsPackage.Literals.IMPORTING_ELEMENT, root); @@ -65,7 +67,9 @@ public Boolean caseModule(tools.mdsd.jamopp.model.java.containers.Module element writer.append("open "); } String n = LogicalJavaURIGenerator.packageName(element); - n = n.substring(0, n.length() - 1); + if (n.length() > 0) { + n = n.substring(0, n.length() - 1); + } writer.append(n); writer.append(" {\n"); for (ModuleDirective dir : element.getTarget()) { diff --git a/jamopp.resolution/src/main/java/tools/mdsd/jamopp/recovery/trivial/TrivialRecovery.java b/jamopp.resolution/src/main/java/tools/mdsd/jamopp/recovery/trivial/TrivialRecovery.java index 1ac6459e..e2ffc715 100644 --- a/jamopp.resolution/src/main/java/tools/mdsd/jamopp/recovery/trivial/TrivialRecovery.java +++ b/jamopp.resolution/src/main/java/tools/mdsd/jamopp/recovery/trivial/TrivialRecovery.java @@ -24,9 +24,11 @@ import tools.mdsd.jamopp.model.java.classifiers.Annotation; import tools.mdsd.jamopp.model.java.classifiers.ClassifiersFactory; +import tools.mdsd.jamopp.model.java.classifiers.Enumeration; import tools.mdsd.jamopp.model.java.containers.CompilationUnit; import tools.mdsd.jamopp.model.java.containers.ContainersFactory; import tools.mdsd.jamopp.model.java.members.ClassMethod; +import tools.mdsd.jamopp.model.java.members.EnumConstant; import tools.mdsd.jamopp.model.java.members.Field; import tools.mdsd.jamopp.model.java.members.InterfaceMethod; import tools.mdsd.jamopp.model.java.members.MembersFactory; @@ -42,6 +44,7 @@ public class TrivialRecovery { private ResourceSet set; private Resource artificialResource; private CompilationUnit artificialCU; + private Enumeration artificialEnum; private tools.mdsd.jamopp.model.java.classifiers.Class artificialClass; private tools.mdsd.jamopp.model.java.classifiers.Class objectClass; private HashMap artClasses = new HashMap<>(); @@ -49,6 +52,7 @@ public class TrivialRecovery { private HashMap artFields = new HashMap<>(); private HashMap artClassMethods = new HashMap<>(); private HashMap artInterfaceMethods = new HashMap<>(); + private HashMap artEnumConstants = new HashMap<>(); private HashMap artPackages = new HashMap<>(); private HashMap artModules = new HashMap<>(); @@ -66,6 +70,9 @@ public void recover() { EList list = (EList) setting.getEObject() .eGet(setting.getEStructuralFeature()); var idx = list.indexOf(proxy); + if (idx == -1) { + continue; + } list.set(idx, actualElement); } else { setting.getEObject().eSet(setting.getEStructuralFeature(), actualElement); @@ -139,6 +146,7 @@ private EObject recoverActualElement(EObject obj) { return this.artPackages.get(name); } var result = ContainersFactory.eINSTANCE.createPackage(); + result.setName(""); p.getNamespaces().forEach(ns -> result.getNamespaces().add(ns)); this.artificialResource.getContents().add(result); this.artPackages.put(name, result); @@ -152,6 +160,15 @@ private EObject recoverActualElement(EObject obj) { this.artificialResource.getContents().add(result); this.artModules.put(name, result); return result; + } else if (obj instanceof EnumConstant) { + if (this.artEnumConstants.containsKey(name)) { + return this.artEnumConstants.get(name); + } + var result = MembersFactory.eINSTANCE.createEnumConstant(); + result.setName(name); + this.artificialEnum.getConstants().add(result); + this.artEnumConstants.put(name, result); + return result; } return null; } @@ -162,23 +179,35 @@ private void initArtificialResource() { URI.createURI("pathmap:/javaclass/ArtificialResource.java")); this.artificialCU = ContainersFactory.eINSTANCE.createCompilationUnit(); + this.artificialCU.setName(""); this.artificialResource.getContents().add(this.artificialCU); this.artificialClass = ClassifiersFactory.eINSTANCE.createClass(); this.artificialClass.setName("SyntheticClass"); this.artificialCU.getClassifiers().add(this.artificialClass); + this.artificialEnum = ClassifiersFactory.eINSTANCE.createEnumeration(); + this.artificialEnum.setName("SyntheticEnum"); + this.artificialCU.getClassifiers().add(this.artificialEnum); + this.objectClass = findObjectClass(); this.artClasses.put("Object", objectClass); } } private tools.mdsd.jamopp.model.java.classifiers.Class findObjectClass() { - return this.set.getResources().stream().filter(resource -> !resource.getContents().isEmpty() + var optionalResult = this.set.getResources().stream().filter(resource -> !resource.getContents().isEmpty() && resource.getContents().get(0) instanceof CompilationUnit) .map(resource -> (CompilationUnit) resource.getContents().get(0)) .filter(cu -> cu.getNamespaces().size() == 2 && cu.getNamespaces().get(0).equals("java") && cu.getNamespaces().get(1).equals("lang") && cu.getName().equals("Object")) - .map(cu -> (tools.mdsd.jamopp.model.java.classifiers.Class) cu.getClassifiers().get(0)).findFirst().get(); + .map(cu -> (tools.mdsd.jamopp.model.java.classifiers.Class) cu.getClassifiers().get(0)).findFirst(); + if (optionalResult.isPresent()) { + return optionalResult.get(); + } + tools.mdsd.jamopp.model.java.classifiers.Class ownObjectClass = ClassifiersFactory.eINSTANCE.createClass(); + ownObjectClass.setName("Object"); + this.artificialCU.getClassifiers().add(ownObjectClass); + return ownObjectClass; } } diff --git a/jamopp.tests/pom.xml b/jamopp.tests/pom.xml index 5610d278..4d4a69b1 100644 --- a/jamopp.tests/pom.xml +++ b/jamopp.tests/pom.xml @@ -7,7 +7,7 @@ jamopp.tests jar Extended JaMoPP Tests - This package contains only tests for the extended JaMoPP. + This package contains only tests for the extended JaMoPP. tools.mdsd @@ -65,6 +65,39 @@ + + org.apache.maven.plugins + maven-jar-plugin + + + + true + dependency + tools.mdsd.jamopp.test.performance.PerformanceTestStandaloneMain + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + + copy-dependencies + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/tools/mdsd/jamopp/test/performance/**/* + + + @@ -123,5 +156,29 @@ org.apache.commons commons-compress + + commons-io + commons-io + + + org.eclipse.emfcloud + emfjson-jackson + + + org.apache.commons + commons-math4-legacy + + + org.eclipse.jgit + org.eclipse.jgit + + + org.knowm.xchart + xchart + + + io.micrometer + micrometer-core + diff --git a/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/ChartUtility.java b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/ChartUtility.java new file mode 100644 index 00000000..fca77727 --- /dev/null +++ b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/ChartUtility.java @@ -0,0 +1,120 @@ +/******************************************************************************* + * Copyright (c) 2026 + * Modelling for Continuous Software Engineering (MCSE) group, + * Institute of Information Security and Dependability (KASTEL), + * Karlsruhe Institute of Technology (KIT). + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Martin Armbruster (MCSE) + * - Initial implementation + ******************************************************************************/ + +package tools.mdsd.jamopp.test; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; + +import org.knowm.xchart.VectorGraphicsEncoder; +import org.knowm.xchart.VectorGraphicsEncoder.VectorGraphicsFormat; +import org.knowm.xchart.XYChartBuilder; +import org.knowm.xchart.XYSeries.XYSeriesRenderStyle; + +import tools.mdsd.jamopp.test.performance.PerformanceData; + +public final class ChartUtility { + public final static String DEFAULT_X_AXIS_TITLE = "# Measurement"; + private final static String UNIT_MILLISECONDS = "ms"; + private final static String UNIT_SECONDS = "s"; + private final static String UNIT_MINUTES = "min"; + private final static String UNIT_HOURS = "h"; + private final static double MILLISECONDS_OF_ONE_SECOND = 1000; + private final static double SECONDS_OF_ONE_MINUTE = 60; + private final static double MINUTES_OF_ONE_HOUR = 60; + private final static double MILLISECONDS_OF_ONE_MINUTE = MILLISECONDS_OF_ONE_SECOND * SECONDS_OF_ONE_MINUTE; + private final static double MILLISECONDS_OF_ONE_HOUR = MILLISECONDS_OF_ONE_MINUTE * MINUTES_OF_ONE_HOUR; + + private ChartUtility() {} + + public static void buildAndSaveChartsForPerformanceData(String dataName, PerformanceData data, Path outputDirectory) throws IOException { + double[] parsingTimes = new double[data.getPoints().size()]; + double[] resolutionTimes = new double[parsingTimes.length]; + double[] recoveryTimes = new double[resolutionTimes.length]; + double[] overallTimes = new double[recoveryTimes.length]; + + int index = 0; + for (var dataPoint : data.getPoints()) { + parsingTimes[index] = dataPoint.getParseTime(); + resolutionTimes[index] = dataPoint.getResolutionTime(); + recoveryTimes[index] = dataPoint.getRecoverTime(); + overallTimes[index] = dataPoint.getParseTime() + dataPoint.getResolutionTime() + dataPoint.getRecoverTime(); + index++; + } + + buildAndSaveChart(parsingTimes, dataName + " - Parsing", + DEFAULT_X_AXIS_TITLE, "Parsing Time (" + adjustTimeMillisecondsUnit(parsingTimes) + ")", outputDirectory.resolve(dataName + "-parsing.pdf")); + buildAndSaveChart(resolutionTimes, dataName + " - Resolution", + DEFAULT_X_AXIS_TITLE, "Resolution Time (" + adjustTimeMillisecondsUnit(resolutionTimes) + ")", outputDirectory.resolve(dataName + "-resolution.pdf")); + buildAndSaveChart(recoveryTimes, dataName + " - Recovery", + DEFAULT_X_AXIS_TITLE, "Recovery Time (" + adjustTimeMillisecondsUnit(recoveryTimes) + ")", outputDirectory.resolve(dataName + "-recovery.pdf")); + buildAndSaveChart(overallTimes, dataName + " - Sum of Parsing, Resolution, and Recovery", + DEFAULT_X_AXIS_TITLE, "Overall Time (" + adjustTimeMillisecondsUnit(overallTimes) + ")", outputDirectory.resolve(dataName + "-overall.pdf")); + } + + public static void buildAndSaveChartWithDiff(double[] data, String title, String xAxisTitle, String yAxisTitle, Path chartFile) throws IOException { + double[] diffData = new double[data.length <= 1 ? 0 : data.length - 1]; + for (var index = 0; index < diffData.length; index++) { + diffData[index] = data[index + 1] - data[index]; + } + + buildAndSaveChart(data, title, xAxisTitle, yAxisTitle, chartFile); + buildAndSaveChart(diffData, title + " (Diff)", xAxisTitle, yAxisTitle, chartFile.resolveSibling(chartFile.getFileName().toString() + "-diff.pdf")); + } + + public static void buildAndSaveChart(double[] data, String title, String xAxisTitle, String yAxisTitle, Path chartFile) throws IOException { + buildAndSaveChart(null, data, title, xAxisTitle, yAxisTitle, chartFile); + } + + public static void buildAndSaveChart(double[] xData, double[] yData, String title, String xAxisTitle, String yAxisTitle, Path chartFile) throws IOException { + var chart = new XYChartBuilder().title(title).xAxisTitle(xAxisTitle).yAxisTitle(yAxisTitle).build(); + chart.getStyler().setDefaultSeriesRenderStyle(XYSeriesRenderStyle.Line).setLegendVisible(false); + + if (xData == null) { + chart.addSeries(yAxisTitle, yData); + } else { + chart.addSeries(yAxisTitle, xData, yData); + } + + VectorGraphicsEncoder.saveVectorGraphic(chart, chartFile.toAbsolutePath().toString(), VectorGraphicsFormat.PDF); + } + + public static String adjustTimeMillisecondsUnit(double[] data) { + var min = Arrays.stream(data).min().getAsDouble(); + var minMaxDiff = Arrays.stream(data).max().getAsDouble() - min; + double unitAdjustingFactor = 0.0; + String unitName = UNIT_MILLISECONDS; + + if (min > MILLISECONDS_OF_ONE_SECOND && minMaxDiff < MILLISECONDS_OF_ONE_MINUTE / 2) { + unitAdjustingFactor = MILLISECONDS_OF_ONE_SECOND; + unitName = UNIT_SECONDS; + } else if (min > MILLISECONDS_OF_ONE_MINUTE && minMaxDiff < MILLISECONDS_OF_ONE_HOUR / 2) { + unitAdjustingFactor = MILLISECONDS_OF_ONE_MINUTE; + unitName = UNIT_MINUTES; + } else if (minMaxDiff >= MILLISECONDS_OF_ONE_HOUR / 2) { + unitAdjustingFactor = MILLISECONDS_OF_ONE_HOUR; + unitName = UNIT_HOURS; + } else { + return unitName; + } + + for (int index = 0; index < data.length; index++) { + data[index] = data[index] / unitAdjustingFactor; + } + return unitName; + } +} diff --git a/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/OutputUtility.java b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/OutputUtility.java new file mode 100644 index 00000000..8bff5551 --- /dev/null +++ b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/OutputUtility.java @@ -0,0 +1,85 @@ +package tools.mdsd.jamopp.test; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; +import org.eclipse.emf.ecore.xmi.XMIResource; + +import tools.mdsd.jamopp.model.java.containers.CompilationUnit; +import tools.mdsd.jamopp.model.java.containers.JavaRoot; +import tools.mdsd.jamopp.model.java.containers.Package; + +public class OutputUtility { + public final static String SUPPORTED_FILE_EXTENSION_JAVA = "java"; + public final static String SUPPORTED_FILE_EXTENSION_XMI = "xmi"; + public final static String SUPPORTED_FILE_EXTENSION_JSON = "json"; + + public record TransferResult(ResourceSet targetSet, Map sourceTargetMapping) {}; + + public static TransferResult transferToOutput(ResourceSet sourceSet, String outputFolder, String fileExtension, boolean includeAllResources) throws IOException { + return transferToOutput(new ArrayList<>(sourceSet.getResources()), outputFolder, fileExtension, includeAllResources); + } + + public static TransferResult transferToOutput(List sources, String outputFolder, String fileExtension, boolean includeAllResources) throws IOException { + int emptyFileName = 0; + + ResourceSet targetSet = new ResourceSetImpl(); + HashMap srcTrgMap = new HashMap<>(); + + for (Resource javaResource : sources) { + if (javaResource.getContents().isEmpty()) { + continue; + } + if (!includeAllResources && !javaResource.getURI().isFile()) { + continue; + } + + JavaRoot root = (JavaRoot) javaResource.getContents().get(0); + String outputFileName = "ERROR"; + if (root instanceof CompilationUnit cu) { + outputFileName = cu.getNamespacesAsString().replace(".", File.separator) + File.separator; + if (cu.getClassifiers().size() > 0) { + outputFileName += cu.getClassifiers().get(0).getName(); + } else { + outputFileName += emptyFileName++; + } + } else if (root instanceof Package) { + outputFileName = root.getNamespacesAsString() + .replace(".", File.separator) + File.separator + "package-info"; + if (outputFileName.startsWith(File.separator)) { + outputFileName = outputFileName.substring(1); + } + } else if (root instanceof tools.mdsd.jamopp.model.java.containers.Module) { + outputFileName = root.getNamespacesAsString() + .replace(".", File.separator) + File.separator + "module-info"; + } else { + continue; + } + + File outputFile = new File(outputFolder + File.separator + outputFileName); + URI fileURI = URI.createFileURI(outputFile.getAbsolutePath()).appendFileExtension(fileExtension); + + Resource targetResource = targetSet.createResource(fileURI); + if (targetResource instanceof XMIResource xmiResource) { + xmiResource.setEncoding(StandardCharsets.UTF_8.toString()); + } + targetResource.getContents().addAll(javaResource.getContents()); + srcTrgMap.put(javaResource, targetResource); + } + + for (Resource targetResource : new ArrayList<>(targetSet.getResources())) { + targetResource.save(targetSet.getLoadOptions()); + } + + return new TransferResult(targetSet, srcTrgMap); + } +} diff --git a/jamopp.tests/src/test/java/tools/mdsd/jamopp/test/performance/PerformanceData.java b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/PerformanceData.java similarity index 57% rename from jamopp.tests/src/test/java/tools/mdsd/jamopp/test/performance/PerformanceData.java rename to jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/PerformanceData.java index 361b382f..a020dad8 100644 --- a/jamopp.tests/src/test/java/tools/mdsd/jamopp/test/performance/PerformanceData.java +++ b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/PerformanceData.java @@ -19,7 +19,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; - +import java.util.List; +import org.apache.commons.math4.legacy.stat.descriptive.SummaryStatistics; import com.google.gson.Gson; /** @@ -27,13 +28,28 @@ */ public class PerformanceData { private ArrayList points = new ArrayList<>(); + private ArrayList storage = new ArrayList<>(); + + public List getPoints() { + return (List) points.clone(); + } - public ArrayList getPoints() { - return points; + public void addPoint(PerformanceDataPoint newPoint) { + this.points.add(newPoint); } - public void setPoints(ArrayList points) { - this.points = points; + public void setPoints(List points) { + this.points.clear(); + this.points.addAll(points); + } + + public List getStorage() { + return (List) storage.clone(); + } + + public void setStorage(List storage) { + this.storage.clear(); + this.storage.addAll(storage); } public double getAverageParseTime() { @@ -44,6 +60,26 @@ public double getAverageResolutionTime() { return (double) points.stream().mapToLong(p -> p.getResolutionTime()).sum() / points.size(); } + public double getAverageRecoveryTime() { + return (double) points.stream().mapToLong(p -> p.getRecoverTime()).sum() / points.size(); + } + + public SummaryStatistics getStatistics() { + SummaryStatistics stats = new SummaryStatistics(); + points.forEach(p -> { + stats.addValue(p.getParseTime() + p.getResolutionTime() + p.getRecoverTime()); + }); + return stats; + } + + public SummaryStatistics getStatisticsWithoutResolution() { + SummaryStatistics stats = new SummaryStatistics(); + points.forEach(p -> { + stats.addValue(p.getParseTime() + p.getRecoverTime()); + }); + return stats; + } + public static PerformanceData load(Path file) { try (BufferedReader reader = Files.newBufferedReader(file)) { Gson gson = new Gson(); diff --git a/jamopp.tests/src/test/java/tools/mdsd/jamopp/test/performance/PerformanceDataPoint.java b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/PerformanceDataPoint.java similarity index 85% rename from jamopp.tests/src/test/java/tools/mdsd/jamopp/test/performance/PerformanceDataPoint.java rename to jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/PerformanceDataPoint.java index be056cef..02c85f40 100644 --- a/jamopp.tests/src/test/java/tools/mdsd/jamopp/test/performance/PerformanceDataPoint.java +++ b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/PerformanceDataPoint.java @@ -19,6 +19,7 @@ public class PerformanceDataPoint { private long parseTime; private long resolutionTime; + private long recoverTime; public long getParseTime() { return parseTime; @@ -35,4 +36,12 @@ public long getResolutionTime() { public void setResolutionTime(long resolutionTime) { this.resolutionTime = resolutionTime; } + + public long getRecoverTime() { + return recoverTime; + } + + public void setRecoverTime(long recoverTime) { + this.recoverTime = recoverTime; + } } diff --git a/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/PerformanceTestExecutor.java b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/PerformanceTestExecutor.java new file mode 100644 index 00000000..1eb15b34 --- /dev/null +++ b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/PerformanceTestExecutor.java @@ -0,0 +1,310 @@ +/******************************************************************************* + * Copyright (c) 2021-2026, Martin Armbruster + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Martin Armbruster + * - Initial implementation + ******************************************************************************/ + +package tools.mdsd.jamopp.test.performance; + +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; + +import org.apache.logging.log4j.Logger; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.file.PathUtils; +import org.apache.logging.log4j.LogManager; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emf.ecore.util.EcoreUtil; + +import tools.mdsd.jamopp.options.ParserOptions; +import tools.mdsd.jamopp.parser.jdt.singlefile.JaMoPPJDTSingleFileParser; +import tools.mdsd.jamopp.proxy.IJavaContextDependentURIFragmentCollector; +import tools.mdsd.jamopp.recovery.trivial.TrivialRecovery; +import tools.mdsd.jamopp.test.ChartUtility; +import tools.mdsd.jamopp.test.OutputUtility; +import tools.mdsd.jamopp.test.OutputUtility.TransferResult; + +/** + * Class to perform performance tests and measurements. + */ +public class PerformanceTestExecutor { + private static final Logger LOGGER = LogManager.getLogger("jamopp." + + PerformanceTestExecutor.class.getSimpleName()); + private final Path inputFolder; + private final Path outputFolder; + private final Path javaOutput; + private final Path xmiOutput; + private final Path jsonOutput; + private final Path summaryFile; + private int numberOfRepetitions; + + public PerformanceTestExecutor(Path inputFolder, Path outputFolder) { + this.inputFolder = inputFolder; + this.outputFolder = outputFolder; + this.javaOutput = outputFolder.resolve(OutputUtility.SUPPORTED_FILE_EXTENSION_JAVA); + this.xmiOutput = outputFolder.resolve(OutputUtility.SUPPORTED_FILE_EXTENSION_XMI); + this.jsonOutput = outputFolder.resolve(OutputUtility.SUPPORTED_FILE_EXTENSION_JSON); + this.summaryFile = outputFolder.resolve("summary.md"); + this.numberOfRepetitions = 100; + } + + public void setupTestEnvironment() throws IOException { + if (Files.exists(javaOutput)) { + PathUtils.deleteDirectory(javaOutput); + PathUtils.deleteDirectory(xmiOutput); + PathUtils.deleteDirectory(jsonOutput); + } + try { + Files.createDirectories(javaOutput); + Files.createDirectories(xmiOutput); + Files.createDirectories(jsonOutput); + } catch (IOException e1) { + } + } + + public void measureTeaStoreFullResolution() { + ParserOptions.CREATE_LAYOUT_INFORMATION.setValue(Boolean.TRUE); + ParserOptions.REGISTER_LOCAL.setValue(Boolean.TRUE); + ParserOptions.PREFER_BINDING_CONVERSION.setValue(Boolean.TRUE); + ParserOptions.RESOLVE_BINDINGS.setValue(Boolean.TRUE); + ParserOptions.RESOLVE_BINDINGS_OF_INFERABLE_TYPES.setValue(Boolean.TRUE); + ParserOptions.RESOLVE_EVERYTHING.setValue(Boolean.TRUE); + ParserOptions.RESOLVE_ALL_BINDINGS.setValue(Boolean.TRUE); + measurePerformance("teastore-full-resolution", getNumberOfRepetitions(), true, false); + } + + /** + * Currently, this method is protected since it probably does not provide further valuable insights in addition to the other tests. + */ + protected void measureTeaStoreWithoutResolvingEverything() { + ParserOptions.CREATE_LAYOUT_INFORMATION.setValue(Boolean.TRUE); + ParserOptions.REGISTER_LOCAL.setValue(Boolean.TRUE); + ParserOptions.PREFER_BINDING_CONVERSION.setValue(Boolean.TRUE); + ParserOptions.RESOLVE_BINDINGS.setValue(Boolean.TRUE); + ParserOptions.RESOLVE_BINDINGS_OF_INFERABLE_TYPES.setValue(Boolean.TRUE); + ParserOptions.RESOLVE_EVERYTHING.setValue(Boolean.FALSE); + ParserOptions.RESOLVE_ALL_BINDINGS.setValue(Boolean.TRUE); + measurePerformance("teastore-without-resolving-everything", getNumberOfRepetitions(), true, false); + } + + private void prepareParserOptionsForOneLevelResolution() { + ParserOptions.CREATE_LAYOUT_INFORMATION.setValue(Boolean.TRUE); + ParserOptions.REGISTER_LOCAL.setValue(Boolean.TRUE); + ParserOptions.PREFER_BINDING_CONVERSION.setValue(Boolean.TRUE); + ParserOptions.RESOLVE_BINDINGS.setValue(Boolean.TRUE); + ParserOptions.RESOLVE_BINDINGS_OF_INFERABLE_TYPES.setValue(Boolean.TRUE); + ParserOptions.RESOLVE_EVERYTHING.setValue(Boolean.FALSE); + ParserOptions.RESOLVE_ALL_BINDINGS.setValue(Boolean.FALSE); + } + + public void measureTeaStoreWithOneLevelResolution() { + prepareParserOptionsForOneLevelResolution(); + measurePerformance("teastore-one-level-resolution", getNumberOfRepetitions(), false, true); + } + + /** + * Currently, this method is protected since it takes several hours to complete. + */ + protected void measureTeaStoreWithOneLevelResolutionAndFullResolution() { + prepareParserOptionsForOneLevelResolution(); + measurePerformance("teastore-one-level-resolution-full", 1, true, false); + } + + private void prepareParserOptionsForSecondVariant() { + ParserOptions.CREATE_LAYOUT_INFORMATION.setValue(Boolean.TRUE); + ParserOptions.REGISTER_LOCAL.setValue(Boolean.TRUE); + ParserOptions.PREFER_BINDING_CONVERSION.setValue(Boolean.TRUE); + ParserOptions.RESOLVE_BINDINGS.setValue(Boolean.FALSE); + ParserOptions.RESOLVE_BINDINGS_OF_INFERABLE_TYPES.setValue(Boolean.FALSE); + ParserOptions.RESOLVE_EVERYTHING.setValue(Boolean.FALSE); + ParserOptions.RESOLVE_ALL_BINDINGS.setValue(Boolean.FALSE); + } + + public void measureTeaStoreSecondVariant() { + prepareParserOptionsForSecondVariant(); + measurePerformance("teastore-second-variant", getNumberOfRepetitions(), false, true); + } + + /** + * Currently, this method is protected since it takes several hours to complete. + */ + protected void measureTeaStoreSecondVariantAndFullResolution() { + prepareParserOptionsForSecondVariant(); + measurePerformance("teastore-second-variant-resolution", 1, true, false); + } + + public void cleanEverything() throws IOException { + calculateAndSaveAllStatistics(); + } + + private void calculateAndSaveAllStatistics() throws IOException { + StringBuilder builder = new StringBuilder(); + + Files + .walk(outputFolder, 1) + .filter(Files::isRegularFile) + .filter(path -> path.toString().endsWith(OutputUtility.SUPPORTED_FILE_EXTENSION_JSON)) + .forEach(path -> { + String name = path.getFileName().toString(); + builder.append("# Results for " + name); + + var data = PerformanceData.load(path); + var stat = data.getStatistics(); + builder.append("\n\nAverage time (ms): " + stat.getMean() + " (with std. " + + stat.getStandardDeviation() + " ms)\n"); + + stat = data.getStatisticsWithoutResolution(); + builder.append("Average time without resolution (ms): " + stat.getMean() + " (with std. " + + stat.getStandardDeviation() + " ms)\n"); + builder.append("Average parsing time (ms): " + data.getAverageParseTime()); + builder.append("\nAverage resolution time (ms): " + data.getAverageResolutionTime()); + builder.append("\nAverage recovery time (ms): " + data.getAverageRecoveryTime()); + + for (var storage : data.getStorage()) { + builder.append("\n\nStorage (" + + storage.getId() + + "): " + + storage.getCodeFiles() + + " code files of overall " + + storage.getOverallFiles() + + " files taking " + + storage.getTakenStorageByCodeFiles() + + " Bytes for code files of overall " + + storage.getTakenStorage() + + " Bytes."); + } + builder.append("\n\n"); + + try { + ChartUtility.buildAndSaveChartsForPerformanceData(name, data, outputFolder); + } catch (IOException e) { + LOGGER.info("Could not create and store charts for: " + name); + } + }); + + Files.writeString(summaryFile, builder.toString()); + } + + protected void setNumberOfRepetitions(int noRepetitions) { + this.numberOfRepetitions = noRepetitions; + } + + protected int getNumberOfRepetitions() { + return this.numberOfRepetitions; + } + + private void measurePerformance(String name, int max, boolean fullResolution, boolean recover) { + LOGGER.debug("Executing performance measurements for " + name); + JaMoPPJDTSingleFileParser parser = new JaMoPPJDTSingleFileParser(); + parser.setExclusionPatterns(".*?src/test/.*?"); + + Path outputMeasurement = outputFolder.resolve(name + ".json"); + PerformanceData result; + if (Files.exists(outputMeasurement)) { + result = PerformanceData.load(outputMeasurement); + } else { + result = new PerformanceData(); + } + int actualMax = Math.min(max, max - result.getPoints().size()); + for (int i = 0; i < actualMax; i++) { + System.out.println("Measurement " + i + " for " + name); + PerformanceDataPoint point = new PerformanceDataPoint(); + long millis = System.currentTimeMillis(); + ResourceSet set = parser.parseDirectory(inputFolder); + millis = System.currentTimeMillis() - millis; + point.setParseTime(millis); + if (fullResolution) { + millis = System.currentTimeMillis(); + EcoreUtil.resolveAll(set); + millis = System.currentTimeMillis() - millis; + } else { + var ress = new HashSet<>(set.getResources()); + millis = System.currentTimeMillis(); + for (Resource r : ress) { + EcoreUtil.resolveAll(r); + } + millis = System.currentTimeMillis() - millis; + } + point.setResolutionTime(millis); + + if (recover) { + millis = System.currentTimeMillis(); + new TrivialRecovery(set).recover(); + millis = System.currentTimeMillis() - millis; + point.setRecoverTime(millis); + } + + result.addPoint(point); + PerformanceData.save(result, outputMeasurement); + + if (i == 0 && (fullResolution || recover)) { + try { + result.setStorage(measureStorage(set)); + } catch (IOException e) { + fail(e); + } + PerformanceData.save(result, outputMeasurement); + } + + for (Resource res : set.getResources()) { + res.unload(); + } + IJavaContextDependentURIFragmentCollector.GLOBAL_INSTANCE + .getContextDependentURIFragmentMap().clear(); + } + LOGGER.debug("Finished measuring " + name); + } + + private List measureStorage(ResourceSet resourceSet) throws IOException { + StoragePerformance javaStorage = new StoragePerformance(); + javaStorage.setId(OutputUtility.SUPPORTED_FILE_EXTENSION_JAVA); + var result = OutputUtility.transferToOutput(resourceSet, javaOutput.toString(), OutputUtility.SUPPORTED_FILE_EXTENSION_JAVA, true); + fillStorageInformationFromTransfer(javaStorage, javaOutput, result); + + result.sourceTargetMapping().forEach((key, value) -> { + key.getContents().addAll(value.getContents()); + }); + + StoragePerformance xmiStorage = new StoragePerformance(); + xmiStorage.setId(OutputUtility.SUPPORTED_FILE_EXTENSION_XMI); + result = OutputUtility.transferToOutput(resourceSet, xmiOutput.toString(), OutputUtility.SUPPORTED_FILE_EXTENSION_XMI, true); + fillStorageInformationFromTransfer(xmiStorage, xmiOutput, result); + + result.sourceTargetMapping().forEach((key, value) -> { + key.getContents().addAll(value.getContents()); + }); + + StoragePerformance jsonStorage = new StoragePerformance(); + jsonStorage.setId(OutputUtility.SUPPORTED_FILE_EXTENSION_JSON); + fillStorageInformationFromTransfer(jsonStorage, jsonOutput, OutputUtility.transferToOutput(resourceSet, jsonOutput.toString(), OutputUtility.SUPPORTED_FILE_EXTENSION_JSON, true)); + + return List.of(javaStorage, xmiStorage, jsonStorage); + } + + private void fillStorageInformationFromTransfer(StoragePerformance storage, Path outputDir, TransferResult outputResult) throws IOException { + storage.setTakenStorage(PathUtils.sizeOfDirectory(outputDir)); + long codeFiles = 0; + long codeSize = 0; + for (var entry : outputResult.sourceTargetMapping().entrySet()) { + if (entry.getKey().getURI().isFile()) { + codeFiles++; + codeSize += FileUtils.sizeOf(new File(entry.getValue().getURI().toFileString())); + } + } + storage.setCodeFiles(codeFiles); + storage.setTakenStorageByCodeFiles(codeSize); + storage.setOverallFiles(outputResult.sourceTargetMapping().entrySet().size()); + } +} diff --git a/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/PerformanceTestStandaloneMain.java b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/PerformanceTestStandaloneMain.java new file mode 100644 index 00000000..62b43f70 --- /dev/null +++ b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/PerformanceTestStandaloneMain.java @@ -0,0 +1,74 @@ +package tools.mdsd.jamopp.test.performance; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.xmi.impl.XMIResourceFactoryImpl; +import org.eclipse.emfcloud.jackson.resource.JsonResourceFactory; +import org.eclipse.jgit.api.errors.GitAPIException; + +import com.google.gson.JsonSyntaxException; + +import tools.mdsd.jamopp.model.java.JavaClasspath; +import tools.mdsd.jamopp.resource.JavaResource2Factory; +import tools.mdsd.jamopp.test.performance.monitor.MemoryMonitor; +import tools.mdsd.jamopp.test.performance.stepwise.StepwisePerformanceExecutor; + +public final class PerformanceTestStandaloneMain { + public final static Path DEFAULT_INPUT_PATH = Paths.get("target", "src-bulk", "TeaStore"); + public final static Path DEFAULT_OUTPUT_PATH = Paths.get("target", "tests", "output_performance"); + private final static Path MAIN_ROOT_PATH = Paths.get("jamopp.tests").toAbsolutePath(); + private final static String OPTION_NAME_STEPWISE_TEST = "stepwise"; + private final static String OPTION_NAME_FULL_TEST = "full"; + + private PerformanceTestStandaloneMain() {} + + public static void setupRegistries() { + Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().put("java", new JavaResource2Factory()); + Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().put(Resource.Factory.Registry.DEFAULT_EXTENSION, new XMIResourceFactoryImpl()); + JavaClasspath.get().clear(); + JavaClasspath.get().registerStdLib(); + Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().put("json", new JsonResourceFactory()); + } + + public static void main(String[] args) { + var actualOutputDirectory = MAIN_ROOT_PATH.resolve(DEFAULT_OUTPUT_PATH); + var memoryMonitor = new MemoryMonitor(actualOutputDirectory.resolve("mem.txt")); + memoryMonitor.initialize(); + + setupRegistries(); + + if (args.length == 1 && args[0].equals(OPTION_NAME_STEPWISE_TEST)) { + StepwisePerformanceExecutor executor = new StepwisePerformanceExecutor(); + try { + executor.measurePerformance("teastore-stepwise", MAIN_ROOT_PATH.resolve(DEFAULT_INPUT_PATH), + actualOutputDirectory); + memoryMonitor.stop(); + memoryMonitor.readDataAndCreateChart(); + } catch (IOException | GitAPIException | NumberFormatException | JsonSyntaxException e) { + e.printStackTrace(); + } + return; + } + + PerformanceTestExecutor executor = new PerformanceTestExecutor(MAIN_ROOT_PATH.resolve(DEFAULT_INPUT_PATH), + actualOutputDirectory); + if (!(args.length == 1 && args[0].equals(OPTION_NAME_FULL_TEST))) { + executor.setNumberOfRepetitions(1); + } + + try { + executor.setupTestEnvironment(); + executor.measureTeaStoreSecondVariant(); + executor.measureTeaStoreWithOneLevelResolution(); + executor.measureTeaStoreFullResolution(); + executor.cleanEverything(); + memoryMonitor.stop(); + memoryMonitor.readDataAndCreateChart(); + } catch (IOException | NumberFormatException | JsonSyntaxException e) { + e.printStackTrace(); + } + } +} diff --git a/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/StoragePerformance.java b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/StoragePerformance.java new file mode 100644 index 00000000..8098e63c --- /dev/null +++ b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/StoragePerformance.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) 2023, Martin Armbruster + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Martin Armbruster + * - Initial implementation + ******************************************************************************/ + +package tools.mdsd.jamopp.test.performance; + +public class StoragePerformance { + private String id; + private long takenStorageByCodeFiles; + private long takenStorage; + private long overallFiles; + private long codeFiles; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public long getTakenStorageByCodeFiles() { + return takenStorageByCodeFiles; + } + + public void setTakenStorageByCodeFiles(long takenStorageByCodeFiles) { + this.takenStorageByCodeFiles = takenStorageByCodeFiles; + } + + public long getTakenStorage() { + return takenStorage; + } + + public void setTakenStorage(long takenStorage) { + this.takenStorage = takenStorage; + } + + public long getOverallFiles() { + return overallFiles; + } + + public void setOverallFiles(long overallFiles) { + this.overallFiles = overallFiles; + } + + public long getCodeFiles() { + return codeFiles; + } + + public void setCodeFiles(long codeFiles) { + this.codeFiles = codeFiles; + } +} diff --git a/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/monitor/JamoppPerformanceStepMeterRegistry.java b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/monitor/JamoppPerformanceStepMeterRegistry.java new file mode 100644 index 00000000..0c01308d --- /dev/null +++ b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/monitor/JamoppPerformanceStepMeterRegistry.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * Copyright (c) 2026 + * Modelling for Continuous Software Engineering (MCSE) group, + * Institute of Information Security and Dependability (KASTEL), + * Karlsruhe Institute of Technology (KIT). + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Martin Armbruster (MCSE) + * - Initial implementation + ******************************************************************************/ + +package tools.mdsd.jamopp.test.performance.monitor; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.step.StepMeterRegistry; +import io.micrometer.core.instrument.step.StepRegistryConfig; +import io.micrometer.core.instrument.util.NamedThreadFactory; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.concurrent.TimeUnit; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class JamoppPerformanceStepMeterRegistry extends StepMeterRegistry { + private static final Logger LOGGER = LogManager.getLogger(JamoppPerformanceStepMeterRegistry.class); + private Path outputFile; + private StepRegistryConfig config; + + public JamoppPerformanceStepMeterRegistry(StepRegistryConfig config, Clock clock, Path output) { + super(config, clock); + this.outputFile = output.toAbsolutePath(); + this.config = config; + this.start(new NamedThreadFactory("jamopp-monitor")); + } + + @Override + protected void publish() { + try (BufferedWriter writer = Files.newBufferedWriter(outputFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.APPEND)) { + writer.append(MonitorConstants.PUBLISHED_TIME_LINE_PREFIX + System.currentTimeMillis()); + writer.append("\n"); + + for (var meter : getMeters()) { + if (meter.getId().getName().equals(MonitorConstants.JVM_METER_USED_MEMORY)) { + writeMeter(writer, meter); + } + } + } catch (IOException e) { + LOGGER.error("Could not write metrics because: {}", e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Writes a meter and its data to the output in a JSON format. The JSON format is directly created instead of using a library + * to avoid overhead. + * + * @param writer the output to write to. + * @param meter the meter to write. + * @throws IOException if an IO error occurs. + */ + private void writeMeter(BufferedWriter writer, Meter meter) throws IOException { + writer.append("{\"name\":\""); + writer.append(meter.getId().getName()); + + writer.append("\",\"baseUnit:\":\""); + writer.append(meter.getId().getBaseUnit()); + + writer.append("\",\"tags\":["); + boolean firstItem = true; + for (var tag : meter.getId().getTags()) { + if (!firstItem) { + writer.append(","); + } + writer.append("{\"key\":\""); + writer.append(tag.getKey()); + writer.append("\",\"value\":\""); + writer.append(tag.getValue()); + writer.append("\"}"); + firstItem = false; + } + + writer.append("],\"values\":["); + firstItem = true; + for (var measurement : meter.measure()) { + if (!firstItem) { + writer.append(","); + } + writer.append("{\"value\":" + measurement.getValue()); + writer.append(",\"statistic\":\""); + writer.append(measurement.getStatistic().name()); + writer.append("\"}"); + firstItem = false; + } + + writer.append("]}\n"); + } + + @Override + protected TimeUnit getBaseTimeUnit() { + return TimeUnit.MILLISECONDS; + } +} diff --git a/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/monitor/JamoppPerformanceStepRegistryConfig.java b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/monitor/JamoppPerformanceStepRegistryConfig.java new file mode 100644 index 00000000..df24601d --- /dev/null +++ b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/monitor/JamoppPerformanceStepRegistryConfig.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2026 + * Modelling for Continuous Software Engineering (MCSE) group, + * Institute of Information Security and Dependability (KASTEL), + * Karlsruhe Institute of Technology (KIT). + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Martin Armbruster (MCSE) + * - Initial implementation + ******************************************************************************/ + +package tools.mdsd.jamopp.test.performance.monitor; + +import java.time.Duration; + +import io.micrometer.core.instrument.step.StepRegistryConfig; + +/** + * Configuration for the JaMoPP step meter registry. + */ +public class JamoppPerformanceStepRegistryConfig implements StepRegistryConfig { + @Override + public String get(String arg0) { + return null; + } + + @Override + public Duration step() { + return Duration.ofSeconds(15); + } + + @Override + public String prefix() { + return "jamopp-performance-step-config"; + } +} \ No newline at end of file diff --git a/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/monitor/MemoryMonitor.java b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/monitor/MemoryMonitor.java new file mode 100644 index 00000000..0912eb03 --- /dev/null +++ b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/monitor/MemoryMonitor.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * Copyright (c) 2026 + * Modelling for Continuous Software Engineering (MCSE) group, + * Institute of Information Security and Dependability (KASTEL), + * Karlsruhe Institute of Technology (KIT). + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Martin Armbruster (MCSE) + * - Initial implementation + ******************************************************************************/ + +package tools.mdsd.jamopp.test.performance.monitor; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.google.gson.Gson; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Statistic; +import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; +import tools.mdsd.jamopp.test.ChartUtility; + +public class MemoryMonitor { + private boolean initialized = false; + private Path outputFile; + + public MemoryMonitor(Path outputFile) { + this.outputFile = outputFile; + } + + public void initialize() { + if (this.initialized) { + return; + } + + Metrics.globalRegistry.add( + new JamoppPerformanceStepMeterRegistry( + new JamoppPerformanceStepRegistryConfig(), + Clock.SYSTEM, + this.outputFile + ) + ); + new JvmMemoryMetrics().bindTo(Metrics.globalRegistry); + this.initialized = true; + } + + public void stop() { + Metrics.globalRegistry.close(); + } + + public void readDataAndCreateChart() throws IOException { + var data = readAndParseMonitorData(); + createMemoryChart(data); + } + + private List readAndParseMonitorData() throws IOException { + List frames = new ArrayList<>(); + MeasurementFrame currentFrame = new MeasurementFrame(); + Gson gson = new Gson(); + + try (var reader = Files.newBufferedReader(outputFile)) { + String readLine; + + while ((readLine = reader.readLine()) != null) { + if (readLine.startsWith(MonitorConstants.PUBLISHED_TIME_LINE_PREFIX)) { + currentFrame = new MeasurementFrame(); + frames.add(currentFrame); + long time = Long.parseLong(readLine.substring(MonitorConstants.PUBLISHED_TIME_LINE_PREFIX.length())); + currentFrame.measurementTime = time; + } else { + var measurement = gson.fromJson(readLine, Measurement.class); + currentFrame.measurements.add(measurement); + } + } + } + + return frames; + } + + private void createMemoryChart(List data) throws IOException { + double[] xValues = new double[data.size()]; + double[] yUsedMemory = new double[xValues.length]; + long lastTime = 0; + + for (var index = 0; index < xValues.length; index++) { + var frame = data.get(index); + if (index != 0) { + xValues[index] = (frame.measurementTime - lastTime) + xValues[index - 1]; + } else { + xValues[0] = 0; + } + + lastTime = frame.measurementTime; + + yUsedMemory[index] = frame + .measurements + .stream() + .filter(measurement -> { + for (var tag : measurement.tags) { + if (tag.key.equals(MonitorConstants.JVM_METER_TAG_KEY_AREA) + && tag.value.equals(MonitorConstants.JVM_METER_TAG_VALUE_HEAP)) { + return true; + } + } + return false; + }) + .flatMap(measurement -> Arrays.stream(measurement.values)) + .mapToDouble(measurement -> measurement.value) + .sum(); + } + + ChartUtility.buildAndSaveChart(xValues, yUsedMemory, + "Used Memory", "Time (ms)", "Used Memory (bytes)", outputFile.resolveSibling("memory.pdf")); + } + + private static class MeasurementFrame { + private long measurementTime; + private List measurements = new ArrayList<>(); + } + + private static class Measurement { + private String name; + private String baseUnit; + private Tag[] tags; + private MeasurementValue[] values; + } + + private static class Tag { + private String key; + private String value; + } + + private static class MeasurementValue { + private double value; + private Statistic statistic; + } +} diff --git a/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/monitor/MonitorConstants.java b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/monitor/MonitorConstants.java new file mode 100644 index 00000000..96fd6678 --- /dev/null +++ b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/monitor/MonitorConstants.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2026 + * Modelling for Continuous Software Engineering (MCSE) group, + * Institute of Information Security and Dependability (KASTEL), + * Karlsruhe Institute of Technology (KIT). + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Martin Armbruster (MCSE) + * - Initial implementation + ******************************************************************************/ + +package tools.mdsd.jamopp.test.performance.monitor; + +public final class MonitorConstants { + public static final String PUBLISHED_TIME_LINE_PREFIX = "=Pub: "; + public static final String JVM_METER_USED_MEMORY = "jvm.memory.used"; + public static final String JVM_METER_TAG_KEY_AREA = "area"; + public static final String JVM_METER_TAG_VALUE_HEAP = "heap"; + + private MonitorConstants() {} +} diff --git a/jamopp.tests/src/test/java/tools/mdsd/jamopp/test/performance/package-info.java b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/package-info.java similarity index 93% rename from jamopp.tests/src/test/java/tools/mdsd/jamopp/test/performance/package-info.java rename to jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/package-info.java index e492afcf..a9a90044 100644 --- a/jamopp.tests/src/test/java/tools/mdsd/jamopp/test/performance/package-info.java +++ b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/package-info.java @@ -13,4 +13,4 @@ /** * Contains tests for performance measurements. */ -package tools.mdsd.jamopp.test.performance; \ No newline at end of file +package tools.mdsd.jamopp.test.performance; diff --git a/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/stepwise/EvaluationStepFileChange.java b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/stepwise/EvaluationStepFileChange.java new file mode 100644 index 00000000..9f566d64 --- /dev/null +++ b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/stepwise/EvaluationStepFileChange.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) 2023-2026 + * Modelling for Continuous Software Engineering (MCSE) group, + * Institute of Information Security and Dependability (KASTEL), + * Karlsruhe Institute of Technology (KIT). + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Martin Armbruster (MCSE) + * - Initial implementation + ******************************************************************************/ + +package tools.mdsd.jamopp.test.performance.stepwise; + +public class EvaluationStepFileChange { + private String path; + private long newSize; + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public long getNewSize() { + return newSize; + } + + public void setNewSize(long newSize) { + this.newSize = newSize; + } +} diff --git a/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/stepwise/EvaluationStepResult.java b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/stepwise/EvaluationStepResult.java new file mode 100644 index 00000000..93853ee7 --- /dev/null +++ b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/stepwise/EvaluationStepResult.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * Copyright (c) 2023-2026 + * Modelling for Continuous Software Engineering (MCSE) group, + * Institute of Information Security and Dependability (KASTEL), + * Karlsruhe Institute of Technology (KIT). + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Martin Armbruster (MCSE) + * - Initial implementation + ******************************************************************************/ + +package tools.mdsd.jamopp.test.performance.stepwise; + +import java.util.ArrayList; +import java.util.List; + +public class EvaluationStepResult { + private int step; + private long timeResolution; + private long timeModelSaving; + private long totalProxies; + private List changedFiles = new ArrayList<>(); + + public int getStep() { + return step; + } + + public void setStep(int step) { + this.step = step; + } + + public long getTimeResolution() { + return timeResolution; + } + + public void setTimeResolution(long totalDuration) { + this.timeResolution = totalDuration; + } + + public long getTimeModelSaving() { + return timeModelSaving; + } + + public void setTimeModelSaving(long timeModelSaving) { + this.timeModelSaving = timeModelSaving; + } + + public long getTotalProxies() { + return totalProxies; + } + + public void setTotalProxies(long totalProxies) { + this.totalProxies = totalProxies; + } + + public List getChangedFiles() { + return changedFiles; + } + + public void addChangedFiles(EvaluationStepFileChange changedFile) { + this.changedFiles.add(changedFile); + } +} diff --git a/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/stepwise/StepwiseEvaluationResult.java b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/stepwise/StepwiseEvaluationResult.java new file mode 100644 index 00000000..b06e7d17 --- /dev/null +++ b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/stepwise/StepwiseEvaluationResult.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) 2023-2026 + * Modelling for Continuous Software Engineering (MCSE) group, + * Institute of Information Security and Dependability (KASTEL), + * Karlsruhe Institute of Technology (KIT). + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Martin Armbruster (MCSE) + * - Initial implementation + ******************************************************************************/ + +package tools.mdsd.jamopp.test.performance.stepwise; + +import java.util.ArrayList; +import java.util.List; + +public class StepwiseEvaluationResult { + private String name; + private List steps = new ArrayList<>(); + private long parsingTime; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getSteps() { + return steps; + } + + public void addStep(EvaluationStepResult step) { + this.steps.add(step); + } + + public long getParsingTime() { + return parsingTime; + } + + public void setParsingTime(long parsingTime) { + this.parsingTime = parsingTime; + } +} diff --git a/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/stepwise/StepwisePerformanceExecutor.java b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/stepwise/StepwisePerformanceExecutor.java new file mode 100644 index 00000000..dce7301a --- /dev/null +++ b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/stepwise/StepwisePerformanceExecutor.java @@ -0,0 +1,256 @@ +/******************************************************************************* + * Copyright (c) 2026 + * Modelling for Continuous Software Engineering (MCSE) group, + * Institute of Information Security and Dependability (KASTEL), + * Karlsruhe Institute of Technology (KIT). + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Martin Armbruster (MCSE) + * - Initial implementation + ******************************************************************************/ + +package tools.mdsd.jamopp.test.performance.stepwise; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.logging.log4j.Logger; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.logging.log4j.LogManager; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.treewalk.AbstractTreeIterator; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.eclipse.jgit.treewalk.EmptyTreeIterator; + +import com.google.gson.Gson; + +import tools.mdsd.jamopp.options.ParserOptions; +import tools.mdsd.jamopp.parser.jdt.singlefile.JaMoPPJDTSingleFileParser; +import tools.mdsd.jamopp.proxy.IJavaContextDependentURIFragmentCollector; +import tools.mdsd.jamopp.test.ChartUtility; +import tools.mdsd.jamopp.test.OutputUtility; + +public class StepwisePerformanceExecutor { + private static final Logger LOGGER = LogManager.getLogger("jamopp." + StepwisePerformanceExecutor.class.getSimpleName()); + private static final String GIT_AUTHOR_NAME = "Extended JaMoPP - Stepwise Performance Test"; + private static final String GIT_AUTHOR_MAIL = "noreply@null.localhost"; + private static final String GIT_DIRECTORY_NAME = "models"; + private static final String RESULTS_FILE_NAME = "results.json"; + private static final String CHARTS_DEFAULT_X_AXIS_NAME = "# Iteration"; + private static final String CHARTS_PROXY_COUNT_Y_AXIS_NAME = "# Proxy Objects"; + private static final String CHARTS_PROXY_COUNT_TITLE = "Proxy Objects"; + private static final String CHARTS_PROXY_COUNT_FILE_NAME = "chart-proxies.pdf"; + private static final String CHARTS_DEFAULT_TIME_Y_AXIS_NAME = "Execution Time (%s)"; + private static final String CHARTS_RESOLUTION_TIME_TITLE = "Resolution Time"; + private static final String CHARTS_RESOLUTION_TIME_FILE_NAME = "chart-resolution-time.pdf"; + private static final String CHARTS_MODEL_SAVING_TIME_TITLE = "Model Saving Time"; + private static final String CHARTS_MODEL_SAVING_TIME_FILE_NAME = "chart-model-saving-time.pdf"; + private static final String CHARTS_MODEL_COUNT_TITLE = "Models"; + private static final String CHARTS_MODEL_COUNT_Y_AXIS_NAME = "# Models"; + private static final String CHARTS_MODEL_COUNT_FILE_NAME = "chart-model-count.pdf"; + private static final String CHARTS_MODEL_SIZE_TITLE = "Model Size"; + private static final String CHARTS_MODEL_SIZE_Y_AXIS_NAME = "Model Size"; + private static final String CHARTS_MODEL_SIZE_FILE_NAME = "chart-model-size.pdf"; + private Git git; + private RevCommit lastCommit; + + private void prepareParserOptionsForSecondVariant() { + ParserOptions.CREATE_LAYOUT_INFORMATION.setValue(Boolean.TRUE); + ParserOptions.REGISTER_LOCAL.setValue(Boolean.TRUE); + ParserOptions.PREFER_BINDING_CONVERSION.setValue(Boolean.FALSE); + ParserOptions.RESOLVE_BINDINGS.setValue(Boolean.FALSE); + ParserOptions.RESOLVE_BINDINGS_OF_INFERABLE_TYPES.setValue(Boolean.FALSE); + ParserOptions.RESOLVE_EVERYTHING.setValue(Boolean.FALSE); + ParserOptions.RESOLVE_ALL_BINDINGS.setValue(Boolean.FALSE); + } + + public void measurePerformance(String name, Path srcDirectory, Path outputDirectory) throws IOException, GitAPIException { + if (Files.notExists(srcDirectory) || !Files.isDirectory(srcDirectory)) { + throw new IllegalStateException("Given input directory '" + srcDirectory.toString() + "' does not exist or is not a directory."); + } + + if (Files.exists(outputDirectory)) { + if (!Files.isDirectory(outputDirectory)) { + throw new IllegalStateException("The given output directory '" + outputDirectory.toString() + "' exists and is not a directory."); + } + } else { + Files.createDirectories(outputDirectory); + } + + prepareParserOptionsForSecondVariant(); + LOGGER.info("Executing performance measurements for: " + name); + StepwiseEvaluationResult result = new StepwiseEvaluationResult(); + result.setName(name); + + JaMoPPJDTSingleFileParser parser = new JaMoPPJDTSingleFileParser(); + parser.setExclusionPatterns(".*?src/test/.*?"); + + LOGGER.info("Parsing the directory " + srcDirectory.toString()); + long millis = System.currentTimeMillis(); + ResourceSet set = parser.parseDirectory(srcDirectory); + result.setParsingTime(System.currentTimeMillis() - millis); + + EvaluationStepResult stepResult = new EvaluationStepResult(); + stepResult.setStep(0); + stepResult.setTimeResolution(0); + stepResult.setTotalProxies(IJavaContextDependentURIFragmentCollector.GLOBAL_INSTANCE.getContextDependentURIFragmentMap().size()); + result.addStep(stepResult); + + var resultFile = outputDirectory.resolve(RESULTS_FILE_NAME); + var gitDir = outputDirectory.resolve(GIT_DIRECTORY_NAME); + this.git = Git.init().setDirectory(gitDir.toFile()).call(); + + var outputResult = this.storeModelsAndCalculateSizeChanges(set, gitDir); + stepResult.setTimeModelSaving(outputResult.getRight()); + outputResult.getLeft().forEach(stepResult::addChangedFiles); + this.saveResults(result, resultFile); + + LOGGER.info("Resolving proxy objects."); + List oldResources = List.of(); + int iteration = 1; + do { + oldResources = new ArrayList<>(set.getResources()); + LOGGER.info("Having " + oldResources.size() + " resources to check for proxy objects."); + + for (Resource resource : oldResources) { + if (EcoreUtil.ProxyCrossReferencer.find(resource).size() == 0) { + continue; + } + + LOGGER.info("Step " + iteration + "."); + + millis = System.currentTimeMillis(); + EcoreUtil.resolveAll(resource); + millis = System.currentTimeMillis() - millis; + + stepResult = new EvaluationStepResult(); + stepResult.setStep(iteration); + stepResult.setTimeResolution(millis); + stepResult.setTotalProxies(IJavaContextDependentURIFragmentCollector.GLOBAL_INSTANCE.getContextDependentURIFragmentMap().size()); + + outputResult = this.storeModelsAndCalculateSizeChanges(set, gitDir); + stepResult.setTimeModelSaving(outputResult.getRight()); + outputResult.getLeft().forEach(stepResult::addChangedFiles); + + result.addStep(stepResult); + this.saveResults(result, resultFile); + iteration++; + } + } while (oldResources.size() != set.getResources().size()); + + this.lastCommit = null; + + this.git.getRepository().close(); + this.git.close(); + this.git = null; + for (Resource res : set.getResources()) { + res.unload(); + } + IJavaContextDependentURIFragmentCollector.GLOBAL_INSTANCE.getContextDependentURIFragmentMap().clear(); + + buildAndSaveCharts(result, outputDirectory); + LOGGER.info("Finished measuring: " + name); + } + + private Pair, Long> storeModelsAndCalculateSizeChanges(ResourceSet resourceSet, Path output) throws GitAPIException, IOException { + // Store all model, and reset the source models. + long storingTime = System.currentTimeMillis(); + var result = OutputUtility.transferToOutput(resourceSet, output.toString(), OutputUtility.SUPPORTED_FILE_EXTENSION_XMI, true); + storingTime = System.currentTimeMillis() - storingTime; + result.sourceTargetMapping().forEach((key, value) -> { + key.getContents().addAll(value.getContents()); + }); + + // Add all generated files to the Git repository to save them. + this.git.add().addFilepattern(".").call(); + var recentCommit = this.git.commit().setAuthor(GIT_AUTHOR_NAME, GIT_AUTHOR_MAIL).setMessage("").call(); + + // Calculate the size of every changed file. + List fileChanges = new ArrayList<>(); + AtomicLong totalChangeSize = new AtomicLong(); + + // Get all changed files. + var gitObjReader = this.git.getRepository().newObjectReader(); + AbstractTreeIterator oldTree = null; + if (this.lastCommit != null) { + oldTree = new CanonicalTreeParser(null, gitObjReader, this.lastCommit.getTree().getId()); + } else { + oldTree = new EmptyTreeIterator(); + } + CanonicalTreeParser newTree = new CanonicalTreeParser(null, gitObjReader, recentCommit.getTree().getId()); + var diffEntries = this.git.diff().setShowNameOnly(true).setOldTree(oldTree).setNewTree(newTree).call(); + + // Calculate the size for every changed file. + diffEntries.forEach(entry -> { + var affectedFile = output.resolve(entry.getNewPath()); + var size = FileUtils.sizeOf(affectedFile.toFile()); + totalChangeSize.addAndGet(size); + + var stepFileChange = new EvaluationStepFileChange(); + stepFileChange.setNewSize(size); + stepFileChange.setPath(entry.getNewPath()); + fileChanges.add(stepFileChange); + }); + + this.lastCommit = recentCommit; + return Pair.of(fileChanges, storingTime); + } + + private void saveResults(StepwiseEvaluationResult results, Path file) throws IOException { + Gson gson = new Gson(); + Files.writeString(file, gson.toJson(results)); + } + + private void buildAndSaveCharts(StepwiseEvaluationResult result, Path outputDir) throws IOException { + // Planned graphs: iteration number (x axis) vs. following numbers on y axis + // memory consumption + var currentData = result.getSteps().stream().mapToDouble(EvaluationStepResult::getTotalProxies).toArray(); + ChartUtility.buildAndSaveChartWithDiff(currentData, CHARTS_PROXY_COUNT_TITLE, CHARTS_DEFAULT_X_AXIS_NAME, + CHARTS_PROXY_COUNT_Y_AXIS_NAME, outputDir.resolve(CHARTS_PROXY_COUNT_FILE_NAME)); + + currentData = result.getSteps().stream().mapToDouble(EvaluationStepResult::getTimeResolution).toArray(); + ChartUtility.buildAndSaveChart(currentData, CHARTS_RESOLUTION_TIME_TITLE, CHARTS_DEFAULT_X_AXIS_NAME, + String.format(CHARTS_DEFAULT_TIME_Y_AXIS_NAME, ChartUtility.adjustTimeMillisecondsUnit(currentData)), + outputDir.resolve(CHARTS_RESOLUTION_TIME_FILE_NAME)); + + currentData = result.getSteps().stream().mapToDouble(EvaluationStepResult::getTimeModelSaving).toArray(); + ChartUtility.buildAndSaveChart(currentData, CHARTS_MODEL_SAVING_TIME_TITLE, CHARTS_DEFAULT_X_AXIS_NAME, + String.format(CHARTS_DEFAULT_TIME_Y_AXIS_NAME, ChartUtility.adjustTimeMillisecondsUnit(currentData)), + outputDir.resolve(CHARTS_MODEL_SAVING_TIME_FILE_NAME)); + + double[] sizes = new double[currentData.length]; + Map filesToSizes = new HashMap<>(); + + for (var index = 0; index < currentData.length; index++) { + var step = result.getSteps().get(index); + step.getChangedFiles().forEach(file -> { + filesToSizes.put(file.getPath(), file.getNewSize()); + }); + + currentData[index] = filesToSizes.size(); + sizes[index] = filesToSizes.entrySet().stream().mapToDouble(Map.Entry::getValue).sum(); + } + + ChartUtility.buildAndSaveChartWithDiff(currentData, CHARTS_MODEL_COUNT_TITLE, CHARTS_DEFAULT_X_AXIS_NAME, + CHARTS_MODEL_COUNT_Y_AXIS_NAME, outputDir.resolve(CHARTS_MODEL_COUNT_FILE_NAME)); + ChartUtility.buildAndSaveChartWithDiff(sizes, CHARTS_MODEL_SIZE_TITLE, CHARTS_DEFAULT_X_AXIS_NAME, + CHARTS_MODEL_SIZE_Y_AXIS_NAME, outputDir.resolve(CHARTS_MODEL_SIZE_FILE_NAME)); + } +} diff --git a/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/stepwise/package-info.java b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/stepwise/package-info.java new file mode 100644 index 00000000..f83a1aab --- /dev/null +++ b/jamopp.tests/src/main/java/tools/mdsd/jamopp/test/performance/stepwise/package-info.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * Copyright (c) 2023-2026 + * Modelling for Continuous Software Engineering (MCSE) group, + * Institute of Information Security and Dependability (KASTEL), + * Karlsruhe Institute of Technology (KIT). + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Martin Armbruster (MCSE) + * - Initial implementation + ******************************************************************************/ + +/** + * A package which enables fine-grained step-wise performance tests. + */ +package tools.mdsd.jamopp.test.performance.stepwise; diff --git a/jamopp.tests/src/test/java/tools/mdsd/jamopp/test/performance/PerformanceTest.java b/jamopp.tests/src/test/java/tools/mdsd/jamopp/test/performance/PerformanceTest.java index 62806485..2213d3dd 100644 --- a/jamopp.tests/src/test/java/tools/mdsd/jamopp/test/performance/PerformanceTest.java +++ b/jamopp.tests/src/test/java/tools/mdsd/jamopp/test/performance/PerformanceTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021, Martin Armbruster + * Copyright (c) 2021-2026, Martin Armbruster * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 @@ -13,172 +13,63 @@ package tools.mdsd.jamopp.test.performance; -import static org.junit.jupiter.api.Assertions.fail; - -import java.io.File; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.HashSet; -import java.util.Set; + import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; -import org.eclipse.emf.ecore.resource.Resource; -import org.eclipse.emf.ecore.resource.ResourceSet; -import org.eclipse.emf.ecore.util.EcoreUtil; -import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - -import tools.mdsd.jamopp.options.ParserOptions; -import tools.mdsd.jamopp.parser.jdt.singlefile.JaMoPPJDTSingleFileParser; -import tools.mdsd.jamopp.resource.JavaResource2; import tools.mdsd.jamopp.test.AbstractJaMoPPTests; -import tools.mdsd.jamopp.test.bulk.SingleFileParserBulkTests; /** * Class to perform performance tests and measurements. */ -@Disabled public class PerformanceTest extends AbstractJaMoPPTests { private static final Logger LOGGER = LogManager.getLogger("jamopp." - + SingleFileParserBulkTests.class.getSimpleName()); - private final String inputFolder = "target" + File.separator + "src-bulk" + File.separator + "TeaStore"; - private final Path parentOutput = Paths.get("output_performance"); + + PerformanceTest.class.getSimpleName()); + protected static PerformanceTestExecutor TEST_EXECUTOR; + + @BeforeAll + public static void setupEverything() { + PerformanceTestStandaloneMain.setupRegistries(); + TEST_EXECUTOR = new PerformanceTestExecutor(PerformanceTestStandaloneMain.DEFAULT_INPUT_PATH.toAbsolutePath(), + PerformanceTestStandaloneMain.DEFAULT_OUTPUT_PATH.toAbsolutePath()); + } - @Test - public void measureTeaStoreFullResolution() { - ParserOptions.CREATE_LAYOUT_INFORMATION.setValue(Boolean.TRUE); - ParserOptions.REGISTER_LOCAL.setValue(Boolean.TRUE); - ParserOptions.PREFER_BINDING_CONVERSION.setValue(Boolean.TRUE); - ParserOptions.RESOLVE_BINDINGS.setValue(Boolean.TRUE); - ParserOptions.RESOLVE_BINDINGS_OF_INFERABLE_TYPES.setValue(Boolean.TRUE); - ParserOptions.RESOLVE_EVERYTHING.setValue(Boolean.TRUE); - ParserOptions.RESOLVE_ALL_BINDINGS.setValue(Boolean.TRUE); - measurePerformance("teastore-full-resolution", 100, true); + @BeforeEach + public void setup() throws IOException { + TEST_EXECUTOR.setupTestEnvironment(); } @Test - public void measureTeaStoreWithoutResolvingEverything() { - ParserOptions.CREATE_LAYOUT_INFORMATION.setValue(Boolean.TRUE); - ParserOptions.REGISTER_LOCAL.setValue(Boolean.TRUE); - ParserOptions.PREFER_BINDING_CONVERSION.setValue(Boolean.TRUE); - ParserOptions.RESOLVE_BINDINGS.setValue(Boolean.TRUE); - ParserOptions.RESOLVE_BINDINGS_OF_INFERABLE_TYPES.setValue(Boolean.TRUE); - ParserOptions.RESOLVE_EVERYTHING.setValue(Boolean.FALSE); - ParserOptions.RESOLVE_ALL_BINDINGS.setValue(Boolean.TRUE); - measurePerformance("teastore-without-resolving-everything", 20, true); + public void measureTeaStoreFullResolution() { + TEST_EXECUTOR.measureTeaStoreFullResolution(); } @Test public void measureTeaStoreWithOneLevelResolution() { - ParserOptions.CREATE_LAYOUT_INFORMATION.setValue(Boolean.TRUE); - ParserOptions.REGISTER_LOCAL.setValue(Boolean.TRUE); - ParserOptions.PREFER_BINDING_CONVERSION.setValue(Boolean.TRUE); - ParserOptions.RESOLVE_BINDINGS.setValue(Boolean.TRUE); - ParserOptions.RESOLVE_BINDINGS_OF_INFERABLE_TYPES.setValue(Boolean.TRUE); - ParserOptions.RESOLVE_EVERYTHING.setValue(Boolean.FALSE); - ParserOptions.RESOLVE_ALL_BINDINGS.setValue(Boolean.FALSE); - measurePerformance("teastore-one-level-resolution", 20, false); + TEST_EXECUTOR.measureTeaStoreWithOneLevelResolution(); } @Test public void measureTeaStoreSecondVariant() { - ParserOptions.CREATE_LAYOUT_INFORMATION.setValue(Boolean.TRUE); - ParserOptions.REGISTER_LOCAL.setValue(Boolean.TRUE); - ParserOptions.PREFER_BINDING_CONVERSION.setValue(Boolean.TRUE); - ParserOptions.RESOLVE_BINDINGS.setValue(Boolean.FALSE); - ParserOptions.RESOLVE_BINDINGS_OF_INFERABLE_TYPES.setValue(Boolean.FALSE); - ParserOptions.RESOLVE_EVERYTHING.setValue(Boolean.FALSE); - ParserOptions.RESOLVE_ALL_BINDINGS.setValue(Boolean.FALSE); - measurePerformance("teastore-second-variant", 20, false); + TEST_EXECUTOR.measureTeaStoreSecondVariant(); } - @Test - public void printAllAverageTimes() { - try { - Files.walk(parentOutput).forEach(path -> { - var data = PerformanceData.load(path); - System.out.println(path.getFileName().toString()); - System.out.println("Average parsing time: " + data.getAverageParseTime()); - System.out.println("Average resolution time: " + data.getAverageResolutionTime()); - }); - } catch (IOException e) { - } + @AfterAll + public static void clean() throws IOException { + TEST_EXECUTOR.cleanEverything(); } @Override protected boolean isExcludedFromReprintTest(String filename) { - return false; + return true; } @Override protected String getTestInputFolder() { - return inputFolder; - } - - private void measurePerformance(String name, int max, boolean fullResolution) { - String testInput = getTestInputFolder(); - LOGGER.debug("Executing performance measurements for " + name); - Path target = Paths.get(testInput); - JaMoPPJDTSingleFileParser parser = new JaMoPPJDTSingleFileParser(); - parser.setExclusionPatterns(".*?src/test/.*?"); - try { - Files.createDirectories(parentOutput); - } catch (IOException e1) { - } - Path outputMeasurement = parentOutput.resolve(name + ".json"); - PerformanceData result; - if (Files.exists(outputMeasurement)) { - result = PerformanceData.load(outputMeasurement); - } else { - result = new PerformanceData(); - } - int actualMax = Math.min(max, max - result.getPoints().size()); - for (int i = 0; i < actualMax; i++) { - PerformanceDataPoint point = new PerformanceDataPoint(); - long millis = System.currentTimeMillis(); - ResourceSet set = parser.parseDirectory(target); - millis = System.currentTimeMillis() - millis; - point.setParseTime(millis); - if (fullResolution) { - millis = System.currentTimeMillis(); - EcoreUtil.resolveAll(set); - millis = System.currentTimeMillis() - millis; - } else { - var ress = new HashSet<>(set.getResources()); - millis = System.currentTimeMillis(); - for (Resource r : ress) { - EcoreUtil.resolveAll(r); - } - millis = System.currentTimeMillis() - millis; - } - point.setResolutionTime(millis); - Set parsedFiles = new HashSet<>(set.getResources()); - LOGGER.debug("Asserting the resolution of all proxy objects."); - for (Resource res : parsedFiles) { - if (res.getContents().size() == 0 || !(fullResolution && res.getURI().isFile())) { - continue; - } - this.assertResolveAllProxies(res); - } - LOGGER.debug("Reprinting."); - for (Resource res : parsedFiles) { - if (res.getContents().size() == 0 || !(fullResolution && res.getURI().isFile())) { - continue; - } - try { - this.testReprint((JavaResource2) res); - } catch (Exception e) { - fail(e.getMessage()); - } - } - result.getPoints().add(point); - PerformanceData.save(result, outputMeasurement); - for (Resource res : parsedFiles) { - res.unload(); - } - } - LOGGER.debug("Finished meausring " + name); + return PerformanceTestStandaloneMain.DEFAULT_INPUT_PATH.toString(); } } diff --git a/jamopp.tests/src/test/java/tools/mdsd/jamopp/test/performance/ReducedPerformanceTest.java b/jamopp.tests/src/test/java/tools/mdsd/jamopp/test/performance/ReducedPerformanceTest.java new file mode 100644 index 00000000..8fbe20e3 --- /dev/null +++ b/jamopp.tests/src/test/java/tools/mdsd/jamopp/test/performance/ReducedPerformanceTest.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2026 + * Modelling for Continuous Software Engineering (MCSE) group, + * Institute of Information Security and Dependability (KASTEL), + * Karlsruhe Institute of Technology (KIT). + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Martin Armbruster (MCSE) + * - Initial implementation + ******************************************************************************/ + +package tools.mdsd.jamopp.test.performance; + +import org.junit.jupiter.api.BeforeAll; + +/** + * This class provides a reduced extent of the performance tests to save time and resources. + * It acts more as a demonstration for the performance test execution. + */ +public class ReducedPerformanceTest extends PerformanceTest { + @BeforeAll() + public static void setupEverything() { + PerformanceTest.setupEverything(); + TEST_EXECUTOR.setNumberOfRepetitions(1); + } +} diff --git a/jamopp.tests/src/test/java/tools/mdsd/jamopp/test/xmi/JavaXMISerializationTest.java b/jamopp.tests/src/test/java/tools/mdsd/jamopp/test/xmi/JavaXMISerializationTest.java index 324687d4..ba44bb19 100644 --- a/jamopp.tests/src/test/java/tools/mdsd/jamopp/test/xmi/JavaXMISerializationTest.java +++ b/jamopp.tests/src/test/java/tools/mdsd/jamopp/test/xmi/JavaXMISerializationTest.java @@ -21,30 +21,24 @@ import static org.junit.jupiter.api.Assertions.fail; import java.io.File; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; -import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.Resource.Diagnostic; import org.eclipse.emf.ecore.resource.ResourceSet; -import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.emf.ecore.util.EcoreUtil.EqualityHelper; -import org.eclipse.emf.ecore.xmi.XMIResource; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import tools.mdsd.jamopp.model.java.containers.CompilationUnit; -import tools.mdsd.jamopp.model.java.containers.JavaRoot; -import tools.mdsd.jamopp.model.java.containers.Package; import tools.mdsd.jamopp.options.ParserOptions; import tools.mdsd.jamopp.parser.jdt.singlefile.JaMoPPJDTSingleFileParser; import tools.mdsd.jamopp.test.AbstractJaMoPPTests; +import tools.mdsd.jamopp.test.OutputUtility; public class JavaXMISerializationTest extends AbstractJaMoPPTests { @@ -88,56 +82,7 @@ public void testXMISerialization() throws Exception { } protected ResourceSet transferToXMI(ResourceSet sourceSet, boolean includeAllResources) throws Exception { - int emptyFileName = 0; - - ResourceSet targetSet = new ResourceSetImpl(); - - for (Resource javaResource : new ArrayList<>(sourceSet.getResources())) { - if (javaResource.getContents().isEmpty()) { - System.out.println("WARNING: Emtpy Resource: " + javaResource.getURI()); - continue; - } - if (!includeAllResources && !javaResource.getURI().isFile()) { - continue; - } - JavaRoot root = (JavaRoot) javaResource.getContents().get(0); - String outputFileName = "ERROR"; - if (root instanceof CompilationUnit) { - outputFileName = root.getNamespacesAsString().replace(".", File.separator) + File.separator; - CompilationUnit cu = (CompilationUnit) root; - if (cu.getClassifiers().size() > 0) { - outputFileName += cu.getClassifiers().get(0).getName(); - } else { - outputFileName += emptyFileName++; - } - - } else if (root instanceof Package) { - outputFileName = root.getNamespacesAsString() - .replace(".", File.separator) + File.separator + "package-info"; - if (outputFileName.startsWith(File.separator)) { - outputFileName = outputFileName.substring(1); - } - } else if (root instanceof tools.mdsd.jamopp.model.java.containers.Module) { - outputFileName = root.getNamespacesAsString() - .replace(".", File.separator) + File.separator + "module-info"; - } else { - fail(); - } - File outputFile = new File("." + File.separator + TEST_OUTPUT_FOLDER_NAME - + File.separator + outputFileName); - URI xmiFileURI = URI.createFileURI(outputFile.getAbsolutePath()).appendFileExtension("xmi"); - XMIResource xmiResource = (XMIResource) targetSet.createResource(xmiFileURI); - xmiResource.setEncoding(StandardCharsets.UTF_8.toString()); - xmiResource.getContents().addAll(javaResource.getContents()); - } - for (Resource xmiResource : targetSet.getResources()) { - try { - xmiResource.save(targetSet.getLoadOptions()); - } catch (Exception e) { - e.printStackTrace(); - } - } - return targetSet; + return OutputUtility.transferToOutput(sourceSet, TEST_OUTPUT_FOLDER_NAME, "xmi", includeAllResources).targetSet(); } protected void compare(ResourceSet rs) throws Exception { diff --git a/pom.xml b/pom.xml index 011e4ef9..bd5f59dc 100644 --- a/pom.xml +++ b/pom.xml @@ -148,6 +148,38 @@ commons-compress 1.23.0 + + commons-io + commons-io + 2.13.0 + + + org.eclipse.emfcloud + emfjson-jackson + 2.2.0 + + + org.apache.commons + commons-math4-legacy + 4.0-beta1 + + + org.eclipse.jgit + org.eclipse.jgit + 6.7.0.202309050840-r + + + org.knowm.xchart + xchart + 3.8.8 + + + io.micrometer + micrometer-bom + 1.16.4 + pom + import +