build(ci): bound concurrent JVMs on the macOS runner - #16167
build(ci): bound concurrent JVMs on the macOS runner#16167jamesfredley wants to merge 2 commits into
Conversation
org.gradle.jvmargs sizes the Gradle daemon only. Test forks are separate child JVMs that take their heap from maxHeapSize in gradle/test-config.gradle, so a job's configured heap is the daemon -Xmx plus the concurrent test forks times the per-fork heap. Those two numbers live in different files and have never been reasoned about together. The concurrent fork count is not maxParallelForks. With org.gradle.parallel=true several Test tasks run at once, so the live JVM count is bounded by Gradle's global worker pool, which defaults to the CPU count. On the 4-CPU, ~16 GB Linux and Windows runners that floor is 5G + 4x768m = 8G and fits. On the 3-CPU, ~7 GB macOS runner it is 5G + 3x768m = 7.25G and does not. Cap --max-workers on the macOS leg, since that is what actually limits concurrent test and compiler JVMs, and keep maxTestParallel alongside it so no single task exceeds the same cap. Both are passed through a new runner_arguments matrix key that is undefined, and therefore empty, for every other entry. The daemon stays at 5 GB: groovydoc is what needs it, and shrinking it would trade a memory problem for a slower build. Document the arithmetic next to org.gradle.jvmargs as a simplified configured-heap floor, explicitly excluding metaspace, native memory and the forked compiler workers that CompilePlugin gives their own -Xmx2G, so it is not mistaken for a true peak. This changes concurrency only. No test is added, removed, skipped or weakened. Assisted-by: claude-code:claude-opus-5
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #16167 +/- ##
=============================================
Coverage 52.3344% 52.3344%
Complexity 18547 18547
=============================================
Files 2039 2039
Lines 97521 97521
Branches 17143 17143
=============================================
Hits 51037 51037
Misses 38998 38998
Partials 7486 7486 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
This PR makes Gradle CI memory usage on macOS deterministic by explicitly capping Gradle’s global worker pool (and test fork parallelism) to prevent over-committing the smaller macOS GitHub-hosted runner.
Changes:
- Add macOS-only Gradle CLI caps (
--max-workers=2and-PmaxTestParallel=2) via a newrunner_argumentsmatrix field. - Document how
org.gradle.jvmargs(daemon) and test fork heaps combine into an overall CI memory budget.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
gradle.properties |
Adds documentation explaining the combined daemon + test fork heap budgeting rationale. |
.github/workflows/gradle.yml |
Adds macOS-only runner arguments and wires them into the Gradle invocation. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| java: 21 | ||
| job_name: macOS JDK 21 | ||
| gradle_task: 'build :grails-shell-cli:installDist groovydoc' | ||
| # The macOS runner has ~7 GB of RAM and 3 CPUs, against ~16 GB and 4 CPUs on the | ||
| # Linux and Windows runners, while org.gradle.jvmargs still asks for a 5 GB daemon | ||
| # (groovydoc needs it). Capping only maxTestParallel would not help: that is a | ||
| # per-Test-task limit, and with org.gradle.parallel=true several projects' test | ||
| # tasks run at once, so the number of live forks is bounded by Gradle's global | ||
| # worker pool - which defaults to the 3 CPUs here. --max-workers is therefore the | ||
| # setting that actually limits concurrent test and compiler JVMs; maxTestParallel | ||
| # is kept alongside it so no single task exceeds that cap either. This reduces | ||
| # memory pressure on the smallest runner rather than proving the job fits. | ||
| runner_arguments: '--max-workers=2 -PmaxTestParallel=2' | ||
| cache_writer: true | ||
| - os: windows-latest | ||
| java: 25 |
| -PonlyCoreTests | ||
| -PskipCodeStyle | ||
| ${{ matrix.shard_arguments }} | ||
| ${{ matrix.runner_arguments }} |
| -PonlyCoreTests | ||
| -PskipCodeStyle | ||
| ${{ matrix.shard_arguments }} | ||
| ${{ matrix.runner_arguments }} |
✅ All tests passed ✅🏷️ Commit: 728f00f Learn more about TestLens at testlens.app/docs. |
What
Make the CI memory budget explicit, and stop the macOS runner from being over-committed.
org.gradle.jvmargssizes the Gradle daemon only. Test forks are separate child JVMs and take their heap frommaxHeapSizeingradle/test-config.gradle. Those two numbers are set in different files and have never been reasoned about together, so nothing stops their sum from exceeding the runner.The real peak of a CI job is:
The number of concurrent forks is not
maxParallelForks. Withorg.gradle.parallel=trueseveralTesttasks run at once, each entitled to its own forks, so the true bound is Gradle's global worker pool (--max-workers, defaulting to the CPU count).Against the GitHub-hosted runner specs:
ubuntu-latest/windows-latestmacos-latest(M1)Linux and Windows have the headroom. macOS does not: the worst case exceeds the machine.
Change
.github/workflows/gradle.yml: the macOS entry - and only that entry - now passes-PmaxTestParallel=2 --max-workers=2, via a newrunner_argumentsmatrix key that is empty for every other entry. Capping--max-workersis the part that actually bounds concurrent forks;-PmaxTestParallelalone would not, for the reason above. New peak:5G + 2x768m = 6.5 GB, which fits in 7 GB.gradle.properties: document the arithmetic next toorg.gradle.jvmargs, including the daemon-vs-fork distinction and the worker-pool bound, so the next person changing either number can see both sides of the budget.The daemon stays at 5 GB deliberately:
groovydocis what needs it, and shrinking it would trade a memory problem for a slower build.Why draft
The macOS runner has 3 CPUs, so going from 3 workers to 2 will cost some wall-clock on that job. That trade is worth discussing, and the alternative - a smaller daemon plus more workers - is worth measuring rather than assuming. Opening as a draft to get that decision made before it lands.
Scope
runner_argumentsis undefined (empty) for the Ubuntu and Windows entries.grails-gradleandgrails-forgeare separate Gradle builds with their own daemon settings and are not touched here.Verification
./gradlew validateActions help -PmaxTestParallel=2- BUILD SUCCESSFUL, "Checked 24 workflow file(s) - all compliant".Related
Follow-up to #16158, which capped the CPUs a forked test JVM believes it has. That PR deliberately changed CPU only; this one is the separate memory question it named as out of scope.