jamesfredley opened a new pull request, #16168:
URL: https://github.com/apache/grails-core/pull/16168

   > [!NOTE]
   > Stacked on #16158 - this branch is based on it, so review that one first. 
The diff here is only the heap change.
   
   ## What
   
   Replace the hardcoded per-fork test heap with an explicit budget derived 
from the machine.
   
   `gradle/test-config.gradle` has always said:
   
   ```groovy
   maxHeapSize = isCiBuild ? '768m' : '1024m'
   ```
   
   Those literals ignore the machine. Every forked test JVM claimed a gigabyte 
locally no matter how much RAM the developer actually had, and nothing tied 
that number to how many forks could be alive at once.
   
   ## The bound
   
   Forked test JVMs are children of the daemon, so `org.gradle.jvmargs` does 
not limit them. With `org.gradle.parallel=true` several `Test` tasks run 
concurrently, so the live fork count is bounded by Gradle's **build-wide worker 
pool** (`maxWorkerCount`), not by any one task's `maxParallelForks`. A build 
with four tasks capped at two forks apiece still runs up to `maxWorkerCount` 
forks at once, so budgeting off the per-task cap would hand every fork a heap 
the machine cannot honour.
   
   This is the same bound - and the same reasoning - that 
`ActiveProcessorCountArgumentProvider` already uses for CPU count a few lines 
below in `build.gradle`. This PR applies it to memory.
   
   The budget: physical RAM, minus the daemon's own heap, halved to leave room 
for the OS, the compiler workers (`-Xmx2G` each via `CompilePlugin`) and the 
Docker containers the mongodb/redis/geb suites start per fork, divided by the 
worker count, clamped to `[512, 1024]`.
   
   The daemon heap is read from `Runtime.runtime.maxMemory()` rather than 
parsed out of `org.gradle.jvmargs` - the build script runs *in* the daemon, so 
this sidesteps multiple `-Xmx` options (the JVM honours the last) and 
unit-parsing entirely.
   
   ## CI is unchanged, by construction
   
   On CI the value is still exactly `768`. This is load-bearing: the PR makes 
the policy explicit and fixes **local** overcommit only. Changing CI numbers 
belongs in a later, measured PR - mixing them here would make any CI flake 
impossible to attribute ("was it the heap or the CPU cap?").
   
   Both `-PtestForkHeapMb` and `isCiBuild` settle the value *before* the probe 
runs, so the CI path performs no fallible work at all.
   
   ## Measured
   
   Real resolved `Test` task properties, captured with a throwaway init script 
(since deleted), not hand arithmetic:
   
   | Scenario | workers | forks | `maxHeapSize` |
   |---|---|---|---|
   | local (64 GB, `-Xmx5G` daemon) | 20 | 10 | `1024m` (ceiling) |
   | `CI=true` | 20 | 4 | **`768m`** |
   | `CI=true -PmaxTestParallel=4` | 20 | 4 | **`768m`** |
   | `-PtestForkHeapMb=900` | 20 | 10 | `900m` |
   | `CI=true -PtestForkHeapMb=900` | 20 | 4 | `900m` |
   | local `--max-workers=64` | 64 | 10 | `512m` (floor) |
   
   Verified on both `grails-core:test` and `grails-test-suite-uber:test`.
   
   ## grails-test-suite-uber
   
   That module carried its **own copy** of the same `768m`/`1024m` literal and 
never applied `gradle/test-config.gradle`, so the heaviest suite in the build 
would have been the one module exempt from the budget. It now reads the same 
property. `grails-test-suite-persistence` keeps its deliberate `2048m` - that 
is a specialized limit, not a copy-paste default.
   
   ## Known limit (deliberately not papered over)
   
   When the computed share falls below the 512m floor, the floor wins and the 
forks collectively still exceed the budget. Heap alone cannot fix that: a fork 
below ~512m cannot run these suites. The knob that has to come down on such a 
host is the **fork count** (`-PmaxTestParallel`, or `--max-workers` which 
bounds them build-wide). This budget lowers memory pressure; it does not by 
itself prove a constrained machine fits. The code says so where it clamps.
   
   ## Why draft
   
   The constants are judgement calls, not measurements, and I would rather 
agree the policy than defend the numbers:
   
   - Is `512` a safe floor for the heaviest suites, or should the fork count 
drop instead once the share falls that low?
   - Should the reserve be "half the remainder", or should the `-Xmx2G` 
compiler workers be subtracted explicitly?
   
   ## Scope
   
   - Root build only. `grails-gradle` and `grails-forge` are separate Gradle 
builds with their own daemon settings, their own `configuredTestParallel` and 
their own heap literals - deliberately a follow-up, not silent collateral.
   - No test is added, removed, skipped or weakened. Heap sizing does not 
change test selection.
   - Reading physical RAM degrades safely: a failed probe keeps the previous 
`1024`, and a *successful* probe on a machine whose RAM sits inside the daemon 
heap drops to the floor rather than pretending the probe failed.
   
   ## Verification
   
   - `./gradlew help` - EXIT 0
   - `CI=true ./gradlew help -PmaxTestParallel=4` - EXIT 0
   - `./gradlew help -PtestForkHeapMb=900` - EXIT 0
   - `./gradlew validateActions` - EXIT 0
   
   ## Related
   
   Follow-up to #16158 (CPU oversubscription) and #16167 (daemon heap vs runner 
RAM). Three separate knobs, deliberately three separate PRs: CPU, daemon heap, 
fork heap.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to