Skip to content

Kotlin: release KotlinCoreEnvironment on every parse path#7705

Merged
jkschneider merged 1 commit into
mainfrom
kotlin-template-parser-disposable-leak
May 17, 2026
Merged

Kotlin: release KotlinCoreEnvironment on every parse path#7705
jkschneider merged 1 commit into
mainfrom
kotlin-template-parser-disposable-leak

Conversation

@jkschneider
Copy link
Copy Markdown
Member

Summary

KotlinParser.parseInputs deferred Disposer.dispose(disposable) to a tail Stream.generate(...).limit(1) element, so the disposable only ran when the caller iterated past every parsed source. JavaTemplateParser's template-stub path consumes the parser stream via .findFirst(), which short-circuits before the dispose element is reached, leaking one KotlinCoreEnvironment per KotlinTemplate.apply() call. Each leaked environment retains the full Kotlin/JDK classpath ProtoBuf metadata (~25MB on a typical recipe-module classpath), so heavy template-driven suites OOM well below their nominal heap ceiling.

Fix

Materialize all source files inside try { … } finally { Disposer.dispose(…); } and return a stream over the materialized list. The existing parse(…) already built every CompiledSource eagerly before the stream produced any element, so the change loses no streaming property — it just makes the cleanup unconditional.

Diagnostic evidence

Captured via jcmd GC.class_histogram snapshots while a downstream Kotlin-recipe suite (189 tests, ~200 template applies) ran at -Xmx2g:

t KotlinCoreEnvironment live ProtoBuf$Type live total heap
+0s 61 1.70M 1.83GB
+15s 71 2.00M 2.14GB (Full-GC churn — OOM)

71 × ~25MB ≈ 1.8GB, matching the heap at OOM. With the fix, the same suite passes at the Gradle default -Xmx512m in ~30s.

Test plan

  • ./gradlew :rewrite-kotlin:test — 1205 / 1205 green
  • Downstream recipes-kotlin (Kotlin1To2Test, 189 tests) passes at default heap (was OOMing at 6g)

`KotlinParser.parseInputs` deferred `Disposer.dispose(disposable)` to a
tail `Stream.generate(...).limit(1)` element, so the disposable only ran
when the caller iterated past every parsed source. `JavaTemplateParser`'s
template-stub path consumes the parser stream via `.findFirst()`, which
short-circuits before the dispose element is reached, leaking one
`KotlinCoreEnvironment` per `KotlinTemplate.apply()` call. Each leaked
environment retains the full Kotlin/JDK classpath `ProtoBuf` metadata
(~25MB on a typical recipe-module classpath), so heavy template-driven
suites OOM well below their nominal heap ceiling.

Materialize all source files inside `try { … } finally { Disposer.dispose(…); }`
and return a stream over the materialized list. The existing `parse(…)`
already built every `CompiledSource` eagerly before the stream produced
any element, so the change loses no streaming property — it just makes
the cleanup unconditional.

Measured on a downstream Kotlin-recipe test suite (189 tests, ~200
recipe-driven template applies): pre-fix the suite OOM'd at `-Xmx6g`;
post-fix it passes at the Gradle default `-Xmx512m` in ~30s.
@github-project-automation github-project-automation Bot moved this to In Progress in OpenRewrite May 17, 2026
@jkschneider jkschneider merged commit 1189bbc into main May 17, 2026
1 check failed
@jkschneider jkschneider deleted the kotlin-template-parser-disposable-leak branch May 17, 2026 15:50
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite May 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant