diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 991ca899596..86c975353cc 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -109,6 +109,8 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-core") compileOnly(libs.develocity) + + testImplementation("me.champeau.jmh:jmh-gradle-plugin:0.7.3") } tasks.compileKotlin { diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/testJvmConstraints/TestJvmConstraintsPlugin.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/testJvmConstraints/TestJvmConstraintsPlugin.kt index 02d1c0d0a9a..e505ecb9215 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/testJvmConstraints/TestJvmConstraintsPlugin.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/testJvmConstraints/TestJvmConstraintsPlugin.kt @@ -13,6 +13,10 @@ import org.gradle.testing.jacoco.plugins.JacocoTaskExtension class TestJvmConstraintsPlugin : Plugin { override fun apply(project: Project) { + if (project.extensions.findByName(TEST_JVM_CONSTRAINTS) != null) { + return + } + project.pluginManager.apply(JavaPlugin::class.java) val projectExtension = project.extensions.create(TEST_JVM_CONSTRAINTS) diff --git a/buildSrc/src/main/kotlin/dd-trace-java.jmh-conventions.gradle.kts b/buildSrc/src/main/kotlin/dd-trace-java.jmh-conventions.gradle.kts new file mode 100644 index 00000000000..b18bfc89273 --- /dev/null +++ b/buildSrc/src/main/kotlin/dd-trace-java.jmh-conventions.gradle.kts @@ -0,0 +1,54 @@ +import datadog.gradle.plugin.testJvmConstraints.TestJvmSpec +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property + +/* + * Applies JMH with defaults from `-PtestJvm` and `-Pjmh.*`. Modules can override them in their + * `jmh {}` block. + */ +// This plugin is produced by the same buildSrc build, so it cannot be resolved from this +// precompiled script's `plugins {}` block. Apply it by ID once both plugins are available at runtime. +pluginManager.apply("dd-trace-java.test-jvm-constraints") + +// JMH is versioned in the root build with `apply false`, not added to buildSrc's implementation +// classpath. Applying it by ID here reuses the consuming build's plugin classpath. +pluginManager.apply("me.champeau.jmh") + +val testJvmSpec = TestJvmSpec(project) +val jmh = extensions.getByName("jmh") + + +jmhProperty("getJvm").convention(testJvmSpec.javaTestLauncher.map { it.executablePath.asFile.absolutePath }) +providers.gradleProperty("jmh.includes").map(::commaSeparated).let { + if (it.isPresent) { + jmhListProperty("getIncludes").convention(it.map { includes -> listOf(includes.joinToString("|")) }) + } +} +providers.gradleProperty("jmh.profilers").map(::commaSeparated).let { + if (it.isPresent) { + jmhListProperty("getProfilers").convention(it) + } +} +providers.gradleProperty("jmh.forks").map(String::toInt).let { + if (it.isPresent) { + jmhProperty("getFork").convention(it) + } +} +providers.gradleProperty("jmh.threads").map(String::toInt).let { + if (it.isPresent) { + jmhProperty("getThreads").convention(it) + } +} + +// JMH types are not on buildSrc's compile classpath, so access its extension through Gradle's public +// property types. +@Suppress("UNCHECKED_CAST") +fun jmhProperty(getterName: String): Property = + jmh.javaClass.getMethod(getterName).invoke(jmh) as Property + +@Suppress("UNCHECKED_CAST") +fun jmhListProperty(getterName: String): ListProperty = + jmh.javaClass.getMethod(getterName).invoke(jmh) as ListProperty + +fun commaSeparated(value: String): List = + value.split(",").map(String::trim).filter(String::isNotEmpty) diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/jmh/JmhConventionsPluginTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/jmh/JmhConventionsPluginTest.kt new file mode 100644 index 00000000000..b34fc54d4ce --- /dev/null +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/jmh/JmhConventionsPluginTest.kt @@ -0,0 +1,126 @@ +package datadog.gradle.plugin.jmh + +import datadog.gradle.plugin.testJvmConstraints.TestJvmConstraintsExtension.Companion.TEST_JVM_CONSTRAINTS +import datadog.gradle.plugin.testJvmConstraints.TestJvmSpec +import me.champeau.jmh.JmhParameters +import org.assertj.core.api.Assertions.assertThat +import org.gradle.api.JavaVersion +import org.gradle.testfixtures.ProjectBuilder +import org.junit.jupiter.api.Test + +class JmhConventionsPluginTest { + @Test + fun `plugin applies jmh and test-jvm-constraints`() { + val project = ProjectBuilder.builder().build() + + project.pluginManager.apply("dd-trace-java.jmh-conventions") + + assertThat(project.plugins.hasPlugin("me.champeau.jmh")).isTrue() + assertThat(project.extensions.findByName(TEST_JVM_CONSTRAINTS)).isNotNull() + } + + @Test + fun `plugin provides the test jvm as an overridable default`() { + val propertyName = "org.gradle.project.${TestJvmSpec.TEST_JVM}" + val previousValue = System.setProperty(propertyName, JavaVersion.current().majorVersion) + + try { + val project = ProjectBuilder.builder().build() + + project.pluginManager.apply("dd-trace-java.jmh-conventions") + + val jmh = project.extensions.getByType(JmhParameters::class.java) + val expectedExecutable = TestJvmSpec(project).javaTestLauncher.get().executablePath.asFile.absolutePath + assertThat(jmh.jvm.get()).isEqualTo(expectedExecutable) + + jmh.jvm.set("module-jvm") + assertThat(jmh.jvm.get()).isEqualTo("module-jvm") + } finally { + if (previousValue == null) { + System.clearProperty(propertyName) + } else { + System.setProperty(propertyName, previousValue) + } + } + } + + @Test + fun `plugin provides jmh project properties as defaults`() { + withGradleProperties( + "jmh.includes" to "FooBenchmark, BarBenchmark", + "jmh.profilers" to "stack, gc", + "jmh.forks" to "1", + "jmh.threads" to "1", + ) { + val project = ProjectBuilder.builder().build() + + project.pluginManager.apply("dd-trace-java.jmh-conventions") + + val jmh = project.extensions.getByType(JmhParameters::class.java) + assertThat(jmh.includes.get()).containsExactly("FooBenchmark|BarBenchmark") + assertThat(jmh.profilers.get()).containsExactly("stack", "gc") + assertThat(jmh.fork.get()).isEqualTo(1) + assertThat(jmh.threads.get()).isEqualTo(1) + } + } + + @Test + fun `jmh properties are absent when the project properties are not set`() { + val project = ProjectBuilder.builder().build() + + project.pluginManager.apply("dd-trace-java.jmh-conventions") + + val jmh = project.extensions.getByType(JmhParameters::class.java) + assertThat(jmh.includes.get()).isEmpty() + assertThat(jmh.profilers.get()).isEmpty() + assertThat(jmh.fork.isPresent).isFalse() + assertThat(jmh.threads.isPresent).isFalse() + } + + @Test + fun `module jmh settings override project property defaults`() { + withGradleProperties( + "jmh.profilers" to "async", + "jmh.forks" to "1", + ) { + val project = ProjectBuilder.builder().build() + + project.pluginManager.apply("dd-trace-java.jmh-conventions") + + val jmh = project.extensions.getByType(JmhParameters::class.java) + jmh.profilers.set(listOf("gc")) + jmh.fork.set(4) + + assertThat(jmh.profilers.get()).containsExactly("gc") + assertThat(jmh.fork.get()).isEqualTo(4) + } + } + + @Test + fun `applying test-jvm-constraints before jmh-conventions is idempotent`() { + val project = ProjectBuilder.builder().build() + + project.pluginManager.apply("dd-trace-java.test-jvm-constraints") + project.pluginManager.apply("dd-trace-java.jmh-conventions") + + assertThat(project.extensions.findByName(TEST_JVM_CONSTRAINTS)).isNotNull() + } + + private fun withGradleProperties(vararg properties: Pair, assertions: () -> Unit) { + val systemProperties = properties.associate { (name, value) -> "org.gradle.project.$name" to value } + val previousValues = systemProperties.keys.associateWith(System::getProperty) + + try { + systemProperties.forEach(System::setProperty) + assertions() + } finally { + previousValues.forEach { (name, value) -> + if (value == null) { + System.clearProperty(name) + } else { + System.setProperty(name, value) + } + } + } + } +} diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/testJvmConstraints/TestJvmConstraintsPluginTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/testJvmConstraints/TestJvmConstraintsPluginTest.kt index 438da18f92b..7e353c61579 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/testJvmConstraints/TestJvmConstraintsPluginTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/testJvmConstraints/TestJvmConstraintsPluginTest.kt @@ -21,6 +21,19 @@ class TestJvmConstraintsPluginTest { assertThat(testTask.extensions.findByName(TEST_JVM_CONSTRAINTS)).isInstanceOf(TestJvmConstraintsExtension::class.java) } + @Test + fun `plugin is idempotent when applied more than once`() { + val project = ProjectBuilder.builder().build() + + project.pluginManager.apply("dd-trace-java.test-jvm-constraints") + TestJvmConstraintsPlugin().apply(project) + + val testTask = project.tasks.named("test", GradleTest::class.java).get() + + assertThat(project.extensions.findByName(TEST_JVM_CONSTRAINTS)).isInstanceOf(TestJvmConstraintsExtension::class.java) + assertThat(testTask.extensions.findByName(TEST_JVM_CONSTRAINTS)).isInstanceOf(TestJvmConstraintsExtension::class.java) + } + @Test fun `jacoco is disabled for additional test jvm when coverage is not checked`() { val testTask = testTaskWithJacoco() diff --git a/components/json/build.gradle.kts b/components/json/build.gradle.kts index ce67f74ff29..5d66ee6dce8 100644 --- a/components/json/build.gradle.kts +++ b/components/json/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("me.champeau.jmh") + id("dd-trace-java.jmh-conventions") } apply(from = "$rootDir/gradle/java.gradle") diff --git a/dd-java-agent/agent-bootstrap/build.gradle b/dd-java-agent/agent-bootstrap/build.gradle index 398c9ad1351..8bdf1a944f3 100644 --- a/dd-java-agent/agent-bootstrap/build.gradle +++ b/dd-java-agent/agent-bootstrap/build.gradle @@ -1,7 +1,7 @@ // The shadowJar of this project will be injected into the JVM's bootstrap classloader plugins { id 'com.gradleup.shadow' - id 'me.champeau.jmh' + id 'dd-trace-java.jmh-conventions' } apply from: "$rootDir/gradle/java.gradle" diff --git a/dd-java-agent/agent-bootstrap/src/jmh/java/datadog/trace/bootstrap/instrumentation/dbm/SharedDBCommenterBenchmark.java b/dd-java-agent/agent-bootstrap/src/jmh/java/datadog/trace/bootstrap/instrumentation/dbm/SharedDBCommenterBenchmark.java index 1919dab923c..9383605928f 100644 --- a/dd-java-agent/agent-bootstrap/src/jmh/java/datadog/trace/bootstrap/instrumentation/dbm/SharedDBCommenterBenchmark.java +++ b/dd-java-agent/agent-bootstrap/src/jmh/java/datadog/trace/bootstrap/instrumentation/dbm/SharedDBCommenterBenchmark.java @@ -32,11 +32,8 @@ * short-circuits on the first check. * *
- *   # agent-bootstrap has no -Pjmh.includes wiring yet (a generalization is in flight), so for now
- *   # either run the whole module (only a handful of benchmarks) ...
- *   ./gradlew :dd-java-agent:agent-bootstrap:jmh
- *   # ... or hack a temporary filter into agent-bootstrap/build.gradle: jmh { includes = ['SharedDBCommenter.*'] }
- *   # add -prof gc (gc.alloc.rate.norm) to corroborate the allocation delta.
+ *   ./gradlew :dd-java-agent:agent-bootstrap:jmh -Pjmh.includes=SharedDBCommenterBenchmark -Pjmh.profilers=gc
+ *   # gc.alloc.rate.norm (B/op) corroborates the allocation delta.
  * 
