From 8f2f16db7c7ffe2eee9e0887d80acfd1e6546c0c Mon Sep 17 00:00:00 2001 From: Cedric Champeau Date: Wed, 20 Sep 2023 09:34:33 +0200 Subject: [PATCH] Add "functional" benchmark This commit introduces a new test project which is a JMH benchmark testing an application. It makes use of the Micronaut Gradle plugins (that is to say builds an application in a similar way as for a user application) and applies the AOT plugin. There are 2 JMH jars being created. The standard JMH jar is using the regular application classpath. Another jar is created with the optimized jar. To run the benchmark with the AOT optimized application, run: `./gradlew :test-suite-benchmarks:optimizedJmhJar` Then: `./test-suite-benchmarks/build/libs/test-suite-benchmarks-jmh-all-.jar` --- benchmarks/build.gradle | 2 +- buildSrc/build.gradle | 3 + ...naut.build.internal.convention-base.gradle | 15 +-- ...build.internal.convention-benchmark.gradle | 108 ++++++++++++++++++ ...ronaut.build.internal.substitutions.gradle | 18 +++ gradle/libs.versions.toml | 12 ++ settings.gradle | 1 + test-suite-benchmarks/build.gradle | 7 ++ .../http/bench/SimpleControllerBenchmark.java | 71 ++++++++++++ .../io/micronaut/http/bench/Application.java | 24 ++++ .../java/io/micronaut/http/bench/Person.java | 10 ++ .../http/bench/SimpleController.java | 34 ++++++ .../src/main/resources/application.properties | 1 + .../src/main/resources/logback.xml | 14 +++ 14 files changed, 305 insertions(+), 15 deletions(-) create mode 100644 buildSrc/src/main/groovy/io.micronaut.build.internal.convention-benchmark.gradle create mode 100644 buildSrc/src/main/groovy/io.micronaut.build.internal.substitutions.gradle create mode 100644 test-suite-benchmarks/build.gradle create mode 100644 test-suite-benchmarks/src/jmh/java/io/micronaut/http/bench/SimpleControllerBenchmark.java create mode 100644 test-suite-benchmarks/src/main/java/io/micronaut/http/bench/Application.java create mode 100644 test-suite-benchmarks/src/main/java/io/micronaut/http/bench/Person.java create mode 100644 test-suite-benchmarks/src/main/java/io/micronaut/http/bench/SimpleController.java create mode 100644 test-suite-benchmarks/src/main/resources/application.properties create mode 100644 test-suite-benchmarks/src/main/resources/logback.xml diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle index 709350df4fa..6b55a29ca42 100644 --- a/benchmarks/build.gradle +++ b/benchmarks/build.gradle @@ -1,6 +1,6 @@ plugins { id 'io.micronaut.build.internal.convention-base' - id "me.champeau.jmh" version "0.7.1" + id "me.champeau.jmh" } dependencies { diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index a4cd40d3ab8..1bb21958ed7 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -14,4 +14,7 @@ dependencies { implementation "org.tomlj:tomlj:1.1.0" implementation "me.champeau.gradle:japicmp-gradle-plugin:0.4.2" implementation "org.graalvm.buildtools.native:org.graalvm.buildtools.native.gradle.plugin:0.9.26" + implementation "io.micronaut.gradle:micronaut-gradle-plugin:4.1.1" + implementation "me.champeau.jmh:jmh-gradle-plugin:0.7.1" + implementation "com.github.johnrengelman:shadow:8.1.1" } diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle index 9d16ebd2472..517fc836dbc 100644 --- a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-base.gradle @@ -3,6 +3,7 @@ import io.micronaut.build.internal.ext.DefaultMicronautCoreExtension plugins { id "io.micronaut.build.internal.common" + id "io.micronaut.build.internal.substitutions" } micronautBuild { @@ -62,20 +63,6 @@ tasks.withType(Jar).configureEach { preserveFileTimestamps = false } -configurations.all { - resolutionStrategy.dependencySubstitution { - rootProject.subprojects.each { - if (!it.name.startsWith('test-')) { - if (it.name.contains('bom')) { - substitute platform(module("io.micronaut:micronaut-${it.name}")) using platform(project(":${it.name}")) because "we want to test with what we're building" - } else { - substitute module("io.micronaut:micronaut-${it.name}") using project(":${it.name}") because "we want to test with what we're building" - } - } - } - } -} - dependencies { annotationProcessor libs.bundles.asm annotationProcessor(libs.micronaut.docs.map { diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-benchmark.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-benchmark.gradle new file mode 100644 index 00000000000..d0e5c8f9878 --- /dev/null +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.convention-benchmark.gradle @@ -0,0 +1,108 @@ +import me.champeau.jmh.JmhBytecodeGeneratorTask + +plugins { + id("io.micronaut.application") + id("io.micronaut.aot") + id("com.github.johnrengelman.shadow") + id("me.champeau.jmh") + id("io.micronaut.build.internal.substitutions") +} + +repositories { + mavenCentral() +} + +application { + mainClass = "io.micronaut.http.bench.Application" +} + +micronaut { + runtime("netty") + testRuntime("junit5") + processing { + incremental(true) + annotations("io.micronaut.http.bench.*") + } + + aot { + version = libs.versions.micronaut.aot + optimizeServiceLoading = true + convertYamlToJava = false + precomputeOperations = true + cacheEnvironment = true + optimizeClassLoading = true + deduceEnvironment = true + optimizeNetty = true + } +} + +dependencies { + annotationProcessor(libs.micronaut.serde.processor) + implementation(libs.micronaut.serde.jackson) + runtimeOnly(libs.logback.classic) +} + +jmh { + includeTests = false +} + +def optimizedJmhJar = tasks.register("optimizedJmhJar", Jar) { + group = "jmh" + description = "Generates a JMH benchmark jar using the Micronaut AOT optimized jar" + archiveAppendix = 'jmh-all' + manifest { + attributes 'Main-Class': 'org.openjdk.jmh.Main' + } + from(configurations.jmh.incoming.artifacts.resolvedArtifacts.map { + it.collect { artifact -> zipTree(artifact.file )} + }) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(tasks.named("optimizedJitJarAll", Jar).map { zipTree(it.archiveFile.get().asFile)}) + from(sourceSets.jmh.output) + def bytecodeGen = tasks.named("jmhRunBytecodeGenerator", JmhBytecodeGeneratorTask) + from(tasks.named("jmhCompileGeneratedClasses", JavaCompile)) + from(bytecodeGen.map { + it.generatedResourcesDir + }) + // For build reproducibility + preserveFileTimestamps = false + reproducibleFileOrder = true +} + +graalvmNative { + toolchainDetection = false + metadataRepository { + enabled = true + } + binaries { + all { + resources.autodetect() + } + } + binaries { + jmhAot { + classpath.from(optimizedJmhJar) + mainClass = 'org.openjdk.jmh.Main' + // We're using a fat jar, so we copy the configuration from the main binary in order + // to include configuration for the embedded jars + configurationFileDirectories.from(graalvmNative.binaries.main.configurationFileDirectories) + } + } +} + +tasks.withType(me.champeau.jmh.JMHTask).configureEach { + doFirst { + throw new UnsupportedOperationException( + """ + Running benchmarks directly from Gradle is not supported. + You can run java -jar ${it.jarArchive.get().asFile} + """.stripIndent() + ) + } +} + +tasks.named("nativeJmhAotCompile") { + doFirst { + println graalvmNative.binaries.jmhAot.configurationFileDirectories.files + } +} diff --git a/buildSrc/src/main/groovy/io.micronaut.build.internal.substitutions.gradle b/buildSrc/src/main/groovy/io.micronaut.build.internal.substitutions.gradle new file mode 100644 index 00000000000..6dad6ffd068 --- /dev/null +++ b/buildSrc/src/main/groovy/io.micronaut.build.internal.substitutions.gradle @@ -0,0 +1,18 @@ +/** + * This plugin applies dependency substitutions so that modules + * which are built locally are preferred over their binary versions. + */ + +configurations.all { + resolutionStrategy.dependencySubstitution { + rootProject.subprojects.each { + if (!it.name.startsWith('test-')) { + if (it.name.contains('bom')) { + substitute platform(module("io.micronaut:micronaut-${it.name}")) using platform(project(":${it.name}")) because "we want to test with what we're building" + } else { + substitute module("io.micronaut:micronaut-${it.name}") using project(":${it.name}") because "we want to test with what we're building" + } + } + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8ae588deaca..60133b42103 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,9 +32,12 @@ junit-platform="1.9.3" logback = "1.4.11" logbook-netty = "2.16.0" log4j = "2.20.0" +micronaut = "4.1.1" # This version is a *platform* version used in benchmarks only +micronaut-aot = "2.0.1" micronaut-aws = "4.0.4" micronaut-groovy = "4.0.1" micronaut-session = "4.0.0" +micronaut-serde = "2.2.4" micronaut-sql = "5.0.0-M7" micronaut-test = "4.0.2" micronaut-validation = "4.0.0-M9" @@ -83,6 +86,7 @@ test-boms-micronaut-validation = { module = "io.micronaut.validation:micronaut-v test-boms-micronaut-rxjava2 = { module = "io.micronaut.rxjava2:micronaut-rxjava2-bom", version.ref = "micronaut-rxjava2" } test-boms-micronaut-rxjava3 = { module = "io.micronaut.rxjava3:micronaut-rxjava3-bom", version.ref = "micronaut-rxjava3" } test-boms-micronaut-reactor = { module = "io.micronaut.reactor:micronaut-reactor-bom", version.ref = "micronaut-reactor" } +test-boms-micronaut-serde = { module = "io.micronaut.serde:micronaut-serde", version.ref = "micronaut-serde" } boms-groovy = { module = "org.apache.groovy:groovy-bom", version.ref = "managed-groovy" } boms-kotlin = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "managed-kotlin" } @@ -214,6 +218,10 @@ logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "lo logbook-netty = { module = "org.zalando:logbook-netty", version.ref = "logbook-netty" } +# This is only present so that the "micronaut" version is updated by renovatebot +micronaut-platform = { module = "io.micronaut.platform:micronaut-platform", version.ref = "micronaut"} + +micronaut-aot-bom = { module = "io.micronaut.aot:micronaut-aot-bom", version.ref = "micronaut-aot" } micronaut-docs = { module = "io.micronaut.docs:micronaut-docs-asciidoc-config-props", version.ref = "micronaut-docs" } micronaut-runtime-groovy = { module = "io.micronaut.groovy:micronaut-runtime-groovy", version.ref = "micronaut-groovy" } micronaut-session = { module = "io.micronaut.session:micronaut-session", version.ref = "micronaut-session" } @@ -225,6 +233,10 @@ micronaut-test-spock = { module = "io.micronaut.test:micronaut-test-spock", vers micronaut-sql-jdbc = { module = "io.micronaut.sql:micronaut-jdbc", version.ref = "micronaut-sql" } micronaut-sql-jdbc-tomcat = { module = "io.micronaut.sql:micronaut-jdbc-tomcat", version.ref = "micronaut-sql" } + +micronaut-serde-processor = { module = "io.micronaut.serde:micronaut-serde-processor", version.ref = "micronaut-serde" } +micronaut-serde-jackson = { module = "io.micronaut.serde:micronaut-serde-jackson", version.ref = "micronaut-serde" } + mysql-driver = { module = "mysql:mysql-connector-java" } neo4j-bolt = { module = "org.neo4j.driver:neo4j-java-driver", version.ref = "neo4j-java-driver" } diff --git a/settings.gradle b/settings.gradle index 2c5b7f2e6b3..6026ba7d499 100644 --- a/settings.gradle +++ b/settings.gradle @@ -67,6 +67,7 @@ include "websocket" // test suites include "test-suite" +include "test-suite-benchmarks" include "test-suite-geb" include "test-suite-helper" include "test-suite-http-client-jdk-ssl" diff --git a/test-suite-benchmarks/build.gradle b/test-suite-benchmarks/build.gradle new file mode 100644 index 00000000000..1c7525405df --- /dev/null +++ b/test-suite-benchmarks/build.gradle @@ -0,0 +1,7 @@ +plugins { + id("io.micronaut.build.internal.convention-benchmark") +} + +application { + mainClass = "io.micronaut.http.bench.Application" +} diff --git a/test-suite-benchmarks/src/jmh/java/io/micronaut/http/bench/SimpleControllerBenchmark.java b/test-suite-benchmarks/src/jmh/java/io/micronaut/http/bench/SimpleControllerBenchmark.java new file mode 100644 index 00000000000..70469bdf8fd --- /dev/null +++ b/test-suite-benchmarks/src/jmh/java/io/micronaut/http/bench/SimpleControllerBenchmark.java @@ -0,0 +1,71 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * 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. + */ +package io.micronaut.http.bench; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.http.HttpStatus; +import io.micronaut.runtime.Micronaut; +import io.micronaut.runtime.server.EmbeddedServer; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.infra.Blackhole; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +public class SimpleControllerBenchmark { + @State(Scope.Benchmark) + public static class BenchmarkState { + private HttpClient client; + private URI uri; + private ApplicationContext context; + private HttpRequest listPersons; + + @Setup + public void setup() { + context = Micronaut.run(BenchmarkState.class); + uri = context.getBean(EmbeddedServer.class).getContextURI(); + client = HttpClient.newBuilder() + .build(); + listPersons = HttpRequest.newBuilder() + .GET() + .uri(uri) + .build(); + } + + @TearDown + public void shutdown() { + context.close(); + } + + } + + @Benchmark + public void getSomeJson(Blackhole blackhole, BenchmarkState app) throws IOException, InterruptedException { + var response = app.client.send(app.listPersons, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == HttpStatus.OK.getCode()) { + blackhole.consume( + response.body() + ); + } + } +} diff --git a/test-suite-benchmarks/src/main/java/io/micronaut/http/bench/Application.java b/test-suite-benchmarks/src/main/java/io/micronaut/http/bench/Application.java new file mode 100644 index 00000000000..324e5add23a --- /dev/null +++ b/test-suite-benchmarks/src/main/java/io/micronaut/http/bench/Application.java @@ -0,0 +1,24 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * 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. + */ +package io.micronaut.http.bench; + +import io.micronaut.runtime.Micronaut; + +public class Application { + public static void main(String[] args) { + Micronaut.run(Application.class); + } +} diff --git a/test-suite-benchmarks/src/main/java/io/micronaut/http/bench/Person.java b/test-suite-benchmarks/src/main/java/io/micronaut/http/bench/Person.java new file mode 100644 index 00000000000..f6114d017b8 --- /dev/null +++ b/test-suite-benchmarks/src/main/java/io/micronaut/http/bench/Person.java @@ -0,0 +1,10 @@ +package io.micronaut.http.bench; + +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +public record Person( + String name, + int age +) { +} diff --git a/test-suite-benchmarks/src/main/java/io/micronaut/http/bench/SimpleController.java b/test-suite-benchmarks/src/main/java/io/micronaut/http/bench/SimpleController.java new file mode 100644 index 00000000000..62d0178030d --- /dev/null +++ b/test-suite-benchmarks/src/main/java/io/micronaut/http/bench/SimpleController.java @@ -0,0 +1,34 @@ +/* + * Copyright 2003-2021 the original author or authors. + * + * 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. + */ +package io.micronaut.http.bench; + +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; + +import java.util.List; + +@Controller +public class SimpleController { + private final List PERSONS = List.of( + new Person("Bob", 44), + new Person("Alice", 42) + ); + + @Get + public List list() { + return PERSONS; + } +} diff --git a/test-suite-benchmarks/src/main/resources/application.properties b/test-suite-benchmarks/src/main/resources/application.properties new file mode 100644 index 00000000000..c055eb7f1cc --- /dev/null +++ b/test-suite-benchmarks/src/main/resources/application.properties @@ -0,0 +1 @@ +micronaut.server.port=-1 diff --git a/test-suite-benchmarks/src/main/resources/logback.xml b/test-suite-benchmarks/src/main/resources/logback.xml new file mode 100644 index 00000000000..44b79c40d49 --- /dev/null +++ b/test-suite-benchmarks/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + +