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 6a5ffd3a8591c8a4216c52da463200a305a700be Author: Ben Weidig <[email protected]> AuthorDate: Sat Aug 15 10:30:23 2026 +0200 TAP5-2839: rename IntegrationTestMutex, add to JMH runs --- BENCHMARKING.md | 3 ++ buildSrc/README.md | 19 ++++++++++--- .../groovy/t5build/IntegrationTestMutex.groovy | 20 -------------- buildSrc/src/main/groovy/t5build/Mutex.groovy | 32 ++++++++++++++++++++++ .../src/main/groovy/tapestry.jmh-convention.gradle | 14 ++++++++++ .../main/groovy/tapestry.testng-convention.gradle | 6 ++-- 6 files changed, 67 insertions(+), 27 deletions(-) diff --git a/BENCHMARKING.md b/BENCHMARKING.md index 927ff05c2..0ef8f3bca 100644 --- a/BENCHMARKING.md +++ b/BENCHMARKING.md @@ -34,6 +34,9 @@ Benchmarks are **not** part of `check` or `build`, as JMH might take minutes or Every option is documented at the top of [`buildSrc/src/main/groovy/tapestry.jmh-convention.gradle`](buildSrc/src/main/groovy/tapestry.jmh-convention.gradle). +A benchmark measures the machine it runs on, so the `jmh` tasks hold a single build-service permit and run one at a time, even under a parallel build. +`./gradlew jmh` across several modules therefore takes as long as all of them added up, instead of producing numbers measured while the other modules fought for the same cores. + The two that matter most: - **`-Pjmh.quick`**\ diff --git a/buildSrc/README.md b/buildSrc/README.md index e09ca7200..ee11ca66d 100644 --- a/buildSrc/README.md +++ b/buildSrc/README.md @@ -142,15 +142,22 @@ The `testng.xml` suite file for these modules should contain **only** integratio Unit tests are handled by the `test` task via testng-engine by Jupiter and must not appear in `testng.xml`. The task **must** be named `testNG`: the convention gives every task with that name a permit on the -`integrationTestMutex` build service, described below. +`mutex` build service, described below. A differently named task gets no permit and will race the others for the ports. -#### Serializing integration tests +#### Serializing tasks that need the machine to themselves Every Selenium suite binds the same ports, so no two may run at once. Rather than disabling parallel builds (not a simple flag on project-level), the convention registers -an `IntegrationTestMutex` build service with `maxParallelUsages = 1` and declares it on each `testNG` task. -Gradle then admits one integration test task at a time while compilation, asset generation and the unit test suites still run in parallel. +a `Mutex` build service with `maxParallelUsages = 1` and declares it on each `testNG` task. +Gradle then admits one such task at a time while compilation, asset generation and the unit test suites still run in parallel. + +`tapestry.jmh-convention` registers the same service, under the same name, for its `jmh` task. +A benchmark measures the machine it runs on, so a second benchmark or a Selenium suite running beside it does not merely slow the run down, it makes the numbers meaningless. +Sharing one permit across both conventions keeps benchmarks apart from each other **and** from the integration tests. + +The permit only holds back tasks that ask for it, so a compilation or a unit test suite can still land next to a benchmark. +Use `--max-workers=1` when a run has to be quiet. --- @@ -158,3 +165,7 @@ Gradle then admits one integration test task at a time while compilation, asset **`GenerateChecksums`**: custom Gradle task type that generates MD5/SHA-256 checksum files for release archives. + +**`Mutex`**: build service without behaviour, registered with one permit so that the tasks +declaring it run one at a time — the Selenium integration tests, which share fixed ports, and +the JMH benchmarks, which share the machine. diff --git a/buildSrc/src/main/groovy/t5build/IntegrationTestMutex.groovy b/buildSrc/src/main/groovy/t5build/IntegrationTestMutex.groovy deleted file mode 100644 index 245bcc743..000000000 --- a/buildSrc/src/main/groovy/t5build/IntegrationTestMutex.groovy +++ /dev/null @@ -1,20 +0,0 @@ -package t5build - -import org.gradle.api.services.BuildService -import org.gradle.api.services.BuildServiceParameters - -/** - * A build service with no behaviour, used purely as a mutex. - * - * <p>Selenium integration tests bind fixed ports (9090/8443 by default, see - * {@code @TapestryTestConfiguration}), so two modules testing at the same time fight over one - * socket and the loser dies with "Failed to bind". - * <p>Registering this service with {@code maxParallelUsages = 1} and declaring it on - * the integration test tasks tells Gradle to allow only one of them at a time, whereas - * compilation, asset generation and the unit test suites carry on in parallel. - * - * @see tapestry.testng-convention.gradle - */ -abstract class IntegrationTestMutex implements BuildService<BuildServiceParameters.None> -{ -} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/t5build/Mutex.groovy b/buildSrc/src/main/groovy/t5build/Mutex.groovy new file mode 100644 index 000000000..b55a6078c --- /dev/null +++ b/buildSrc/src/main/groovy/t5build/Mutex.groovy @@ -0,0 +1,32 @@ +package t5build + +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters + +/** + * A build service with no behaviour, used purely as a mutex. + * + * <p>Registered with {@code maxParallelUsages = 1} and declared on a task with + * {@code usesService}, it makes Gradle admit one such task at a time, whereas compilation, + * asset generation and the unit test suites carry on in parallel. + * + * <p>Two kinds of task need this, and they share the single permit: + * + * <ul> + * <li><b>Selenium integration tests</b> bind fixed ports (9090/8443 by default, see + * {@code @TapestryTestConfiguration}), so two modules testing at the same time fight over one + * socket and the loser dies with "Failed to bind".</li> + * <li><b>JMH benchmarks</b> measure the machine they run on. A second benchmark task, or a + * Selenium suite, running beside one competes for the same cores and makes the numbers + * meaningless rather than merely slow.</li> + * </ul> + * + * <p>The permit only holds back tasks that ask for it. It cannot keep a compilation or a unit + * test suite off the machine while a benchmark runs; {@code --max-workers=1} does that. + * + * @see tapestry.testng-convention.gradle + * @see tapestry.jmh-convention.gradle + */ +abstract class Mutex implements BuildService<BuildServiceParameters.None> +{ +} diff --git a/buildSrc/src/main/groovy/tapestry.jmh-convention.gradle b/buildSrc/src/main/groovy/tapestry.jmh-convention.gradle index 2e8bde94e..d7fbd96e0 100644 --- a/buildSrc/src/main/groovy/tapestry.jmh-convention.gradle +++ b/buildSrc/src/main/groovy/tapestry.jmh-convention.gradle @@ -10,6 +10,11 @@ // Benchmarks are deliberately NOT part of 'check' or 'build': a JMH run takes // minutes or longer and has no place in CI. // +// A benchmark measures the machine it runs on, so the 'jmh' tasks hold a one-permit +// build service (t5build.Mutex) and run one at a time. './gradlew jmh' over several +// modules therefore takes as long as the sum of them, rather than producing numbers +// taken while the other modules fought for the cores. +// // `./gradlew jmhCompileAll` (root project) compiles every benchmark source set // so they cannot silently rot. // @@ -49,6 +54,8 @@ // // ./gradlew :tapestry-ioc:jmh -Pjmh.include=Lambda -Pjmh.profilers=gc +import t5build.Mutex + plugins { id 'tapestry.java-convention' } @@ -187,6 +194,11 @@ def jmhRunArguments = { File resultFile -> rawArguments() } +// A benchmark has the machine to itself, or it measures the rest of the build instead. +def mutex = gradle.sharedServices.registerIfAbsent('mutex', Mutex) { + maxParallelUsages = 1 +} + 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.' @@ -194,6 +206,8 @@ tasks.register('jmh', JavaExec) { mainClass = 'org.openjdk.jmh.Main' classpath = sourceSets.jmh.runtimeClasspath + usesService(mutex) + // 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 } diff --git a/buildSrc/src/main/groovy/tapestry.testng-convention.gradle b/buildSrc/src/main/groovy/tapestry.testng-convention.gradle index 70d0c7636..f568a0770 100644 --- a/buildSrc/src/main/groovy/tapestry.testng-convention.gradle +++ b/buildSrc/src/main/groovy/tapestry.testng-convention.gradle @@ -14,7 +14,7 @@ // tapestry.testing-base-convention platform runner, BOM, logging // └─ tapestry.testng-convention + TestNG + EasyMock + testng-engine (this file) -import t5build.IntegrationTestMutex +import t5build.Mutex plugins { id 'tapestry.testing-base-convention' @@ -29,12 +29,12 @@ dependencies { // Every Selenium suite binds the same ports, so no two of them may run at once. // Gradle's build services express exactly that: one permit, handed to one integration // test task at a time, while everything else in the build still runs in parallel. -def integrationTestMutex = gradle.sharedServices.registerIfAbsent('integrationTestMutex', IntegrationTestMutex) { +def mutex = gradle.sharedServices.registerIfAbsent('mutex', Mutex) { maxParallelUsages = 1 } tasks.withType(Test).configureEach { if (name == 'testNG') { - usesService(integrationTestMutex) + usesService(mutex) } }
