[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:
Marco Pennekamp
2023-08-18 19:08:47 +02:00
committed by Space Team
parent 35856ab58f
commit efb56ed30a
2 changed files with 85 additions and 31 deletions
@@ -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}"
}
@@ -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) {