[kotlin] Rework SdkInfoCache
authorVladimir Dolzhenko <vladimir.dolzhenko@jetbrains.com>
Tue, 28 Jun 2022 16:10:31 +0000 (18:10 +0200)
committerintellij-monorepo-bot <intellij-monorepo-bot-no-reply@jetbrains.com>
Tue, 28 Jun 2022 17:43:38 +0000 (17:43 +0000)
#KTIJ-21890

GitOrigin-RevId: 99f64dd30f36d388f8cbdd895e80cb6b5374eb10

plugins/kotlin/base/analysis/src/org/jetbrains/kotlin/idea/base/analysis/LibraryDependenciesCache.kt
plugins/kotlin/base/project-structure/src/org/jetbrains/kotlin/idea/base/projectStructure/KotlinStdlibCache.kt
plugins/kotlin/base/project-structure/src/org/jetbrains/kotlin/idea/base/projectStructure/SdkInfoCache.kt
plugins/kotlin/base/project-structure/src/org/jetbrains/kotlin/idea/base/projectStructure/moduleInfo/IdeaModuleInfo.kt
plugins/kotlin/base/project-structure/src/org/jetbrains/kotlin/idea/base/projectStructure/moduleInfo/LibraryInfo.kt
plugins/kotlin/base/project-structure/src/org/jetbrains/kotlin/idea/base/projectStructure/moduleInfo/ModuleSourceInfo.kt
plugins/kotlin/base/util/src/org/jetbrains/kotlin/idea/base/util/caching/FineGrainedEntityCache.kt
plugins/kotlin/plugin/resources/META-INF/caches.xml

index 0764433244106b999df99e72f2ae8687f633db6e..978cc05b921f3090646130e7b45d05e07168bb15 100644 (file)
@@ -37,8 +37,8 @@ import org.jetbrains.kotlin.idea.base.projectStructure.LibraryDependenciesCache.
 import org.jetbrains.kotlin.idea.base.projectStructure.moduleInfo.LibraryInfo
 import org.jetbrains.kotlin.idea.base.projectStructure.moduleInfo.SdkInfo
 import org.jetbrains.kotlin.idea.base.projectStructure.moduleInfo.checkValidity
+import org.jetbrains.kotlin.idea.base.util.caching.AbstractFineGrainedEntityCache.Companion.isFineGrainedCacheInvalidationEnabled
 import org.jetbrains.kotlin.idea.base.util.caching.FineGrainedEntityCache
-import org.jetbrains.kotlin.idea.base.util.caching.FineGrainedEntityCache.Companion.isFineGrainedCacheInvalidationEnabled
 import org.jetbrains.kotlin.idea.base.util.caching.WorkspaceEntityChangeListener
 import org.jetbrains.kotlin.idea.caches.project.*
 import org.jetbrains.kotlin.utils.addIfNotNull
index 5f26dc8ab4abf23798791fd02eaba45e7c9c2d63..2bbde8fd524cbd31f77aa106c6dc6b1e6c16e1b4 100644 (file)
@@ -2,6 +2,7 @@
 
 package org.jetbrains.kotlin.idea.base.projectStructure
 
+import com.intellij.ProjectTopics
 import com.intellij.openapi.Disposable
 import com.intellij.openapi.progress.ProcessCanceledException
 import com.intellij.openapi.progress.ProgressManager
@@ -170,10 +171,14 @@ internal class KotlinStdlibCacheImpl(private val project: Project) : KotlinStdli
 
         private abstract inner class AbstractCache<Key : IdeaModuleInfo> :
             FineGrainedEntityCache<Key, StdlibDependency>(project, cleanOnLowMemory = true),
