Oversubscribe test forks in CI to cut wall clock time - #1217
Conversation
`:test` is 83% of CI wall clock (8m09s of a 9m47s build). The shared `RewriteJavaPlugin` sets `maxParallelForks = availableProcessors()`, which is 4 on `ubuntu-latest`. Those 4 forks finished at 3m47s, 4m55s, 7m27s and 8m07s -- 24m16s of executor time that would take 6m04s if evenly packed, so roughly two minutes went to imbalance alone. Gradle hands out whole test classes, and 47 of the 267 test classes resolve Maven poms over the network, so a couple of long latency-bound classes landed late on two forks with no work left to steal. Running 1.5 forks per core gives finer granularity and lets the CPU-bound classes fill the gaps while the network-bound ones block. Heap drops to 1500m so the extra forks still fit: 6 x 1500m is ~9g of a 16g runner, where 6 x 2g would not have been comfortable. GitHub scales runner RAM at 4g per core, so 1.5 forks/core at 1500m stays within budget on larger runners too. Local dev behaviour is unchanged -- the plugin's `availableProcessors() / 2` and the 2g heap still apply when `CI` is unset.
|
Marking draft — the first CI run showed this change is a no-op on parallelism, and I want to fix it properly before this merges.
|
| baseline | this run | |
|---|---|---|
| executor finish times | 3m47, 4m55, 7m27, 8m07 | 4m42, 4m42, 5m23, 10m07 |
| total executor time | 24m16s | 24m54s |
:test wall clock |
8m09s | 10m07s |
Same total work and same 4 forks, but the tail moved from 8m07 to 10m07 purely on which fork drew the slow classes. Run-to-run variance is ±2 min, which is the same size as the effect I was trying to measure — so any future attempt here needs several runs, not one.
Where that leaves it
Oversubscribing forks requires also raising org.gradle.workers.max, and the only place this repo can set that is gradle.properties, which would apply to local dev too. Meanwhile the 8-core runner raises availableProcessors() and so lifts both limits together, with no build-script change at all — that now looks clearly like the better lever.
|
Closing — this change cannot do what it set out to do.
The CI run confirmed it: still 4 executors, unchanged from baseline. Raising the core count lifts both limits at once and needs no build-script change, so the fix belongs in the shared workflow instead: openrewrite/gh-automation#111 adds a Two findings from the investigation worth keeping:
|
Why
:testdominates CI. On the most recentmainbuild (9m47s total)::compileJava:jar/:javadoc/:sourcesJar:recipeCsvValidate:compileTestJava:testEliminating every task except
:testwould save only ~1.5 min, so:testis the only phase worth attacking.The problem
RewriteJavaPluginsetsmaxParallelForks = availableProcessors(), andubuntu-latestis 4 vCPU, so 4 forks. Their finish times:That's 24m16s of executor time. Evenly packed across 4 forks it would be 6m04s, so roughly two minutes is fork imbalance alone.
Gradle hands out whole test classes, and 47 of the 267 test classes resolve Maven poms over the network. A couple of long, latency-bound classes landed late on two forks, and there was no work left for the other two to steal. There's no single slow test to fix — 1778 tests at ~1.1s of executor time each, largest inter-test gap 41s and tapering smoothly.
The change
Run 1.5 forks per core in CI. Finer granularity packs better, and the CPU-bound classes can use the cores that the network-bound classes are blocking on.
Heap drops to 1500m so the extra forks fit: 6 x 1500m is ~9g of a 16g runner, where 6 x 2g would have been tight. GitHub scales runner RAM at 4g per core, so 1.5 forks/core at 1500m stays in budget on larger runners too.
Local dev is untouched — the plugin's
availableProcessors() / 2and the 2g heap still apply whenCIis unset. Verified both paths resolve as intended.Not done here
Moving to an 8-core runner is the bigger lever (
:testto ~4 min, total ~5m30s), butruns-on: ubuntu-latestis hardcoded inopenrewrite/gh-automation/.github/workflows/ci-gradle.ymland needs an input added there. Happy to follow up if we want it.Sharding across matrix jobs isn't worth it — each shard re-pays ~1m40s of setup and compile, and since shards start simultaneously they all miss the remote build cache, landing around 4 min for far more complexity.