@@ -8,6 +8,7 @@ import org.gradle.api.tasks.testing.logging.TestExceptionFormat
88import org.gradle.internal.jvm.Jvm
99import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
1010import org.jlleitschuh.gradle.ktlint.KtlintExtension
11+ import java.lang.management.ManagementFactory
1112import java.util.Properties
1213import kotlin.math.max
1314import kotlin.system.exitProcess
@@ -44,6 +45,12 @@ val testSummaryService = System.getenv("GITHUB_STEP_SUMMARY")?.let { path ->
4445 }
4546}
4647
48+ /* *
49+ * Per-fork JVM heap for unit tests.
50+ * See [Test.setMaxHeapSize]
51+ */
52+ val unitTestForkMaxHeapGb = 2
53+
4754// Here we extract per-module "best practices" settings to a single top-level evaluation
4855subprojects {
4956 apply (plugin = " org.jlleitschuh.gradle.ktlint" )
@@ -61,7 +68,7 @@ subprojects {
6168 // tell backend to avoid rollover time, and disable interval fuzzing
6269 it.environment(" ANKI_TEST_MODE" , " 1" )
6370
64- it.maxHeapSize = " 2g "
71+ it.maxHeapSize = " ${unitTestForkMaxHeapGb} g "
6572 it.minHeapSize = " 1g"
6673
6774 it.useJUnitPlatform()
@@ -158,6 +165,7 @@ if (jvmVersion !in jvmVersionLowerBound..jvmVersionUpperBound) {
158165}
159166
160167val ciBuild by extra(System .getenv(" CI" ) == " true" ) // true when running on GitHub Actions
168+ val isMacOs = System .getProperty(" os.name" ) == " Mac OS X"
161169// allows for -Dpre-dex=false to be set
162170val preDexEnabled by extra(" true" == System .getProperty(" pre-dex" , " true" ))
163171// allows for universal APKs to be generated
@@ -168,12 +176,48 @@ var androidTestVariantName by extra(
168176 if (testReleaseBuild) " Release" else " Debug"
169177)
170178
179+ private fun sysctl (key : String ): Long =
180+ providers.exec {
181+ commandLine(" sysctl" , " -n" , key)
182+ }.standardOutput.asText.get().trim().toLong()
183+
184+ /* *
185+ * The Gradle daemon's `-Xmx` max heap, in bytes.
186+ *
187+ * Reads from the launch flags as `getRuntime().maxMemory()` is GC-dependent.
188+ *
189+ * @throws IllegalStateException if `-Xmx` is missing or invalid.
190+ */
191+ private fun gradleDaemonHeapBytes (): Long {
192+ val xmx = ManagementFactory .getRuntimeMXBean().inputArguments
193+ .lastOrNull { it.startsWith(" -Xmx" ) } // last -Xmx wins, as in the JVM
194+ ? : error(" Gradle daemon has no -Xmx flag" )
195+ val match = Regex (" -Xmx(\\ d+)([MG])" , RegexOption .IGNORE_CASE ).matchEntire(xmx)
196+ ? : error(" Cannot parse Gradle daemon heap from '$xmx '; expected -Xmx<n>M or -Xmx<n>G" )
197+ val size = match.groupValues[1 ].toLong()
198+ val byteMultiplier = when (match.groupValues[2 ].uppercase()) {
199+ " G" -> 1024L * 1024 * 1024
200+ else -> 1024L * 1024 // M
201+ }
202+ return size * byteMultiplier
203+ }
204+
171205val gradleTestMaxParallelForks by extra(
172- if (System .getProperty(" os.name" ) == " Mac OS X" ) {
173- // macOS reports hardware cores. This is accurate for CI, Intel (halved due to SMT) and Apple Silicon
174- providers.exec {
175- commandLine(" sysctl" , " -n" , " hw.physicalcpu" )
176- }.standardOutput.asText.get().trim().toInt()
206+ if (isMacOs) {
207+ // macOS reports hardware cores.
208+ // This is accurate for CI, Intel (halved due to SMT) and Apple Silicon
209+ val physicalCpus = sysctl(" hw.physicalcpu" )
210+
211+ if (ciBuild) {
212+ // #21168: The `macos-14` CI runner has only 7GB RAM and OOMs (exit 134) so bound by RAM.
213+ // Reserve the daemon's own heap (the OS shares its slack); split the rest into forks.
214+ val forkHeapBytes = unitTestForkMaxHeapGb * 1024L * 1024 * 1024
215+ val availableBytes = sysctl(" hw.memsize" ) - gradleDaemonHeapBytes()
216+ val memoryBoundForkProcesses = max(1L , availableBytes / forkHeapBytes)
217+ minOf(physicalCpus, memoryBoundForkProcesses).toInt()
218+ } else {
219+ physicalCpus.toInt()
220+ }
177221 } else if (ciBuild) {
178222 // GitHub Actions run on Standard_D4ads_v5 Azure Compute Units with 4 vCPUs
179223 // They appear to be 2:1 vCPU to CPU on Linux/Windows with two vCPU cores but with performance 1:1-similar
0 commit comments