-            OutdatedLibraryInfoListener {
+            OutdatedLibraryInfoListener,
+            ProjectJdkTable.Listener,
+            ModuleRootListener {
             override fun subscribe() {
                 val connection = project.messageBus.connect(this)
                 connection.subscribe(OutdatedLibraryInfoListener.TOPIC, this)
+                connection.subscribe(ProjectJdkTable.JDK_TABLE_TOPIC, this)
+                connection.subscribe(ProjectTopics.PROJECT_ROOTS, this)
                 subscribe(connection)
             }
 
@@ -199,6 +204,24 @@ internal class KotlinStdlibCacheImpl(private val project: Project) : KotlinStdli
             override fun libraryInfosRemoved(libraryInfos: Collection<LibraryInfo>) {
                 invalidateEntries({ _, v -> v.libraryInfo in libraryInfos }, validityCondition = { _, v -> v.libraryInfo != null })
             }
+
+            override fun jdkRemoved(jdk: Sdk) {
+                invalidateEntries({ k, _ -> k.safeAs<SdkInfo>()?.sdk == jdk })
+            }
+
+            override fun jdkNameChanged(jdk: Sdk, previousName: String) {
+                jdkRemoved(jdk)
+            }
+
+            override fun rootsChanged(event: ModuleRootEvent) {
+                // SDK could be changed (esp in tests) out of message bus subscription
+                val jdks = ProjectJdkTable.getInstance().allJdks.toHashSet()
+                invalidateEntries(
+                    { k, _ -> k.safeAs<SdkInfo>()?.let { it.sdk !in jdks } == true  },
+                    // unable to check entities properly: an event could be not the last
+                    validityCondition = null
+                )
+            }
         }
 
         private inner class LibraryCache : AbstractCache<LibraryInfo>() {
@@ -221,24 +244,12 @@ internal class KotlinStdlibCacheImpl(private val project: Project) : KotlinStdli
             }
         }
 
-        private inner class SdkCache : AbstractCache<SdkInfo>(),
-                                       ProjectJdkTable.Listener {
-            override fun subscribe(connection: MessageBusConnection) {
-                connection.subscribe(ProjectJdkTable.JDK_TABLE_TOPIC, this)
-            }
-
+        private inner class SdkCache : AbstractCache<SdkInfo>() {
             override fun calculate(key: SdkInfo): StdlibDependency =
                 key.findStdLib().toStdlibDependency()
 
             override fun checkKeyValidity(key: SdkInfo) = Unit
 
-            override fun jdkRemoved(jdk: Sdk) {
-                invalidateEntries({ k, _ -> k.sdk == jdk })
-            }
-
-            override fun jdkNameChanged(jdk: Sdk, previousName: String) {
-                jdkRemoved(jdk)
-            }
         }
 
         private inner class ModuleCache : AbstractCache<IdeaModuleInfo>(), WorkspaceModelChangeListener {
@@ -285,12 +296,12 @@ internal class KotlinStdlibCacheImpl(private val project: Project) : KotlinStdli
             }
 
             override fun checkKeyValidity(key: IdeaModuleInfo) {
-                key.safeAs<ModuleSourceInfo>()?.module?.checkValidity()
+                key.checkValidity()
             }
 
             override fun changed(event: VersionedStorageChange) {
                 event.getChanges(ModuleEntity::class.java).ifEmpty { return }
-                invalidate()
+                invalidateEntries({ k, _ -> k !is LibraryInfo && k !is SdkInfo }, validityCondition = null)
             }
         }
     }
index a8ab22dfb75a7ae68a035597e2db57c138486954..d68e8a8b105de5cb2b5f2dbec4b0e82836394cee 100644 (file)
@@ -2,14 +2,22 @@
 
 package org.jetbrains.kotlin.idea.base.projectStructure
 
+import com.intellij.ProjectTopics
+import com.intellij.openapi.Disposable
 import com.intellij.openapi.components.service
 import com.intellij.openapi.progress.ProgressManager
 import com.intellij.openapi.project.Project
-import com.jetbrains.rd.util.concurrentMapOf
+import com.intellij.openapi.projectRoots.ProjectJdkTable
+import com.intellij.openapi.projectRoots.Sdk
+import com.intellij.openapi.roots.ModuleRootEvent
+import com.intellij.openapi.roots.ModuleRootListener
+import com.intellij.workspaceModel.ide.WorkspaceModelChangeListener
+import com.intellij.workspaceModel.ide.WorkspaceModelTopics
+import com.intellij.workspaceModel.storage.VersionedStorageChange
+import com.intellij.workspaceModel.storage.bridgeEntities.api.ModuleEntity
 import org.jetbrains.kotlin.analyzer.ModuleInfo
-import org.jetbrains.kotlin.caches.project.cacheInvalidatingOnRootModifications
-import org.jetbrains.kotlin.idea.base.projectStructure.moduleInfo.LibraryInfo
-import org.jetbrains.kotlin.idea.base.projectStructure.moduleInfo.SdkInfo
+import org.jetbrains.kotlin.idea.base.projectStructure.moduleInfo.*
+import org.jetbrains.kotlin.idea.base.util.caching.LockFreeFineGrainedEntityCache
 import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
 import org.jetbrains.kotlin.utils.addToStdlib.safeAs
 import java.util.ArrayDeque
@@ -34,28 +42,82 @@ interface SdkInfoCache {
     }
 }
 
-internal class SdkInfoCacheImpl(private val project: Project) : SdkInfoCache {
+internal class SdkInfoCacheImpl(project: Project) :
+    SdkInfoCache,
+    LockFreeFineGrainedEntityCache<ModuleInfo, SdkInfoCacheImpl.SdkDependency>(project, true),
+    ProjectJdkTable.Listener,
+    ModuleRootListener,
+    OutdatedLibraryInfoListener,
+    WorkspaceModelChangeListener {
+
     @JvmInline
     value class SdkDependency(val sdk: SdkInfo?)
 
-    private val cache: MutableMap<ModuleInfo, SdkDependency>
-        get() = project.cacheInvalidatingOnRootModifications { concurrentMapOf() }
+    override fun subscribe() {
+        val connection = project.messageBus.connect(this)
+        connection.subscribe(OutdatedLibraryInfoListener.TOPIC, this)
+        connection.subscribe(ProjectJdkTable.JDK_TABLE_TOPIC, this)
+        connection.subscribe(ProjectTopics.PROJECT_ROOTS, this)
+        WorkspaceModelTopics.getInstance(project).subscribeImmediately(connection, this)
+    }
+
+    override fun changed(event: VersionedStorageChange) {
+        event.getChanges(ModuleEntity::class.java).ifEmpty { return }
+        invalidateEntries(
+            { k, _ ->
+                k !is LibraryInfo && k !is SdkInfo
+            },
+            validityCondition = null
+        )
+    }
+
+    override fun libraryInfosRemoved(libraryInfos: Collection<LibraryInfo>) {
+        useCache { instance ->
+            libraryInfos.forEach { instance.remove(it) }
+        }
+    }
+
+    override fun jdkRemoved(jdk: Sdk) {
+        useCache { instance ->
+            val iterator = instance.entries.iterator()
+            while (iterator.hasNext()) {
+                val (key, value) = iterator.next()
+                if (key.safeAs<SdkInfo>()?.sdk == jdk || value.sdk?.sdk == jdk) {
+                    iterator.remove()
+                }
+            }
+        }
+    }
 
-    override fun findOrGetCachedSdk(moduleInfo: ModuleInfo): SdkInfo? {
-        // get an operate on the fixed instance of a cache to avoid case of roots modification in the middle of lookup
-        val instance = cache
+    override fun jdkNameChanged(jdk: Sdk, previousName: String) {
+        jdkRemoved(jdk)
+    }
 
-        if (!instance.containsKey(moduleInfo)) {
-            findSdk(instance, moduleInfo)
+    override fun rootsChanged(event: ModuleRootEvent) {
+        // SDK could be changed (esp in tests) out of message bus subscription
+        val jdks = ProjectJdkTable.getInstance().allJdks.toHashSet()
+        useCache { instance ->
+            val iterator = instance.entries.iterator()
+            while (iterator.hasNext()) {
+                val (key, value) = iterator.next()
+                if (key.safeAs<SdkInfo>()?.sdk?.let { it !in jdks } == true || value.sdk?.sdk?.let { it !in jdks } == true) {
+                    iterator.remove()
+                }
+            }
         }
+    }
 
-        return instance[moduleInfo]?.sdk
+    override fun checkKeyValidity(key: ModuleInfo) {
+        key.safeAs<IdeaModuleInfo>()?.checkValidity()
     }
 
-    private fun findSdk(cache: MutableMap<ModuleInfo, SdkDependency>, moduleInfo: ModuleInfo) {
-        moduleInfo.safeAs<SdkDependency>()?.let {
-            cache[moduleInfo] = it
-            return
+    override fun findOrGetCachedSdk(moduleInfo: ModuleInfo): SdkInfo? = get(moduleInfo).sdk
+
+    override fun calculate(cache: MutableMap<ModuleInfo, SdkDependency>, key: ModuleInfo): SdkDependency {
+        key.safeAs<SdkInfo>()?.let {
+            val sdkDependency = SdkDependency(it)
+            cache[key] = sdkDependency
+            return sdkDependency
         }
 
         val libraryDependenciesCache = LibraryDependenciesCache.getInstance(this.project)
@@ -65,7 +127,7 @@ internal class SdkInfoCacheImpl(private val project: Project) : SdkInfoCache {
         // 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))
+            it.add(listOf(key))
         }
 
         val (path, sdkInfo) = run {
@@ -131,6 +193,8 @@ internal class SdkInfoCacheImpl(private val project: Project) : SdkInfoCache {
         }
         // mark all visited modules (apart from found path) as dead ends
         visitedModuleInfos.forEach { info -> cache[info] = noSdkDependency }
+
+        return cache[key] ?: noSdkDependency
     }
 
     companion object {
index a9253c6ad93739208c5627c87071edc8695a86cd..4245209f5ad449bf54cd9701c33466bbb14ca4b3 100644 (file)
@@ -32,15 +32,11 @@ interface IdeaModuleInfo : ModuleInfo {
         get() = super.capabilities + mapOf(OriginCapability to moduleOrigin)
 
     override fun dependencies(): List<IdeaModuleInfo>
+
+    fun checkValidity() {}
 }
 
 interface LanguageSettingsOwner {
     val languageVersionSettings: LanguageVersionSettings
     val targetPlatformVersion: TargetPlatformVersion
 }
-
-fun Module.checkValidity() {
-    if (isDisposed) {
-        throw AlreadyDisposedException("Module '${name}' is already disposed")
-    }
-}
\ No newline at end of file
index ad6c9bfc51521a14666937893d563a6ab8b75fa1..52da1445b4e4d4b235cfc00edccf27e963ef3730 100644 (file)
@@ -72,7 +72,7 @@ abstract class LibraryInfo(
     internal val isDisposed
         get() = if (library is LibraryEx) library.isDisposed else false
 
-    fun checkValidity() {
+    override fun checkValidity() {
         if (isDisposed) {
             throw AlreadyDisposedException("Library '${name}' is already disposed")
         }
index 7db3774cb81bda535546e1650b0ad38958fb3936..71aa71e62ad1e4f180dec0f105a25499e1fb4252 100644 (file)
@@ -4,6 +4,7 @@ package org.jetbrains.kotlin.idea.base.projectStructure.moduleInfo
 import com.intellij.openapi.module.Module
 import com.intellij.openapi.project.Project
 import com.intellij.openapi.util.ModificationTracker
+import com.intellij.serviceContainer.AlreadyDisposedException
 import org.jetbrains.kotlin.analyzer.ModuleSourceInfoBase
 import org.jetbrains.kotlin.analyzer.TrackableModuleInfo
 import org.jetbrains.kotlin.idea.base.facet.platform.platform
@@ -44,4 +45,14 @@ interface ModuleSourceInfo : OldModuleSourceInfo, IdeaModuleInfo, TrackableModul
     override fun createModificationTracker(): ModificationTracker {
         return KotlinModificationTrackerProvider.getInstance(module.project).createModuleModificationTracker(module)
     }
+
+    override fun checkValidity() {
+        module.checkValidity()
+    }
+}
+
+fun Module.checkValidity() {
+    if (isDisposed) {
+        throw AlreadyDisposedException("Module '${name}' is already disposed")
+    }
 }
\ No newline at end of file
index 2d8ab7d64e4cdea9f4e0641cb179329d72e0a4ea..04238c083a46dae88a47f1e59c2bab7a2b0e04a5 100644 (file)
@@ -9,19 +9,17 @@ import com.intellij.openapi.util.LowMemoryWatcher
 import com.intellij.openapi.util.registry.Registry
 import org.jetbrains.kotlin.caches.project.cacheByClassInvalidatingOnRootModifications
 import org.jetbrains.kotlin.utils.addIfNotNull
+import java.util.concurrent.ConcurrentHashMap
 import kotlin.properties.ReadOnlyProperty
 import kotlin.reflect.KProperty
 
-abstract class FineGrainedEntityCache<Key: Any, Value: Any>(protected val project: Project, cleanOnLowMemory: Boolean): Disposable {
-    private val cache: MutableMap<Key, Value> by StorageProvider(project, javaClass) { HashMap() }
-        @Deprecated("Do not use directly", level = DeprecationLevel.ERROR) get
+abstract class AbstractFineGrainedEntityCache<Key: Any, Value: Any>(protected val project: Project, cleanOnLowMemory: Boolean): Disposable {
+    protected abstract val cache: MutableMap<Key, Value>
 
     private var invalidationCount: Int = 0
 
     private var currentInvalidationCount: Int = 0
 
-    private val lock = Any()
-
     protected val logger = Logger.getInstance(javaClass)
 
     init {
@@ -36,18 +34,21 @@ abstract class FineGrainedEntityCache<Key: Any, Value: Any>(protected val projec
         }
     }
 
-    private inline fun <T> useCache(block: (MutableMap<Key, Value>) -> T) {
-        synchronized(lock) {
-            @Suppress("DEPRECATION_ERROR")
-            cache.run(block)
-        }
-    }
+    protected abstract fun <T> useCache(block: (MutableMap<Key, Value>) -> T?): T?
 
     override fun dispose() {
         invalidate()
     }
 
-    fun get(key: Key): Value {
+    abstract fun get(key: Key): Value
+
+    protected fun checkEntitiesIfRequired(cache: MutableMap<Key, Value>) {
+        if (isValidityChecksEnabled && currentInvalidationCount > invalidationCount) {
+            checkEntities(cache, CHECK_ALL)
+        }
+    }
+
+    protected fun checkKeyAndDisposeIllegalEntry(key: Key) {
         if (isValidityChecksEnabled) {
             try {
                 checkKeyValidity(key)
@@ -59,30 +60,6 @@ abstract class FineGrainedEntityCache<Key: Any, Value: Any>(protected val projec
                 logger.error(e)
             }
         }
-
-        useCache { cache ->
-            if (currentInvalidationCount > invalidationCount) {
-                checkEntities(cache, CHECK_ALL)
-            }
-
-            cache[key]?.let { return it }
-        }
-
-        ProgressManager.checkCanceled()
-
-        val newValue = calculate(key)
-
-        if (isValidityChecksEnabled) {
-            checkValueValidity(newValue)
-        }
-
-        ProgressManager.checkCanceled()
-
-        useCache { cache ->
-            cache.putIfAbsent(key, newValue)?.let { return it }
-        }
-
-        return newValue
     }
 
     protected fun putAll(map: Map<Key, Value>) {
@@ -181,8 +158,6 @@ abstract class FineGrainedEntityCache<Key: Any, Value: Any>(protected val projec
 
     protected open fun checkValueValidity(value: Value) {}
 
-    protected abstract fun calculate(key: Key): Value
-
     companion object {
 
         val CHECK_ALL:(Any, Any) -> Boolean = { _, _ -> true }
@@ -197,6 +172,77 @@ abstract class FineGrainedEntityCache<Key: Any, Value: Any>(protected val projec
     }
 }
 
+abstract class FineGrainedEntityCache<Key: Any, Value: Any>(project: Project, cleanOnLowMemory: Boolean): AbstractFineGrainedEntityCache<Key, Value>(project, cleanOnLowMemory) {
+    override val cache: MutableMap<Key, Value> by StorageProvider(project, javaClass) { HashMap() }
+        @Deprecated("Do not use directly", level = DeprecationLevel.ERROR) get
+
+    private val lock = Any()
+
+    final override fun <T> useCache(block: (MutableMap<Key, Value>) -> T?): T? =
+        synchronized(lock) {
+            @Suppress("DEPRECATION_ERROR")
+            cache.run(block)
+        }
+
+    override fun get(key: Key): Value {
+        checkKeyAndDisposeIllegalEntry(key)
+
+        useCache { cache ->
+            checkEntitiesIfRequired(cache)
+
+            cache[key]
+        }?.let { return it }
+
+        ProgressManager.checkCanceled()
+
+        val newValue = calculate(key)
+
+        if (isValidityChecksEnabled) {
+            checkValueValidity(newValue)
+        }
+
+        ProgressManager.checkCanceled()
+
+        useCache { cache ->
+            cache.putIfAbsent(key, newValue)
+        }?.let { return it }
+
+        return newValue
+    }
+
+    abstract fun calculate(key: Key): Value
+
+}
+
+abstract class LockFreeFineGrainedEntityCache<Key: Any, Value: Any>(project: Project, cleanOnLowMemory: Boolean): AbstractFineGrainedEntityCache<Key, Value>(project, cleanOnLowMemory) {
+    override val cache: MutableMap<Key, Value> by StorageProvider(project, javaClass) { ConcurrentHashMap() }
+        @Deprecated("Do not use directly", level = DeprecationLevel.ERROR) get
+
+    final override fun <T> useCache(block: (MutableMap<Key, Value>) -> T?): T? =
+        @Suppress("DEPRECATION_ERROR")
+        cache.run(block)
+
+    override fun get(key: Key): Value {
+        checkKeyAndDisposeIllegalEntry(key)
+
+        useCache { cache ->
+            checkEntitiesIfRequired(cache)
+
+            cache[key]
+        }?.let { return it }
+
+        ProgressManager.checkCanceled()
+
+        return useCache { cache ->
+            calculate(cache, key)
+        }!!
+    }
+
+    abstract fun calculate(cache:MutableMap<Key, Value>, key: Key): Value
+
+}
+
+
 private class StorageProvider<Storage: Any>(
     private val project: Project,
     private val key: Class<*>,
@@ -205,7 +251,7 @@ private class StorageProvider<Storage: Any>(
     private val storage = lazy(factory)
 
     override fun getValue(thisRef: Any, property: KProperty<*>): Storage {
-        if (!FineGrainedEntityCache.isFineGrainedCacheInvalidationEnabled) {
+        if (!AbstractFineGrainedEntityCache.isFineGrainedCacheInvalidationEnabled) {
             @Suppress("DEPRECATION")
             return project.cacheByClassInvalidatingOnRootModifications(key) { factory() }
         }
index 13bafee92ac4df8fb5b3a9fb5a139fca5b8a217d..e587f244336938f9129e3910a3c4bc1066c6d4bc 100644 (file)
     <projectService serviceImplementation="org.jetbrains.kotlin.idea.base.projectStructure.LibraryInfoCache"/>
     <projectService serviceImplementation="org.jetbrains.kotlin.idea.project.ModulePlatformCache"/>
   </extensions>
-
-  <projectListeners>
-<!--    <listener class="org.jetbrains.kotlin.idea.base.projectStructure.LibraryInfoCache$ModelChangeListener"-->
-<!--              topic="com.intellij.workspaceModel.ide.WorkspaceModelChangeListener"/>-->
-<!--    <listener class="org.jetbrains.kotlin.idea.project.ModulePlatformCache$ModelChangeListener"-->
-<!--              topic="com.intellij.workspaceModel.ide.WorkspaceModelChangeListener"/>-->
-  </projectListeners>
 </idea-plugin>
\ No newline at end of file