* *

Results (JDK 17, MacBook M-series, {@code @Threads(8)}, {@code @Fork(5)}, {@code -prof diff --git a/dd-java-agent/agent-iast/build.gradle b/dd-java-agent/agent-iast/build.gradle index 1909cf4bdc2..99374646fe1 100644 --- a/dd-java-agent/agent-iast/build.gradle +++ b/dd-java-agent/agent-iast/build.gradle @@ -3,7 +3,7 @@ import net.ltgt.gradle.errorprone.CheckSeverity plugins { id 'com.gradleup.shadow' - id 'me.champeau.jmh' + id 'dd-trace-java.jmh-conventions' id 'com.google.protobuf' version '0.10.0' id 'net.ltgt.errorprone' version '3.1.0' id 'dd-trace-java.version-file' diff --git a/dd-java-agent/agent-profiling/profiling-ddprof/build.gradle b/dd-java-agent/agent-profiling/profiling-ddprof/build.gradle index fe99122c3fc..f2f96a88c4b 100644 --- a/dd-java-agent/agent-profiling/profiling-ddprof/build.gradle +++ b/dd-java-agent/agent-profiling/profiling-ddprof/build.gradle @@ -1,6 +1,6 @@ plugins { id 'com.gradleup.shadow' - id 'me.champeau.jmh' + id 'dd-trace-java.jmh-conventions' } ext { diff --git a/dd-java-agent/agent-tooling/build.gradle b/dd-java-agent/agent-tooling/build.gradle index 3b60e25d281..8f0913d133d 100644 --- a/dd-java-agent/agent-tooling/build.gradle +++ b/dd-java-agent/agent-tooling/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'me.champeau.jmh' + id 'dd-trace-java.jmh-conventions' id 'java-test-fixtures' } apply from: "$rootDir/gradle/java.gradle" diff --git a/dd-java-agent/appsec/build.gradle b/dd-java-agent/appsec/build.gradle index e7428aa1b12..3f448a05158 100644 --- a/dd-java-agent/appsec/build.gradle +++ b/dd-java-agent/appsec/build.gradle @@ -2,7 +2,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar plugins { id 'com.gradleup.shadow' - id 'me.champeau.jmh' + id 'dd-trace-java.jmh-conventions' id 'dd-trace-java.version-file' } diff --git a/dd-java-agent/benchmark/build.gradle b/dd-java-agent/benchmark/build.gradle index 41e30eca7c3..178eefd2def 100644 --- a/dd-java-agent/benchmark/build.gradle +++ b/dd-java-agent/benchmark/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'me.champeau.jmh' + id 'dd-trace-java.jmh-conventions' } apply from: "$rootDir/gradle/java.gradle" diff --git a/dd-java-agent/instrumentation/jdbc/build.gradle b/dd-java-agent/instrumentation/jdbc/build.gradle index 3f02f8a95b9..4f109f8d294 100644 --- a/dd-java-agent/instrumentation/jdbc/build.gradle +++ b/dd-java-agent/instrumentation/jdbc/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java-test-fixtures' - id 'me.champeau.jmh' + id 'dd-trace-java.jmh-conventions' } muzzle { diff --git a/dd-trace-core/build.gradle b/dd-trace-core/build.gradle index a5ec602366e..164b6d3b283 100644 --- a/dd-trace-core/build.gradle +++ b/dd-trace-core/build.gradle @@ -1,7 +1,5 @@ -import datadog.gradle.plugin.testJvmConstraints.TestJvmSpec - plugins { - id 'me.champeau.jmh' + id 'dd-trace-java.jmh-conventions' id 'dd-trace-java.version-file' } @@ -129,14 +127,4 @@ dependencies { jmh { jmhVersion = libs.versions.jmh.get() duplicateClassesStrategy = DuplicatesStrategy.EXCLUDE - if (project.hasProperty('jmh.includes')) { - includes = [project.property('jmh.includes').replace(',', '|')] - } - if (project.hasProperty('jmh.profilers')) { - profilers = project.property('jmh.profilers').tokenize(',') - } - if (project.hasProperty('testJvm')) { - def testJvmSpec = new TestJvmSpec(project) - jvm = testJvmSpec.javaTestLauncher.map { it.executablePath.asFile.absolutePath } - } } diff --git a/dd-trace-ot/build.gradle.kts b/dd-trace-ot/build.gradle.kts index 1df2d46087b..89d5645f1fa 100644 --- a/dd-trace-ot/build.gradle.kts +++ b/dd-trace-ot/build.gradle.kts @@ -3,7 +3,7 @@ import groovy.lang.Closure plugins { `java-library` id("com.gradleup.shadow") - id("me.champeau.jmh") + id("dd-trace-java.jmh-conventions") } description = "dd-trace-ot" diff --git a/docs/how_to_work_with_gradle.md b/docs/how_to_work_with_gradle.md index df0bfc4f7b0..edf49b46df9 100644 --- a/docs/how_to_work_with_gradle.md +++ b/docs/how_to_work_with_gradle.md @@ -971,6 +971,49 @@ tasks.named("latestDepTest") { ./gradlew allTests -PtestJvm=zulu11 ``` +### JMH Benchmarks (`dd-trace-java.jmh-conventions` Plugin) + +The convention uses the JVM selected by the `dd-trace-java.test-jvm-constraints` plugin via `-PtestJvm` as the +JMH launcher. It also maps optional `-Pjmh.*` project properties to JMH parameters, allowing an individual run +to be adjusted without editing benchmark annotations. +Explicit settings in a module’s `jmh {}` block take precedence (i.e. the managed property won't apply). + +The new properties enable to override the defaults, in other words + +* without `jmh.forks` or `jmh.threads`, forks and threads come from `@Fork` and `@Threads`; otherwise JMH defaults apply, +* without `jmh.includes`, JMH runs all discovered benchmarks, +* without `jmh.profilers`, no profiler is attached unless configured elsewhere. + +```Gradle Kotlin DSL +plugins { + id("dd-trace-java.jmh-conventions") +} + +jmh { + jmhVersion = libs.versions.jmh.get() + duplicateClassesStrategy = DuplicatesStrategy.EXCLUDE + + // jvm, fork, profilers are managed by the jmh convention plugin + threads = 10 // Threads is enforced and jmh.threads won't be applied +} +``` + +| Property | Effect | +|-----------------|---------------------------------------------------------------| +| `jmh.includes` | Comma-separated benchmark name patterns to run (a subset run) | +| `jmh.profilers` | Comma-separated JMH profilers to attach (e.g. `stack`, `gc`) | +| `jmh.forks` | Overrides the fork count for a spot-check run | +| `jmh.threads` | Overrides the thread count for a spot-check run | + +```bash +./gradlew :dd-trace-core:jmh -Pjmh.includes=SpanCreationBenchmark -Pjmh.forks=1 -Pjmh.threads=1 -PtestJvm=21 +``` + +**Tips:** + +* Keep the benchmark's fork and thread settings aligned with what it measures. +* Use `-Pjmh.profilers=gc` when investigating allocations. + ### `tracerJava` Extension Manages multi-version Java source sets, allowing a single project to compile code targeting different JVM versions. @@ -1576,4 +1619,3 @@ The report shows exactly which code paths capture disallowed references. # Validate build logic without running tasks ./gradlew help --scan ``` - diff --git a/internal-api/build.gradle.kts b/internal-api/build.gradle.kts index 65be1563eda..f9dbfd290af 100644 --- a/internal-api/build.gradle.kts +++ b/internal-api/build.gradle.kts @@ -1,10 +1,9 @@ -import datadog.gradle.plugin.testJvmConstraints.TestJvmSpec import de.thetaphi.forbiddenapis.gradle.CheckForbiddenApis import groovy.lang.Closure plugins { `java-library` - id("me.champeau.jmh") + id("dd-trace-java.jmh-conventions") } apply(from = "$rootDir/gradle/java.gradle") @@ -287,13 +286,4 @@ dependencies { jmh { jmhVersion = libs.versions.jmh.get() duplicateClassesStrategy = DuplicatesStrategy.EXCLUDE - - if (project.hasProperty("jmh.includes")) { - includes.add(project.property("jmh.includes") as String) - } - - if (project.hasProperty("testJvm")) { - val testJvmSpec = TestJvmSpec(project) - jvm.set(testJvmSpec.javaTestLauncher.map { it.executablePath.asFile.absolutePath }) - } } diff --git a/internal-api/internal-api-9/build.gradle.kts b/internal-api/internal-api-9/build.gradle.kts index 07683371538..ea876374277 100644 --- a/internal-api/internal-api-9/build.gradle.kts +++ b/internal-api/internal-api-9/build.gradle.kts @@ -1,17 +1,16 @@ import groovy.lang.Closure -import java.nio.file.Paths plugins { `java-library` id("de.thetaphi.forbiddenapis") version "3.10" - id("me.champeau.jmh") + id("dd-trace-java.jmh-conventions") idea } apply(from = "$rootDir/gradle/java.gradle") -extensions.getByName("tracerJava").withGroovyBuilder { - invokeMethod("addSourceSetFor", JavaVersion.VERSION_17) +testJvmConstraints { + minJavaVersion = JavaVersion.VERSION_11 } java { @@ -53,7 +52,4 @@ idea { jmh { jmhVersion = libs.versions.jmh duplicateClassesStrategy = DuplicatesStrategy.EXCLUDE - jvm = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(11) }.map { - it.executablePath.asFile.toString() - } } diff --git a/telemetry/build.gradle.kts b/telemetry/build.gradle.kts index c6e72af33ed..e35d4eeb187 100644 --- a/telemetry/build.gradle.kts +++ b/telemetry/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("me.champeau.jmh") + id("dd-trace-java.jmh-conventions") id("java-library") }