This is an automated email from the ASF dual-hosted git repository.

benweidig pushed a commit to branch TAP5-2839
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git

commit 0d207439bb4b6514c3b2fccb2829d052aa258e35
Author: Ben Weidig <[email protected]>
AuthorDate: Sat Aug 15 10:08:08 2026 +0200

    TAP5-2839: add JMH (convention, version catalog)
---
 build.gradle                                       |  10 +
 .../src/main/groovy/tapestry.jmh-convention.gradle | 222 +++++++++++++++++++++
 gradle/libs.versions.toml                          |   9 +
 3 files changed, 241 insertions(+)

diff --git a/build.gradle b/build.gradle
index 9b34d28ac..83403fe7a 100755
--- a/build.gradle
+++ b/build.gradle
@@ -295,6 +295,16 @@ tasks.register('aggregateJavadoc', Javadoc) {
     }
 }
 
+// TAP5-2839: Add JMH
+tasks.register('jmhCompileAll') {
+    group = 'benchmark'
+    description = 'Compiles the JMH benchmark sources of every module that has 
them'
+
+    dependsOn provider {
+        rootProject.getTasksByName('jmhClasses', true)
+    }
+}
+
 tasks.register('typeScriptDocs') {
     group = 'documentation'
     description = 'Builds typedoc documentation for all TypeScript sources'
diff --git a/buildSrc/src/main/groovy/tapestry.jmh-convention.gradle 
b/buildSrc/src/main/groovy/tapestry.jmh-convention.gradle
new file mode 100644
index 000000000..2e8bde94e
--- /dev/null
+++ b/buildSrc/src/main/groovy/tapestry.jmh-convention.gradle
@@ -0,0 +1,222 @@
+// Use for modules that carry JMH microbenchmarks under src/jmh/java.
+//
+// Adds a 'jmh' source set that sees the module's own classes and its compile
+// dependencies (including the 'provided' configuration), wires the JMH 
annotation
+// processor that generates the benchmark harness, and registers two tasks:
+//
+//   ./gradlew :tapestry-core:jmhList     lists the benchmarks of the module
+//   ./gradlew :tapestry-core:jmh         runs them
+//
+// Benchmarks are deliberately NOT part of 'check' or 'build': a JMH run takes
+// minutes or longer and has no place in CI.
+//
+// `./gradlew jmhCompileAll` (root project) compiles every benchmark source set
+// so they cannot silently rot.
+//
+// Run options, all passed as -P properties:
+//
+//   -Pjmh.include=Regex     benchmarks to run, matched against the fully 
qualified
+//                           method name (default: all of them)
+//   -Pjmh.exclude=Regex     benchmarks to skip
+//   -Pjmh.quick             one fork, 3x1s warmup, 3x1s measurement. Good 
enough to
+//                           see a 2x difference, NOT good enough to publish. 
Do not use it
+//                           on the cold/startup benchmarks: those measure a 
single shot in a
+//                           fresh JVM, and forcing three measurement 
iterations means two of
+//                           the three run against an already-warmed JVM. 
Override -Pjmh.forks
+//                           on its own instead. Any of the properties below 
overrides the
+//                           matching -Pjmh.quick value, the rest of it stays 
in effect.
+//   -Pjmh.forks=N           JVM forks per benchmark (JMH default: 5)
+//   -Pjmh.warmups=N         warmup iterations per fork
+//   -Pjmh.iterations=N      measurement iterations per fork
+//   -Pjmh.warmupTime=1s     duration of a warmup iteration
+//   -Pjmh.time=1s           duration of a measurement iteration
+//   -Pjmh.threads=N         threads per benchmark ('max' for one per core)
+//   -Pjmh.mode=avgt         benchmark mode override (thrpt|avgt|sample|ss|all)
+//   -Pjmh.profilers=gc,stack   comma-separated JMH profilers; 'gc' reports the
+//                           allocation rate, which is what most Tapestry
+//                           benchmarks are actually about
+//   -Pjmh.jvmArgs=-Xmx2g    extra JVM arguments for the forked JVMs
+//   -Pjmh.args=...          raw arguments appended verbatim, for anything 
above
+//                           that this convention does not expose
+//   -Pjmh.resultFile=path   where to write the JSON results, relative to the 
root project
+//                           instead of the module's build directory. Use it 
to keep a
+//                           before/after pair side by side, see 
benchmark-results/README.md
+//
+// Results are always written as JSON to build/reports/jmh/results.json, so two
+// runs can be diffed (for instance with https://jmh.morethan.io/).
+//
+// Example, comparing anonymous classes against lambdas with allocation 
numbers:
+//
+//   ./gradlew :tapestry-ioc:jmh -Pjmh.include=Lambda -Pjmh.profilers=gc
+
+plugins {
+    id 'tapestry.java-convention'
+}
+
+sourceSets {
+    jmh {
+        compileClasspath += sourceSets.main.output + configurations.provided
+        runtimeClasspath += sourceSets.main.output + configurations.provided
+    }
+}
+
+configurations {
+    // Benchmarks need everything the module itself needs
+    jmhImplementation.extendsFrom api, implementation
+    jmhRuntimeOnly.extendsFrom runtimeOnly
+}
+
+dependencies {
+    jmhImplementation libs.jmh.core
+    jmhAnnotationProcessor libs.jmh.core
+    jmhAnnotationProcessor libs.jmh.generator.annprocess
+
+    // Benchmarks that boot an IoC registry log during setup.
+    // Without a binding SLF4J prints its warning into the middle of the 
measurement output.
+    jmhRuntimeOnly libs.slf4j.simple
+}
+
+idea.module.testSources.from(sourceSets.jmh.java.srcDirs)
+eclipse.classpath.plusConfigurations += [configurations.jmhRuntimeClasspath]
+
+def jmhResults = layout.buildDirectory.file('reports/jmh/results.json')
+
+// Everything below translates the -P properties documented above into JMH 
command line
+// arguments, one closure per group of arguments.
+
+// The value of -Pjmh.<name>, trimmed, or null when the property is unset or 
blank.
+def jmhProperty = { String name ->
+    project.findProperty("jmh.${name}")?.toString()?.trim() ?: null
+}
+
+// The JMH flag of every property that carries a single value, in the order 
they are passed.
+def valueFlags = [
+        forks     : '-f',
+        warmups   : '-wi',
+        iterations: '-i',
+        warmupTime: '-w',
+        time      : '-r',
+        threads   : '-t',
+        mode      : '-bm']
+
+// What -Pjmh.quick stands for: "I am iterating on an optimization and want a 
number now".
+def quickValues = [
+        forks     : '1',
+        warmups   : '3',
+        iterations: '3',
+        warmupTime: '1s',
+        time      : '1s']
+
+// Which benchmarks to touch.
+// JMH takes the include pattern as its positional argument.
+def selectionArguments = {
+
+    def args = [jmhProperty('include') ?: '.*']
+
+    def exclude = jmhProperty('exclude')
+    if (exclude)
+    {
+        args += ['-e', exclude]
+    }
+
+    return args
+}
+
+// How to run them.
+// JMH refuses a flag it sees twice ("Found multiple arguments for option f"),
+// so an explicit property has to replace its -Pjmh.quick counterpart rather 
than follow it.
+def measurementArguments = {
+
+    def quick = project.hasProperty('jmh.quick')
+    def args = []
+
+    valueFlags.each { property, flag ->
+        def value = jmhProperty(property) ?: (quick ? quickValues[property] : 
null)
+        if (value)
+        {
+            args += [flag, value]
+        }
+    }
+
+    jmhProperty('profilers')?.split(',')?.each { profiler ->
+        args += ['-prof', profiler.trim()]
+    }
+
+    return args
+}
+
+// Benchmarks that boot a registry or load a page log a line per operation at 
INFO, which
+// would be measured along with the work and would drown the results.
+// JMH splits a single -jvmArgsAppend on spaces, so everything has to go into 
one value.
+def jvmArguments = {
+
+    def jvmArgs = ['-Dorg.slf4j.simpleLogger.defaultLogLevel=warn']
+
+    def extraJvmArgs = jmhProperty('jvmArgs')
+    if (extraJvmArgs)
+    {
+        jvmArgs << extraJvmArgs
+    }
+
+    return ['-jvmArgsAppend', jvmArgs.join(' ')]
+}
+
+// Where the JSON results go: the module's own build directory unless 
-Pjmh.resultFile
+// points somewhere else, relative to the root project.
+def jmhResultFile = {
+
+    def override = jmhProperty('resultFile')
+
+    return override ? project.rootProject.file(override) : 
jmhResults.get().asFile
+}
+
+// -Pjmh.args, appended verbatim for anything this convention does not expose.
+def rawArguments = {
+    jmhProperty('args')?.split('\\s+')?.toList() ?: []
+}
+
+def jmhListArguments = {
+    selectionArguments() + ['-l']
+}
+
+def jmhRunArguments = { File resultFile ->
+    selectionArguments() +
+            measurementArguments() +
+            jvmArguments() +
+            ['-rf', 'json', '-rff', resultFile.absolutePath] +
+            rawArguments()
+}
+
+tasks.register('jmh', JavaExec) {
+    group = 'benchmark'
+    description = 'Runs this module\'s JMH benchmarks. Use -Pjmh.include=Regex 
to pick some, -Pjmh.quick for a rough number.'
+
+    mainClass = 'org.openjdk.jmh.Main'
+    classpath = sourceSets.jmh.runtimeClasspath
+
+    // A benchmark result is never up to date: the point of re-running it is 
that
+    // the machine, the JDK or the code under measurement may have changed.
+    outputs.upToDateWhen { false }
+
+    doFirst {
+        def resultFile = jmhResultFile()
+        resultFile.parentFile.mkdirs()
+
+        args = jmhRunArguments(resultFile)
+        logger.lifecycle "JMH: ${args.join(' ')}"
+    }
+}
+
+tasks.register('jmhList', JavaExec) {
+    group = 'benchmark'
+    description = 'Lists the JMH benchmarks of this module without running 
them.'
+
+    mainClass = 'org.openjdk.jmh.Main'
+    classpath = sourceSets.jmh.runtimeClasspath
+
+    outputs.upToDateWhen { false }
+
+    doFirst {
+        args = jmhListArguments()
+    }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 156cdb007..35432f06b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -79,6 +79,10 @@ easymock = "5.6.0"
 
 spock = "2.4-groovy-4.0"
 
+# BENCHMARKING
+
+jmh = "1.37"
+
 selenium-java    = "4.12.1"
 selenium-legrc   = "4.5.0"
 webdrivermanager = "5.5.2"
@@ -206,6 +210,11 @@ easymock = { module = "org.easymock:easymock", version.ref 
= "easymock" }
 spock-bom  = { module = "org.spockframework:spock-bom", version.ref = "spock" }
 spock-core = { module = "org.spockframework:spock-core" }
 
+# BENCHMARKING
+
+jmh-core                 = { module = "org.openjdk.jmh:jmh-core", version.ref 
= "jmh" }
+jmh-generator-annprocess = { module = 
"org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" }
+
 selenium-java    = { module = "org.seleniumhq.selenium:selenium-java", 
version.ref = "selenium-java" }
 selenium-legrc   = { module = "org.seleniumhq.selenium:selenium-leg-rc", 
version.ref = "selenium-legrc" }
 webdrivermanager = { module = "io.github.bonigarcia:webdrivermanager", 
version.ref = "webdrivermanager" }

Reply via email to