[LL FIR] Implement disposal support for LLFirSessions
- This commit adds a `Disposable` to `LLFirSession` which lives as long as the session. For example, session components can now open a message bus connection that is automatically disposed after the session has been invalidated or reclaimed. - Because `LLFirSession`s should still benefit from soft reference-based automatic reclamation by the GC, we have to use a soft value map with cleanup in `LLFirSessionCache`. It ensures that the child disposables are properly disposed even when the session is collected as a soft value. - The session cannot be the `Disposable` itself because a valid disposable reference is needed to call `Disposer.dispose`. When a soft reference is added to a reference queue (such as the reference queue employed by the soft value map), its referent is already cleared. Hence, it's not possible to watch a softly referenced `Disposable` with a reference queue and also pass it to `Disposer.dispose`. This is why the `LLFirSessionCleaner` contains a strong reference to the `disposable`, because that will not keep the `LLFirSession` from being reclaimed, while keeping the `disposable` alive slightly longer than the session. ^KT-61222
This commit is contained in:
committed by
Space Team
parent
35856ab58f
commit
efb56ed30a
+64
-8
@@ -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<Disposable> = 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<LLFirSession> {
|
||||
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<LLFirSession> {
|
||||
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}"
|
||||
}
|
||||
|
||||
+21
-23
@@ -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<KtModule, LLFirSession>
|
||||
private typealias SessionStorage = CleanableSoftValueCache<KtModule, LLFirSession>
|
||||
|
||||
@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) {
|
||||
|
||||
Reference in New Issue
Block a user