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:
committed by
Space
parent
972cd9e9e7
commit
de76aeb9d1
+8
-7
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user