Optimize SdkInfo lookup for a huge monorepo with no SdkInfo

DFS of SdkInfo on moduleInfo and reuse cached libraryInfo dependencies
fix traverse order

#KT-47065 Fixed
Relates to #KT-46622
This commit is contained in:
Vladimir Dolzhenko
2021-06-09 08:31:46 +00:00
committed by Space
parent 972cd9e9e7
commit de76aeb9d1
3 changed files with 110 additions and 18 deletions
@@ -6,6 +6,7 @@
package org.jetbrains.kotlin.idea.caches.project package org.jetbrains.kotlin.idea.caches.project
import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.OrderRootType import com.intellij.openapi.roots.OrderRootType
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
@@ -112,16 +113,16 @@ class KotlinStdlibCacheImpl(val project: Project) : KotlinStdlibCache {
// bfs // bfs
while (stack.isNotEmpty()) { while (stack.isNotEmpty()) {
ProgressManager.checkCanceled()
val poll = stack.poll() val poll = stack.poll()
if (!checkedLibraryInfo.add(poll)) continue if (!checkedLibraryInfo.add(poll)) continue
val dependencies = poll.dependencies().filter { !checkedLibraryInfo.contains(it) } stack += poll.dependencies().also { dependencies ->
dependencies dependencies
.filterIsInstance<LibraryInfo>() .firstOrNull { it is LibraryInfo && isStdlib(it) }
.firstOrNull { isStdlib(it) } ?.let { return@run it as LibraryInfo }
?.let { return@run it } }
stack += dependencies
} }
null null
} }
@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.idea.caches.project
import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.module.Module import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.* import com.intellij.openapi.roots.*
import com.intellij.openapi.roots.impl.libraries.LibraryEx import com.intellij.openapi.roots.impl.libraries.LibraryEx
@@ -94,6 +95,7 @@ class LibraryDependenciesCacheImpl(private val project: Project) : LibraryDepend
val platform = libraryInfo.platform val platform = libraryInfo.platform
for (module in getLibraryUsageIndex().getModulesLibraryIsUsedIn(libraryInfo)) { for (module in getLibraryUsageIndex().getModulesLibraryIsUsedIn(libraryInfo)) {
ProgressManager.checkCanceled()
val (moduleLibraries, moduleSdks) = moduleDependenciesCache.getOrPut(module) { val (moduleLibraries, moduleSdks) = moduleDependenciesCache.getOrPut(module) {
computeLibrariesAndSdksUsedIn(module) computeLibrariesAndSdksUsedIn(module)
} }
@@ -6,12 +6,14 @@
package org.jetbrains.kotlin.idea.caches.project package org.jetbrains.kotlin.idea.caches.project
import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.util.containers.ConcurrentWeakKeySoftValueHashMap import com.jetbrains.rd.util.concurrentMapOf
import com.intellij.util.containers.ContainerUtil
import org.jetbrains.kotlin.analyzer.ModuleInfo import org.jetbrains.kotlin.analyzer.ModuleInfo
import org.jetbrains.kotlin.caches.project.cacheInvalidatingOnRootModifications import org.jetbrains.kotlin.caches.project.cacheInvalidatingOnRootModifications
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
import java.util.ArrayDeque
/** /**
* Maintains and caches mapping ModuleInfo -> SdkInfo *form its dependencies* * Maintains and caches mapping ModuleInfo -> SdkInfo *form its dependencies*
@@ -35,18 +37,105 @@ interface SdkInfoCache {
} }
class SdkInfoCacheImpl(private val project: Project) : SdkInfoCache { class SdkInfoCacheImpl(private val project: Project) : SdkInfoCache {
private val cache: MutableMap<ModuleInfo, SdkInfo?> @JvmInline
// Note: it's ok to use thread-unsafe map, because 'doFindSdk' is pure value class SdkDependency(val sdk: SdkInfo?)
get() = project.cacheInvalidatingOnRootModifications { mutableMapOf() }
private val cache: MutableMap<ModuleInfo, SdkDependency>
get() = project.cacheInvalidatingOnRootModifications { concurrentMapOf() }
override fun findOrGetCachedSdk(moduleInfo: ModuleInfo): SdkInfo? { override fun findOrGetCachedSdk(moduleInfo: ModuleInfo): SdkInfo? {
if (!cache.containsKey(moduleInfo)) { // get an operate on the fixed instance of a cache to avoid case of roots modification in the middle of lookup
cache[moduleInfo] = doFindSdk(moduleInfo) val instance = cache
if (!instance.containsKey(moduleInfo)) {
findSdk(instance, moduleInfo)
} }
return cache[moduleInfo] return instance[moduleInfo]?.sdk
} }
private fun doFindSdk(moduleInfo: ModuleInfo): SdkInfo? = private fun findSdk(cache: MutableMap<ModuleInfo, SdkDependency>, moduleInfo: ModuleInfo) {
moduleInfo.dependencies().lazyClosure { it.dependencies() }.firstOrNull { it is SdkInfo } as SdkInfo? moduleInfo.safeAs<SdkDependency>()?.let {
cache[moduleInfo] = it
return
}
val libraryDependenciesCache = LibraryDependenciesCache.getInstance(this.project)
val visitedModuleInfos = mutableSetOf<ModuleInfo>()
// graphs is a stack of paths is used to implement DFS without recursion
// it depends on a number of libs, that could be > 10k for a huge monorepos
val graphs = ArrayDeque<List<ModuleInfo>>().also {
// initial graph item
it.add(listOf(moduleInfo))
}
val (path, sdkInfo) = run {
while (graphs.isNotEmpty()) {
ProgressManager.checkCanceled()
// graph of DFS from the root i.e from `moduleInfo`
// note: traverse of graph goes in a depth over the most left edge:
// - poll() retrieves and removes the head of the queue
// - push(element) inserts the element at the front of the deque
val graph = graphs.poll()
val last = graph.last()
// the result could be immediately returned when cache already has it
val cached = cache[last]
if (cached != null) {
cached.sdk?.let { return@run graph to cached }
continue
}
if (!visitedModuleInfos.add(last)) continue
val dependencies = run deps@{
if (last is LibraryInfo) {
// use a special case for LibraryInfo to reuse values from a library dependencies cache
val (libraries, sdks) = libraryDependenciesCache.getLibrariesAndSdksUsedWith(last)
sdks.firstOrNull()?.let {
return@run graph to SdkDependency(it)
}
libraries
} else {
last.dependencies()
.also { dependencies ->
dependencies.firstIsInstanceOrNull<SdkInfo>()?.let {
return@run graph to SdkDependency(it)
}
}
}
}
dependencies.forEach { dependency ->
val sdkDependency = cache[dependency]
if (sdkDependency != null) {
sdkDependency.sdk?.let {
// sdk is found when some dependency is already resolved
return@run (graph + dependency) to sdkDependency
}
} else {
// otherwise add a new graph of (existed graph + dependency) as candidates for DFS lookup
if (!visitedModuleInfos.contains(dependency)) {
graphs.push(graph + dependency)
}
}
}
}
return@run null to noSdkDependency
}
// when sdk is found: mark all graph elements could be resolved to the same sdk
path?.let {
it.forEach { info -> cache[info] = sdkInfo }
visitedModuleInfos.removeAll(it)
}
// mark all visited modules (apart of found path) as dead ends
visitedModuleInfos.forEach { info -> cache[info] = noSdkDependency }
}
companion object {
private val noSdkDependency = SdkDependency(null)
}
} }