Skip to content

Commit d91dfbe

Browse files
ADFA-6381: Build one Kotlin library module per jar
collectKtModules held the boot classpath as a lazy Sequence that called addLibrary, so every source module re-ran it and got a fresh KtLibraryModule for android.jar from each Android module: modules x Android-modules copies. Each copy materializes the jar's full file list for its search scope. GlitchTip breadcrumbs show the same android.jar scope built 55-71 times on large multi-module projects, behind 219 of 336 OOM events. addLibrary now reuses the module for a path, and the boot classpath is a distinct List, so each jar maps to exactly one module.
1 parent bb9efab commit d91dfbe

2 files changed

Lines changed: 77 additions & 14 deletions

File tree

‎lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/WorkspaceExts.kt‎

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,24 +30,25 @@ internal fun Workspace.collectKtModules(
3030

3131
val jarToModMap = mutableMapOf<Path, KtLibraryModule>()
3232

33-
fun addLibrary(path: Path): KtLibraryModule {
34-
val module = buildKtLibraryModule(project, appEnv) {
35-
id = path.pathString
36-
addContentRoot(path)
33+
// One module per jar: every module shares android.jar, and each KtLibraryModule
34+
// materializes the jar's full file list for its search scope (ADFA-6381).
35+
fun addLibrary(path: Path): KtLibraryModule =
36+
jarToModMap.getOrPut(path) {
37+
buildKtLibraryModule(project, appEnv) {
38+
id = path.pathString
39+
addContentRoot(path)
40+
}
3741
}
38-
jarToModMap[path] = module
39-
return module
40-
}
4142

43+
// A List, not a Sequence: a Sequence would re-run addLibrary for every source module below.
4244
val bootClassPaths = moduleProjects
4345
.filterIsInstance<AndroidModule>()
44-
.flatMap { project ->
45-
project.bootClassPaths
46-
.asSequence()
47-
.filter { it.exists() }
48-
.map { it.toPath() }
49-
.map(::addLibrary)
50-
}
46+
.flatMap { it.bootClassPaths.asSequence() }
47+
.filter { it.exists() }
48+
.map { it.toPath() }
49+
.distinct()
50+
.map(::addLibrary)
51+
.toList()
5152

5253
val libraryDependencies = moduleProjects
5354
.flatMap { it.getCompileClasspaths() }
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.itsaky.androidide.lsp.kotlin.compiler
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtLibraryModule
5+
import com.itsaky.androidide.lsp.kotlin.fixtures.KtLspTest
6+
import com.itsaky.androidide.project.AndroidModels
7+
import com.itsaky.androidide.project.GradleModels
8+
import com.itsaky.androidide.projects.api.AndroidModule
9+
import com.itsaky.androidide.projects.api.GradleProject
10+
import com.itsaky.androidide.projects.api.Workspace
11+
import org.junit.Test
12+
import kotlin.io.path.createFile
13+
import kotlin.io.path.pathString
14+
15+
class CollectKtModulesTest : KtLspTest() {
16+
@Test
17+
fun `modules sharing android jar share one library module`() {
18+
val androidJar = lspTestRule.tempDir.root.toPath().resolve("android.jar").createFile()
19+
val modulePaths = listOf(":a", ":b", ":c")
20+
val workspace =
21+
Workspace(
22+
rootProject = GradleProject(gradleProject(":")),
23+
subProjects = modulePaths.map { AndroidModule(gradleProject(it, androidJar.pathString)) },
24+
syncIssues = emptyList(),
25+
)
26+
27+
val sourceModules = workspace.collectKtModules(env.project, env.applicationEnv)
28+
29+
val androidJarDeps =
30+
sourceModules.map { module ->
31+
module.directRegularDependencies.filterIsInstance<KtLibraryModule>().filter { it.id == androidJar.pathString }
32+
}
33+
assertThat(sourceModules).hasSize(modulePaths.size)
34+
// Before ADFA-6381 each source module depended on one fresh copy per Android module (3 x 3 here).
35+
androidJarDeps.forEach { assertThat(it).hasSize(1) }
36+
androidJarDeps.flatten().forEach { assertThat(it).isSameInstanceAs(androidJarDeps.first().first()) }
37+
}
38+
39+
private fun gradleProject(
40+
path: String,
41+
bootClassPath: String? = null,
42+
): GradleModels.GradleProject {
43+
val dir = "/tmp/collect-kt-modules${path.replace(':', '/')}"
44+
return GradleModels.GradleProject
45+
.newBuilder()
46+
.setName(path.trimStart(':'))
47+
.setPath(path)
48+
.setProjectDirPath(dir)
49+
.setBuildDirPath("$dir/build")
50+
.setBuildScriptPath("$dir/build.gradle")
51+
.apply {
52+
if (bootClassPath != null) {
53+
setAndroidProject(
54+
AndroidModels.AndroidProject
55+
.newBuilder()
56+
.setProjectType(AndroidModels.ProjectType.LibraryProject)
57+
.addBootClassPaths(bootClassPath),
58+
)
59+
}
60+
}.build()
61+
}
62+
}

0 commit comments

Comments
 (0)