Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,14 @@ ext {
// needing --rerun-tasks. Useful for repeatedly running the same test command while
// chasing flaky tests across runs.
doNotCacheTests = System.getenv('DO_NOT_CACHE_TESTS')?.toBoolean()
configuredTestParallel = findProperty('maxTestParallel') as Integer ?: (isCiBuild ? 4 : Runtime.runtime.availableProcessors() * 3 / 4 as int ?: 1)
// availableProcessors() reports LOGICAL processors: SMT threads on x64, and on Apple silicon
// every efficiency core as well as every performance core. Taking 3/4 of that therefore
// asked for more concurrent Grails/Spring test JVMs than the machine has real capacity for
// (each fork also holds a 1G heap, and the mongodb, redis and geb suites start a Docker
// container per fork). Half is the conservative fraction. CI keeps its existing budget, so
// this does not reduce CI fork counts - though CI forks do see fewer processors, below.
configuredTestParallel = findProperty('maxTestParallel') as Integer ?:
(isCiBuild ? 4 : Math.max(1, (Runtime.runtime.availableProcessors() / 2) as int))
excludeUnusedTransDeps = findProperty('excludeUnusedTransDeps')

testProjectsStartWith = [
Expand All @@ -60,8 +67,53 @@ ext {
profileProjects = [ /* Will be populated by subprojects loop below */]
}

/**
* Tells a forked test JVM how many processors it may size its GC and JIT thread pools from.
*
* Every forked JVM otherwise sizes those pools for the WHOLE machine, because no fork knows the
* others exist. On a 20-processor host that is 15 ParallelGCThreads + 4 ConcGCThreads + 12
* CICompilerCount = 31 threads per fork before a single test runs, so N concurrent forks become
* N*31 threads competing for the same cores. Gradle cannot see this: it charges each fork one
* worker lease, as though a fork were a single thread.
*
* The share is availableProcessors / maxWorkerCount, i.e. the BUILD-WIDE worker limit, because
* that - not any one task's maxParallelForks - is what bounds how many forks can run at once.
* With org.gradle.parallel=true, Test tasks from several projects execute concurrently, so a
* per-task share would still oversubscribe the machine.
*
* Supplied as an argument provider rather than through jvmArgs because several modules assign
* jvmArgs wholesale in their own build scripts, which would discard anything added here first.
*/
final class ActiveProcessorCountArgumentProvider implements CommandLineArgumentProvider {

@Input
final int processorCount

ActiveProcessorCountArgumentProvider(int availableProcessorCount, int maxWorkerCount) {
// Floor of 2: at ActiveProcessorCount=1 HotSpot's ergonomics select Serial GC, which
// costs more on a 1G test heap than the contention it saves. Two keeps G1 with a
// bounded worker count.
//
// That floor also means -PmaxTestParallel=1 still advertises 2 processors to the
// single fork whenever maxWorkerCount is larger than availableProcessors/2 (the
// usual local case: org.gradle.parallel=true with no org.gradle.workers.max).
// It is not a "give the one fork the whole machine" switch.
this.processorCount = Math.max(2, (availableProcessorCount / Math.max(1, maxWorkerCount)) as int)
}

@Override
Iterable<String> asArguments() {
["-XX:ActiveProcessorCount=$processorCount".toString()]
}
}

subprojects {

tasks.withType(Test).configureEach { testTask ->
testTask.jvmArgumentProviders.add(new ActiveProcessorCountArgumentProvider(
Runtime.runtime.availableProcessors(), gradle.startParameter.maxWorkerCount))
}

for (String testPrefix : testProjectsStartWith) {
if (name.startsWith(testPrefix)) {
rootProject.ext['testProjects'] << name
Expand Down
2 changes: 1 addition & 1 deletion gradle/functional-test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ tasks.withType(Test).configureEach { Test task ->
showStackTraces = true
}
if (findProperty('testForked')) {
task.maxParallelForks = configuredTestParallel
task.maxParallelForks = rootProject.ext.configuredTestParallel
task.forkEvery = (findProperty('testForkEvery') ?: 0)
}
if (findProperty('testJvmArgs')) {
Expand Down
4 changes: 2 additions & 2 deletions gradle/mongodb-forked-test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ tasks.withType(Test).configureEach {
}

useJUnitPlatform()
maxParallelForks = configuredTestParallel
maxParallelForks = rootProject.ext.configuredTestParallel
jvmArgs = ['-Xmx768M']
addTestListener([
afterSuite: { desc, result ->
Expand All @@ -67,4 +67,4 @@ tasks.withType(Test).configureEach {
}
// Used in the TCK test to selectively enable/disable tests
systemProperty('mongodb.gorm.suite', 'true')
}
}
2 changes: 1 addition & 1 deletion gradle/test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ tasks.withType(Test).configureEach {
showStackTraces = true
}
excludes = ['**/*TestCase.class', '**/*$*.class']
maxParallelForks = configuredTestParallel
maxParallelForks = rootProject.ext.configuredTestParallel
maxHeapSize = isCiBuild ? '768m' : '1024m'
forkEvery = hasProperty('forkEveryUnitTest') ? getProperty('forkEveryUnitTest') as long : (isCiBuild ? 50 : 100)
if (System.getProperty('debug.tests')) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 grails.util;

import java.lang.management.ManagementFactory;
import java.util.List;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Asserts that {@code -XX:ActiveProcessorCount} from the root build's
* {@code jvmArgumentProviders} actually reaches this forked test JVM.
* Wholesale {@code jvmArgs = ...} assignments in module scripts would drop a
* flag added via {@code jvmArgs}, which is why the build uses a provider.
*/
class ActiveProcessorCountForkTests {

private static final String FLAG_PREFIX = "-XX:ActiveProcessorCount=";

@Test
void forkedTestJvmReceivesActiveProcessorCount() {
String flag = findActiveProcessorCountFlag();
assertNotNull(flag,
"forked test JVM must receive -XX:ActiveProcessorCount from jvmArgumentProviders");
int advertised = Integer.parseInt(flag.substring(FLAG_PREFIX.length()));
assertTrue(advertised >= 2, "floor is 2 so HotSpot keeps G1: " + advertised);
assertEquals(advertised, Runtime.getRuntime().availableProcessors(),
"HotSpot must honour the advertised processor count");
}

private static String findActiveProcessorCountFlag() {
List<String> args = ManagementFactory.getRuntimeMXBean().getInputArguments();
for (String arg : args) {
if (arg.startsWith(FLAG_PREFIX)) {
return arg;
}
}
return null;
}
}
2 changes: 1 addition & 1 deletion grails-data-neo4j/grails-datastore-gorm-neo4j/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ dependencies {

test {
useJUnitPlatform()
maxParallelForks = configuredTestParallel
maxParallelForks = rootProject.ext.configuredTestParallel
forkEvery = 10

jvmArgs = ['-Xmx1028M']
Expand Down
28 changes: 26 additions & 2 deletions grails-forge/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,32 @@ ext {
// needing --rerun-tasks. Useful for repeatedly running the same test command while
// chasing flaky tests across runs.
doNotCacheTests = System.getenv('DO_NOT_CACHE_TESTS')?.toBoolean()
// Mirror grails-gradle: honour -PmaxTestParallel, and cap CI forks so a 4-vCPU
// runner does not inherit the local "half the logical processors" formula.
configuredTestParallel = findProperty('maxTestParallel') as Integer ?:
(isCiBuild ? 3 : Math.max(1, (Runtime.runtime.availableProcessors() / 2) as int))
}

/** Caps each test fork's view of the machine. See the root build.gradle for the rationale. */
final class ActiveProcessorCountArgumentProvider implements CommandLineArgumentProvider {

@Input
final int processorCount

ActiveProcessorCountArgumentProvider(int availableProcessorCount, int maxWorkerCount) {
// Floor of 2 keeps G1. -PmaxTestParallel=1 still sees 2 processors when
// maxWorkerCount is the usual unrestricted local worker pool.
this.processorCount = Math.max(2, (availableProcessorCount / Math.max(1, maxWorkerCount)) as int)
}

@Override
Iterable<String> asArguments() {
["-XX:ActiveProcessorCount=$processorCount".toString()]
}
}

allprojects {
tasks.withType(Test).configureEach { Task testTask ->
tasks.withType(Test).configureEach { testTask ->
testTask.dependsOn(
gradle.includedBuild('grails-core').task(':publishAllPublicationsToTestCaseMavenRepoRepository'),
gradle.includedBuild('grails-gradle').task(':publishAllPublicationsToTestCaseMavenRepoRepository')
Expand All @@ -55,6 +77,8 @@ allprojects {
// without --rerun-tasks (and without recompiling everything else).
testTask.outputs.cacheIf { !doNotCacheTests }
testTask.outputs.upToDateWhen { !doNotCacheTests }
testTask.jvmArgumentProviders.add(new ActiveProcessorCountArgumentProvider(
Runtime.runtime.availableProcessors(), gradle.startParameter.maxWorkerCount))
}
}

Expand All @@ -72,4 +96,4 @@ apply {
// we must apply the publish configuration first or the docs config will not work
from layout.projectDirectory.file('gradle/publish-root-config.gradle')
from layout.projectDirectory.file('gradle/gradle-wrapper-root-config.gradle')
}
}
2 changes: 1 addition & 1 deletion grails-forge/gradle/test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ tasks.withType(Test).configureEach {
jvmArgs('-Duser.country=US', '-Duser.language=en',
'--add-opens', 'java.base/java.lang=ALL-UNNAMED')
forkEvery = 100
maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
maxParallelForks = rootProject.ext.configuredTestParallel
maxHeapSize = '2G'

useJUnitPlatform()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.grails.forge

import java.lang.management.ManagementFactory

import spock.lang.Specification

class ActiveProcessorCountForkSpec extends Specification {

private static final String FLAG_PREFIX = '-XX:ActiveProcessorCount='

void 'forked test JVM receives ActiveProcessorCount from jvmArgumentProviders'() {
given:
String flag = ManagementFactory.runtimeMXBean.inputArguments.find { it.startsWith(FLAG_PREFIX) }

expect:
flag != null
int advertised = Integer.parseInt(flag.substring(FLAG_PREFIX.length()))
advertised >= 2
Runtime.runtime.availableProcessors() == advertised
}
}
30 changes: 29 additions & 1 deletion grails-gradle/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,35 @@ ext {
// needing --rerun-tasks. Useful for repeatedly running the same test command while
// chasing flaky tests across runs.
doNotCacheTests = System.getenv('DO_NOT_CACHE_TESTS')?.toBoolean()
configuredTestParallel = findProperty('maxTestParallel') as Integer ?: (isCiBuild ? 3 : Runtime.runtime.availableProcessors() * 3/4 as int ?: 1)
// Half the LOGICAL processors, not 3/4. This separate Gradle build mirrors the root
// build.gradle - see it for the full rationale. CI keeps its existing budget.
configuredTestParallel = findProperty('maxTestParallel') as Integer ?:
(isCiBuild ? 3 : Math.max(1, (Runtime.runtime.availableProcessors() / 2) as int))
}

/** Caps each test fork's view of the machine. See the root build.gradle for the rationale. */
final class ActiveProcessorCountArgumentProvider implements CommandLineArgumentProvider {

@Input
final int processorCount

ActiveProcessorCountArgumentProvider(int availableProcessorCount, int maxWorkerCount) {
// Floor of 2 keeps G1. -PmaxTestParallel=1 still sees 2 processors when
// maxWorkerCount is the usual unrestricted local worker pool.
this.processorCount = Math.max(2, (availableProcessorCount / Math.max(1, maxWorkerCount)) as int)
}

@Override
Iterable<String> asArguments() {
["-XX:ActiveProcessorCount=$processorCount".toString()]
}
}

subprojects {
tasks.withType(Test).configureEach { testTask ->
testTask.jvmArgumentProviders.add(new ActiveProcessorCountArgumentProvider(
Runtime.runtime.availableProcessors(), gradle.startParameter.maxWorkerCount))
}
}

apply {
Expand Down
2 changes: 1 addition & 1 deletion grails-gradle/gradle/test-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ tasks.withType(Test).configureEach {
showStackTraces = true
}
excludes = ['**/*TestCase.class', '**/*$*.class']
maxParallelForks = configuredTestParallel
maxParallelForks = rootProject.ext.configuredTestParallel
maxHeapSize = isCiBuild ? '768m' : '1024m'
if (System.getProperty('debug.tests')) {
jvmArgs += debugArguments
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.grails.gradle.plugin.core

import java.lang.management.ManagementFactory

import spock.lang.Specification

class ActiveProcessorCountForkSpec extends Specification {

private static final String FLAG_PREFIX = '-XX:ActiveProcessorCount='

void 'forked test JVM receives ActiveProcessorCount from jvmArgumentProviders'() {
given:
String flag = ManagementFactory.runtimeMXBean.inputArguments.find { it.startsWith(FLAG_PREFIX) }

expect:
flag != null
int advertised = Integer.parseInt(flag.substring(FLAG_PREFIX.length()))
advertised >= 2
Runtime.runtime.availableProcessors() == advertised
}
}
2 changes: 1 addition & 1 deletion grails-test-suite-persistence/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ dependencies {
}

test {
maxParallelForks = configuredTestParallel
maxParallelForks = rootProject.ext.configuredTestParallel
forkEvery = isCiBuild ? 25 : 100
if(!isCiBuild) {
maxHeapSize = '2048m'
Expand Down
15 changes: 9 additions & 6 deletions grails-test-suite-uber/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,6 @@ isolatedTestPatterns.keySet().each { taskName ->
}
}

tasks.named('isolatedTestsTwo', Test) {
maxParallelForks = 1
forkEvery = 100
}

tasks.withType(Test).configureEach {
// Honor DO_NOT_CACHE_TESTS=1 so developers can repeatedly invoke the same test command
// without --rerun-tasks (and without recompiling everything else).
Expand All @@ -135,12 +130,20 @@ tasks.withType(Test).configureEach {

onlyIf { !testSkippingProperties.any {project.hasProperty(it) } }
useJUnitPlatform()
maxParallelForks = configuredTestParallel
maxParallelForks = rootProject.ext.configuredTestParallel
forkEvery = isCiBuild ? 50 : 100
maxHeapSize = isCiBuild ? '768m' : '1024m'
jvmArgs('--add-opens=java.base/java.lang=ALL-UNNAMED', '--add-opens=java.base/java.util=ALL-UNNAMED')
}

// Must be configured AFTER the tasks.withType(Test) block above. Both are deferred
// configuration actions and Gradle runs them in registration order, so a tasks.named
// block registered first would be silently overwritten by the later configureEach.
tasks.named('isolatedTestsTwo', Test) {
maxParallelForks = 1
forkEvery = 100
}

tasks.named('test', Test) {
// Exclude the isolated tests from the main test task
filter.excludePatterns = isolatedTestPatterns.values().flatten()
Expand Down
Loading
Loading