Skip to content

Size the daemon heaps from measurement, and add the script that measures them - #157

Merged
simtop merged 1 commit into
masterfrom
refactor/jacoco-root-report-and-build-tuning
Aug 9, 2026
Merged

Size the daemon heaps from measurement, and add the script that measures them#157
simtop merged 1 commit into
masterfrom
refactor/jacoco-root-report-and-build-tuning

Conversation

@simtop

@simtop simtop commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Follow-up to #156, which set the daemon heaps by reasoning rather than measurement. This measures them, and adds the script that did it.

The measurement

scripts/measure-jvm-memory.sh 2g 3g 4g against assembleDebug on a 16 GB machine with the usual emulator and browser open:

heap wall live set GC overhead swapped
2g (before #156) 39.3s 1621 MB 38.1% 955 MB
3g 33.3s 1608 MB 24.9% 40 MB
4g (what #156 shipped) 36.3s 1599 MB 23.7% 326 MB

Two things that settles.

Raising it from 2g was right. The live set is ~1.6 GB, so -Xmx2048m was running at 1.26x — under the 1.5–2x a collector needs to stay out of the way — and paying 38% of the build in GC.

But 4g was wrong. It wins marginally on GC and loses on everything else: it swaps 8x more and finishes 3s slower. The extra gigabyte comes out of the same 16 GB the emulator and browser are using. More heap stops being more speed once the machine, not the JVM, is the constraint — which is exactly what the usual "4g on a 16 GB box" advice misses, because it assumes the build is the only thing running.

The script is the actual deliverable

The numbers above are a property of this machine, not of the project, so they should not be copied anywhere. scripts/measure-jvm-memory.sh sweeps candidate heaps and reports the three things that decide the answer:

  • live set — max heap still in use after a GC. The floor; below it the collector thrashes.
  • GC overhead — share of wall time in GC pauses. Compare candidates to each other, not to an absolute: the measured build is a full recompile, so the percentages are a worst case.
  • swap delta — the ceiling, and the number the usual advice ignores. A JVM can look healthy while the machine pages around it.

It prints the exact properties to paste. Run it on new hardware instead of copying these values.

Two traps it had to handle, both of the "measurement silently returns nothing" kind:

  • The GC log must be read from a byte offset, not deleted between the warm-up and measured runs. The daemon holds the file open, so deleting it leaves it writing to an unlinked inode and every GC figure reads as zero — which is exactly what the first version reported.
  • Each candidate needs its own daemon (./gradlew --stop first), or it measures the previous heap setting.

On the Kotlin daemon

kotlin.daemon.jvmargs was previously unset entirely, so the JVM that every module's Kotlin compilation runs through took whatever default the plugin chose. It is now set explicitly, and setting it explicitly is the point — the specific 1536m is not yet measured, and the comment in gradle.properties says so. A separate sweep for it is in progress; the first attempt produced numbers I do not trust, so it is deliberately not being asserted here.

Verification

make build-budget re-run cleanly on the final settings (the run made during #156 was contaminated — gradle.properties was edited inside its measurement window):

clean_build_cold:  17.7s <= 120s
clean_build_warm:   3.3s <=  20s
incremental_deep:   1.6s <=  10s
incremental_leaf:   1.6s <=  10s
unit_tests:         3.6s <=  20s
deep/leaf ratio:   0.99x <= 3.00x

All budgets met. The cold figure is well under the 37s recorded in ADR 0011, but its 115% spread is too wide to claim that as an improvement rather than noise; the stable result is the 0.99x deep/leaf ratio, unchanged.

…t measures them

The earlier 4g was reasoning from a reference machine, not evidence, and it was
wrong for a 16 GB laptop that also runs an emulator and a browser. Measured
with scripts/measure-jvm-memory.sh against assembleDebug:

  heap   wall     live set   GC overhead   swapped
  2g     39.3s    1621 MB    38.1%         955 MB
  3g     33.3s    1608 MB    24.9%          40 MB   <- chosen
  4g     36.3s    1599 MB    23.7%         326 MB

The old 2048m sat at 1.26x a ~1.6 GB live set and spent 38% of the build in
GC, so raising it was right. But 4g is slower than 3g on this machine: it wins
marginally on GC and loses on swap, because the extra heap comes out of the
same 16 GB the emulator is using.

The script is the point rather than the numbers, which are a property of the
machine and not of the project. It sweeps candidate heaps and reports the three
things that actually decide the answer - live set (the floor), GC overhead, and
swap delta (the ceiling the usual advice ignores) - then prints the properties
to paste. Run it again on different hardware instead of copying these values.

Two traps it had to handle: the GC log must be read from a byte offset rather
than deleted between the warm-up and measured runs, because the daemon holds it
open and deleting it makes every GC figure read as zero; and each candidate
needs its own daemon or it measures the previous heap.
@simtop
simtop force-pushed the refactor/jacoco-root-report-and-build-tuning branch from dc509f8 to 11621ac Compare August 9, 2026 13:20
@simtop
simtop enabled auto-merge (squash) August 9, 2026 13:21
@simtop
simtop merged commit 8034c9f into master Aug 9, 2026
12 checks passed
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.

1 participant