Skip to content

build(ci): bound concurrent JVMs on the macOS runner - #16167

Open
jamesfredley wants to merge 2 commits into
8.0.xfrom
build/ci-daemon-heap-vs-runner-ram
Open

build(ci): bound concurrent JVMs on the macOS runner#16167
jamesfredley wants to merge 2 commits into
8.0.xfrom
build/ci-daemon-heap-vs-runner-ram

Conversation

@jamesfredley

Copy link
Copy Markdown
Contributor

What

Make the CI memory budget explicit, and stop the macOS runner from being over-committed.

org.gradle.jvmargs sizes the Gradle daemon only. Test forks are separate child JVMs and take their heap from maxHeapSize in gradle/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:

daemon -Xmx  +  (concurrent test forks x per-fork maxHeapSize)  +  OS

The number of concurrent forks is not maxParallelForks. With org.gradle.parallel=true several Test tasks 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:

Runner CPU RAM Default workers Worst-case peak
ubuntu-latest / windows-latest 4 16 GB 4 5G + 4x768m = 8.0 GB
macos-latest (M1) 3 7 GB 3 5G + 3x768m = 7.25 GB

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 new runner_arguments matrix key that is empty for every other entry. Capping --max-workers is the part that actually bounds concurrent forks; -PmaxTestParallel alone would not, for the reason above. New peak: 5G + 2x768m = 6.5 GB, which fits in 7 GB.
  • gradle.properties: document the arithmetic next to org.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: groovydoc is 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

  • No test is added, removed, skipped, or weakened. This changes concurrency only, not which tests run.
  • No other runner changes behaviour: runner_arguments is undefined (empty) for the Ubuntu and Windows entries.
  • grails-gradle and grails-forge are 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.

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

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 52.3344%. Comparing base (89e993c) to head (728f00f).
⚠️ Report is 1 commits behind head on 8.0.x.

Additional details and impacted files

Impacted file tree graph

@@              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:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jamesfredley
jamesfredley marked this pull request as ready for review August 19, 2026 19:32
Copilot AI lite review requested due to automatic review settings August 19, 2026 19:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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=2 and -PmaxTestParallel=2) via a new runner_arguments matrix 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.

Comment on lines 142 to 157
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 }}
@testlens-app

testlens-app Bot commented Aug 19, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 728f00f
▶️ Tests: 60112 executed
⚪️ Checks: 79/79 completed


Learn more about TestLens at testlens.app/docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants