diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSession.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSession.kt index 9c410400284..3e85a9abdbd 100644 --- a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSession.kt +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSession.kt @@ -5,8 +5,11 @@ package org.jetbrains.kotlin.analysis.low.level.api.fir.sessions +import com.intellij.openapi.Disposable import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.ModificationTracker +import org.jetbrains.kotlin.analysis.low.level.api.fir.caches.SoftValueCleaner import org.jetbrains.kotlin.analysis.project.structure.KtModule import org.jetbrains.kotlin.fir.BuiltinTypes import org.jetbrains.kotlin.fir.FirElementWithResolveState @@ -34,14 +37,6 @@ abstract class LLFirSession( var isValid: Boolean = true private set - /** - * [markInvalid] should be called at the same time as the session is removed from [LLFirSessionCache]. Hence, session validity should be - * managed by [LLFirSessionCache]. - */ - internal fun markInvalid() { - isValid = false - } - /** * Creates a [ModificationTracker] which tracks the validity of this session via [isValid]. */ @@ -61,6 +56,67 @@ abstract class LLFirSession( } } + private val lazyDisposable: Lazy = lazy { + val disposable = Disposer.newDisposable() + + // `LLFirSessionCache` is used as a disposable parent so that disposal is triggered after the Kotlin plugin is unloaded. We don't + // register a module as a disposable parent, because (1) IJ `Module`s may persist beyond the plugin's lifetime, (2) not all + // `KtModule`s have a corresponding `Module`, and (3) sessions are invalidated (and subsequently cleaned up) when their module is + // removed. + Disposer.register(LLFirSessionCache.getInstance(project), disposable) + + disposable + } + + /** + * Returns an already registered [Disposable] which is alive until the session is invalidated. It can be used as a parent disposable for + * disposable session components, such as [resolve extensions][org.jetbrains.kotlin.analysis.api.resolve.extensions.KtResolveExtension]. + * When the session is invalidated or garbage-collected, all disposable session components will be disposed with this parent disposable. + * + * Because not all sessions have disposable components, this disposable is created and registered on-demand with the first call to + * [requestDisposable]. This avoids polluting [Disposer] with unneeded disposables. + * + * The disposable must only be requested during session creation, before the session is added to [LLFirSessionCache]. + */ + fun requestDisposable(): Disposable = lazyDisposable.value + + /** + * Creates a [SoftValueCleaner] that performs cleanup after the session has been invalidated or reclaimed. This cleanup will mark the + * session as invalid and dispose its disposable (if requested during session creation). + */ + internal fun createCleaner(): SoftValueCleaner { + val disposable = if (lazyDisposable.isInitialized()) lazyDisposable.value else null + return LLFirSessionCleaner(disposable) + } + + /** + * [LLFirSessionCleaner] must not keep a strong reference to its associated [LLFirSession], because otherwise the soft reference-based + * garbage collection of unused sessions will not work. + * + * @param disposable The associated [LLFirSession]'s [disposable]. Keeping a separate reference ensures that the disposable can be + * disposed even after the session has been reclaimed by the GC. + */ + private class LLFirSessionCleaner(private val disposable: Disposable?) : SoftValueCleaner { + override fun cleanUp(value: LLFirSession?) { + // If both the session and the disposable are present, we can check their consistency. Otherwise, this is not possible, because + // we cannot store the session in the session cleaner (otherwise the session will never be garbage-collected). + if (value != null && disposable != null) { + require(value.lazyDisposable.isInitialized()) { + "The session to clean up should have an initialized disposable when a disposable is also registered with this" + + " cleaner. The session to clean up might not be consistent with the session from which this cleaner was created." + } + + require(value.lazyDisposable.value == disposable) { + "The session to clean up should have a disposable that is equal to the disposable registered with this cleaner. The" + + " session to clean up might not be consistent with the session from which this cleaner was created." + } + } + + value?.isValid = false + disposable?.let { Disposer.dispose(it) } + } + } + override fun toString(): String { return "${this::class.simpleName} for ${ktModule.moduleDescription}" } diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSessionCache.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSessionCache.kt index 71f534b71d2..b5b7dd7ec34 100644 --- a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSessionCache.kt +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSessionCache.kt @@ -5,10 +5,11 @@ package org.jetbrains.kotlin.analysis.low.level.api.fir.sessions +import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.project.Project -import com.intellij.util.containers.CollectionFactory import org.jetbrains.kotlin.analysis.low.level.api.fir.LLFirInternals +import org.jetbrains.kotlin.analysis.low.level.api.fir.caches.CleanableSoftValueCache import org.jetbrains.kotlin.analysis.low.level.api.fir.util.checkCanceled import org.jetbrains.kotlin.analysis.project.structure.* import org.jetbrains.kotlin.fir.FirModuleDataImpl @@ -21,26 +22,29 @@ import org.jetbrains.kotlin.platform.jvm.JvmPlatform import org.jetbrains.kotlin.platform.jvm.JvmPlatforms import org.jetbrains.kotlin.platform.konan.NativePlatform import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatformAnalyzerServices -import java.util.concurrent.ConcurrentMap -private typealias SessionStorage = ConcurrentMap +private typealias SessionStorage = CleanableSoftValueCache @LLFirInternals -class LLFirSessionCache(private val project: Project) { +class LLFirSessionCache(private val project: Project) : Disposable { companion object { fun getInstance(project: Project): LLFirSessionCache { return project.getService(LLFirSessionCache::class.java) } } - private val sourceCache: SessionStorage = CollectionFactory.createConcurrentSoftValueMap() - private val binaryCache: SessionStorage = CollectionFactory.createConcurrentSoftValueMap() - private val danglingFileSessionCache: SessionStorage = CollectionFactory.createConcurrentSoftValueMap() - private val unstableDanglingFileSessionCache: SessionStorage = CollectionFactory.createConcurrentSoftValueMap() + // Removal from the session storage invokes the `LLFirSession`'s cleaner, which marks the session as invalid and disposes any + // disposables registered with the `LLFirSession`'s disposable. + private val sourceCache: SessionStorage = CleanableSoftValueCache(LLFirSession::createCleaner) + private val binaryCache: SessionStorage = CleanableSoftValueCache(LLFirSession::createCleaner) + private val danglingFileSessionCache: SessionStorage = CleanableSoftValueCache(LLFirSession::createCleaner) + private val unstableDanglingFileSessionCache: SessionStorage = CleanableSoftValueCache(LLFirSession::createCleaner) /** * Returns the existing session if found, or creates a new session and caches it. * Analyzable session will be returned for a library module. + * + * Must be called from a read action. */ fun getSession(module: KtModule, preferBinary: Boolean = false): LLFirSession { if (module is KtBinaryModule && (preferBinary || module is KtSdkModule)) { @@ -123,11 +127,7 @@ class LLFirSessionCache(private val project: Project) { return didSourceSessionExist || didBinarySessionExist || didDanglingFileSessionExist || didUnstableDanglingFileSessionExist } - private fun removeSessionFrom(module: KtModule, storage: SessionStorage): Boolean { - val session = storage.remove(module) ?: return false - session.markInvalid() - return true - } + private fun removeSessionFrom(module: KtModule, storage: SessionStorage): Boolean = storage.remove(module) != null /** * Removes all sessions after global invalidation. If [includeLibraryModules] is `false`, sessions of library modules will not be @@ -190,21 +190,16 @@ class LLFirSessionCache(private val project: Project) { } private fun removeAllSessionsFrom(storage: SessionStorage) { - // Because `removeAllSessionsFrom` is executed in a write action, the order of setting `isValid` and clearing `storage` is not - // important. - storage.values.forEach { it.markInvalid() } storage.clear() } private inline fun removeAllMatchingSessionsFrom(storage: SessionStorage, shouldBeRemoved: (KtModule) -> Boolean) { - // `ConcurrentSoftValueHashMap` (the implementation used by `storage`) does not back its entry set but rather creates a copy, which - // is in violation of the contract of `Map.entrySet`, and thus changes to the entry set are not reflected in `storage`. Because this - // function is executed in a write action, we do not need the weak consistency guarantees made by `ConcurrentMap`'s iterator, so a + // Because this function is executed in a write action, we do not need concurrency guarantees to remove all matching sessions, so a // "collect and remove" approach also works. - val scriptEntries = storage.entries.filter { (module, _) -> shouldBeRemoved(module) } - for ((module, session) in scriptEntries) { - session.markInvalid() - storage.remove(module) + storage.keys.forEach { module -> + if (shouldBeRemoved(module)) { + storage.remove(module) + } } } @@ -241,6 +236,9 @@ class LLFirSessionCache(private val project: Project) { else -> LLFirCommonSessionFactory(project) } } + + override fun dispose() { + } } internal fun LLFirSessionConfigurator.Companion.configure(session: LLFirSession) {