Java analysis went from 18 to 60+ minutes. JVM spends half its time in GC

Our main-branch analysis ran in ~18–20 minutes through May and June, had drifted to ~26 by mid-August, and since 18 August takes anywhere between 25 and 67 minutes — on the same code, same heap and same quality profile. Six runs on one day gave JavaSensor times of 16.7, 30.1, 31.5, 31.9, 40.1 and 49.3 minutes.

We ran one analysis with GC logging on the scanner engine, and it looks like the JVM is simply out of room.

ALM used: GitHub
CI system used: GitHub Actions, `ubuntu-latest` (4 vCPU / 16 GB), Zulu JDK 21
Scanner command used:

mvn org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \
-Dsonar.projectKey=<masked> \
-Dsonar.organization=<masked> \
-Dsonar.host.url=https://sonarcloud.io \
-Dsonar.coverage.jacoco.xmlReportPaths=target/coverage-report/jacoco.xml \
-Dsonar.java.binaries=target/classes \
-Dsonar.exclusions=target/generated-sources/** \
-Dsonar.internal.analysis.dbd=false

with SONAR_SCANNER_JAVA_OPTS=-Xmx8g. We don’t pin the plugin version, but it resolved to
sonar-maven-plugin:5.7.0.6970 both before and after this started, so the plugin itself didn’t change on our end.

Languages of the repository: Java — ~700k ncloc, ~11k main + ~3.8k test files, single Maven module.
Project URL: private.
Error observed: no error as such since we disabled DBD, just the runtime. GC log summary from the 1:06:59 run (JavaSensor 53.3 min of it):

heap max 8192 MB
collections 689 young, 45 full
total GC pause 34.8 min = 54.9% of wall time
of which full GC 30.7 min
longest single pause 90.3 s
live set after FULL GC 7731 MB median = 94% of heap
reclaimed per full GC 454 MB median

Full collections free about 450 MB and put the heap straight back to ~7730 MB — the last twelve all landed within 30 MB of each other.

Until we disabled DBD, `javabugs` OOMed on every main-branch run instead:

[INFO] Sensor javabugs [dbd]
[ERROR] Error during SonarScanner Engine execution
java.lang.OutOfMemoryError: Java heap space


        at com.sonarsource.dbd.platform.internal.B.G$_A.<init>(na:1803)
        at com.sonarsource.dbd.platform.internal.B.D.A(na:3476)
        at com.sonarsource.dbd.G.doExecute(na:2150)
        at org.sonar.scanner.sensor.AbstractSensorWrapper.analyse(AbstractSensorWrapper.java:67)

Steps to reproduce: run the command above against the main (long-lived) branch on a 16 GB runner. Add `-Xlog:gc*:file=/tmp/gc.log:time,uptime,level,tags` to `SONAR_SCANNER_JAVA_OPTS` to see the collection behaviour. Ours is a full analysis every time. The log says `The Java analyzer cannot skip unchanged files in this context` and `Server-side caching is enabled. The Java analyzer will not try to leverage data from a previous analysis.`

One thing that may help narrow it down: between our last fast run (17 Aug) and the first slow one (19 Aug), the main-source ECJ pass went 9.0 → 12.2 min, but the test-source pass went 1.6 → 14.5 min, for 10 extra test files. On 17 Aug it logged `Starting batch processing` straight through to `100% analyzed`; on 19 Aug it crawls out 1%, 3%, 5%, 6%, 9% over a quarter of an hour. Both use ECJ batch size 429 KB. The server-side sensor cache also went from 106 MB to 222 MB across those two days.

Potential workaround: `-Dsonar.internal.analysis.dbd=false`, from [this thread]( Sensor javabugs [dbd] takes 5 hours ), stops the OOM. It didn’t help the runtime, though — analysis is slower now than before we set it, which fits DBD not being the thing under memory pressure.

Raising the heap isn’t an option either. At `-Xmx11g` the analysis never even reaches DBD: the JVM grows during JavaSensor’s ECJ parse until the 16 GB runner runs out of memory and the CI agent is killed at 47% analyzed, with no stack trace. 6g and 8g thrash. We haven’t found a usable value in between.

So, two questions:

  1. Is a ~7.7 GB live set expected for JavaSensor on a project this size? If it is, “allocate a larger heap” isn’t really actionable on GitHub-hosted runners, since 16 GB can’t host it — is there a heap-per-ncloc figure, or a runner size, you’d recommend?
  2. Is the jump in the test-source pass on 18 August expected?

Thank you.

Hi @Yura_Kalemi. Welcome to the community.

  1. *Is a ~7.7 GB live set expected for JavaSensor on a project this size? If it is, “allocate a larger heap” isn’t really actionable on GitHub-hosted runners, since 16 GB can’t host it — is there a heap-per-ncloc figure, or a runner size, you’d recommend?

~700K ncloc in ~15K files is a large project, but 8GB should be fine.

Six runs on one day gave JavaSensor times of 16.7, 30.1, 31.5, 31.9, 40.1 and 49.3 minutes.

Are all of the following the same for each of these six runs?

  1. project
  2. branch
  3. code
  4. analysis parameters
  5. quality profile

If so, this indicates that the something on the self-hosted runner external to SonarScanner is impacting the scanner’s performance.

  1. Is the runner host machine hosting any other applications?
  2. Does the runner host do multiple pipeline runs in parallel?

To provide me information, I would need to see scanner logs – DEBUG-level, preferably.

Hi Austin,
Thanks for looking at this.

One thing first: these aren’t self-hosted runners. We’re on GitHub-hosted ubuntu-latest. We have no self-hosted runners registered on the repo at all.

  • Is the runner host hosting any other applications? No — single-use VM, nothing on it but this job.

  • Does the runner host do multiple pipeline runs in parallel? No — one job per VM.

  • project - same

  • branch — same (master)

  • analysis parameters — same

  • quality profile — same

  • code — not truly identical. They’re six consecutive pushes to master. Across the whole span, 90 of 14,919 Java files changed (0.6%). Nothing structural.

The scanner DEBUG log is large and the project is private, what’s the best way to get it to you?

Hi Austin,

Happy to share the logs. Could you say how I can do that privately?