[Analysis API] Implement separate caching for unstable dangling files

PSI events do not arrive for dangling files with no backing
'VirtualFile', so its partial invalidation becomes non-trivial.
In the change, a short-living cache is implemented for these 'unstable'
dangling files.
This commit is contained in:
Yan Zhulanow
2023-11-15 16:18:31 +09:00
committed by Space Team
parent 9c91158be6
commit 6bbd252c07
6 changed files with 93 additions and 20 deletions
@@ -20,8 +20,10 @@ import org.jetbrains.kotlin.analysis.api.session.KtAnalysisSessionProvider
import org.jetbrains.kotlin.analysis.low.level.api.fir.LLFirInternals
import org.jetbrains.kotlin.analysis.low.level.api.fir.api.getFirResolveSession
import org.jetbrains.kotlin.analysis.low.level.api.fir.file.structure.LLFirDeclarationModificationService
import org.jetbrains.kotlin.analysis.project.structure.KtDanglingFileModule
import org.jetbrains.kotlin.analysis.project.structure.KtModule
import org.jetbrains.kotlin.analysis.project.structure.ProjectStructureProvider
import org.jetbrains.kotlin.analysis.project.structure.isStable
import org.jetbrains.kotlin.analysis.providers.createProjectWideOutOfBlockModificationTracker
import org.jetbrains.kotlin.psi.KtElement
import java.util.concurrent.ConcurrentHashMap
@@ -42,6 +44,12 @@ class KtFirAnalysisSessionProvider(project: Project) : KtAnalysisSessionProvider
}
override fun getAnalysisSessionByUseSiteKtModule(useSiteKtModule: KtModule): KtAnalysisSession {
if (useSiteKtModule is KtDanglingFileModule && !useSiteKtModule.isStable) {
val firResolveSession = useSiteKtModule.getFirResolveSession(project)
val validityToken = tokenFactory.create(project)
return KtFirAnalysisSession.createAnalysisSessionByFirResolveSession(firResolveSession, validityToken)
}
val identifier = tokenFactory.identifier
identifier.flushPendingChanges(project)
@@ -50,6 +50,8 @@ import org.jetbrains.kotlin.resolve.jvm.modules.JavaModuleResolver
import org.jetbrains.kotlin.scripting.compiler.plugin.FirScriptingSamWithReceiverExtensionRegistrar
import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition
import org.jetbrains.kotlin.utils.exceptions.errorWithAttachment
import org.jetbrains.kotlin.utils.exceptions.requireWithAttachment
import org.jetbrains.kotlin.utils.exceptions.withPsiEntry
import org.jetbrains.kotlin.utils.exceptions.withVirtualFileEntry
import kotlin.script.experimental.host.ScriptingHostConfiguration
import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration
@@ -466,14 +468,16 @@ internal abstract class LLFirAbstractSessionFactory(protected val project: Proje
contextSession: LLFirSession,
additionalSessionConfiguration: context(DanglingFileSessionCreationContext) LLFirDanglingFileSession.() -> Unit,
): LLFirSession {
val danglingFile = module.file
val platform = module.platform
val builtinsSession = LLFirBuiltinsSessionFactory.getInstance(project).getBuiltinsSession(platform)
val languageVersionSettings = wrapLanguageVersionSettings(contextSession.languageVersionSettings)
val scopeProvider = FirKotlinScopeProvider(::wrapScopeWithJvmMapped)
val components = LLFirModuleResolveComponents(module, globalResolveComponents, scopeProvider)
val session = LLFirDanglingFileSession(module, components, builtinsSession.builtinTypes)
val session = LLFirDanglingFileSession(module, components, builtinsSession.builtinTypes, danglingFile.modificationStamp)
components.session = session
val moduleData = createModuleData(session)
@@ -490,7 +494,7 @@ internal abstract class LLFirAbstractSessionFactory(protected val project: Proje
this,
components,
canContainKotlinPackage = true,
declarationProviderFactory = { scope -> scope.createScopedDeclarationProviderForFile(module.file) }
declarationProviderFactory = { scope -> scope.createScopedDeclarationProviderForFile(danglingFile) }
)
register(FirProvider::class, firProvider)
@@ -575,8 +579,19 @@ internal abstract class LLFirAbstractSessionFactory(protected val project: Proje
fun getOrCreateSessionForDependency(dependency: KtModule): LLFirSession? = when (dependency) {
is KtBuiltinsModule -> null // Built-ins are already added
is KtBinaryModule -> llFirSessionCache.getSession(dependency, preferBinary = true)
is KtSourceModule, is KtDanglingFileModule -> llFirSessionCache.getSession(dependency)
is KtSourceModule -> llFirSessionCache.getSession(dependency)
is KtDanglingFileModule -> {
requireWithAttachment(dependency.isStable, message = { "Unstable dangling modules cannot be used as a dependency" }) {
withKtModuleEntry("module", module)
withKtModuleEntry("dependency", dependency)
withPsiEntry("dependencyFile", dependency.file)
}
llFirSessionCache.getSession(dependency)
}
is KtScriptModule,
is KtScriptDependencyModule,
@@ -14,4 +14,5 @@ internal class LLFirDanglingFileSession @PrivateSessionConstructor constructor(
ktModule: KtDanglingFileModule,
override val moduleComponents: LLFirModuleResolveComponents,
builtinTypes: BuiltinTypes,
val modificationStamp: Long
) : LLFirResolvableModuleSession(ktModule, builtinTypes)
@@ -36,6 +36,7 @@ class LLFirSessionCache(private val project: Project) {
private val sourceCache: SessionStorage = CollectionFactory.createConcurrentSoftValueMap()
private val binaryCache: SessionStorage = CollectionFactory.createConcurrentSoftValueMap()
private val danglingFileSessionCache: SessionStorage = CollectionFactory.createConcurrentSoftValueMap()
private val unstableDanglingFileSessionCache: SessionStorage = CollectionFactory.createConcurrentSoftValueMap()
/**
* Returns the existing session if found, or creates a new session and caches it.
@@ -48,12 +49,11 @@ class LLFirSessionCache(private val project: Project) {
}
}
val targetCache = when (module) {
is KtDanglingFileModule -> danglingFileSessionCache
else -> sourceCache
if (module is KtDanglingFileModule) {
return getDanglingFileCachedSession(module)
}
return getCachedSession(module, targetCache, ::createSession)
return getCachedSession(module, sourceCache, factory = ::createSession)
}
/**
@@ -64,16 +64,38 @@ class LLFirSessionCache(private val project: Project) {
return createSession(module)
}
private fun <T : KtModule> getCachedSession(
module: T,
storage: SessionStorage,
factory: (T) -> LLFirSession
): LLFirSession {
private fun getDanglingFileCachedSession(module: KtDanglingFileModule): LLFirSession {
if (module.isStable) {
return getCachedSession(module, danglingFileSessionCache, ::createSession)
}
checkCanceled()
return storage.computeIfAbsent(module) { factory(module) }.also { session ->
require(session.isValid) { "A session acquired via `getSession` should always be valid. Module: $module" }
val session = unstableDanglingFileSessionCache.compute(module) { _, existingSession ->
if (existingSession is LLFirDanglingFileSession && existingSession.modificationStamp == module.file.modificationStamp) {
existingSession
} else {
createSession(module)
}
}
requireNotNull(session)
checkSessionValidity(session)
return session
}
private fun <T : KtModule> getCachedSession(module: T, storage: SessionStorage, factory: (T) -> LLFirSession): LLFirSession {
checkCanceled()
val session = storage.computeIfAbsent(module) { factory(module) }
checkSessionValidity(session)
return session
}
private fun checkSessionValidity(session: LLFirSession) {
require(session.isValid) { "A session acquired via `getSession` should always be valid. Module: ${session.ktModule}" }
}
/**
@@ -87,8 +109,9 @@ class LLFirSessionCache(private val project: Project) {
val didSourceSessionExist = removeSessionFrom(module, sourceCache)
val didBinarySessionExist = module is KtBinaryModule && removeSessionFrom(module, binaryCache)
val didDanglingFileSessionExist = module is KtDanglingFileModule && removeSessionFrom(module, danglingFileSessionCache)
val didUnstableDanglingFileSessionExist = module is KtDanglingFileModule && removeSessionFrom(module, unstableDanglingFileSessionCache)
return didSourceSessionExist || didBinarySessionExist || didDanglingFileSessionExist
return didSourceSessionExist || didBinarySessionExist || didDanglingFileSessionExist || didUnstableDanglingFileSessionExist
}
private fun removeSessionFrom(module: KtModule, storage: SessionStorage): Boolean {
@@ -117,7 +140,13 @@ class LLFirSessionCache(private val project: Project) {
removeAllDanglingFileSessions()
}
fun removeUnstableDanglingFileSessions() {
removeAllSessionsFrom(unstableDanglingFileSessionCache)
}
fun removeContextualDanglingFileSessions(contextModule: KtModule) {
removeUnstableDanglingFileSessions()
if (contextModule is KtDanglingFileModule) {
removeAllMatchingSessionsFrom(danglingFileSessionCache) { it is KtDanglingFileModule && hasContextModule(it, contextModule) }
} else {
@@ -136,6 +165,7 @@ class LLFirSessionCache(private val project: Project) {
fun removeAllDanglingFileSessions() {
removeAllSessionsFrom(danglingFileSessionCache)
removeAllSessionsFrom(unstableDanglingFileSessionCache)
}
// Removing script sessions is only needed temporarily until KTIJ-25620 has been implemented.
@@ -8,6 +8,7 @@ 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.psi.util.PsiModificationTracker
import org.jetbrains.kotlin.analysis.project.structure.*
import org.jetbrains.kotlin.analysis.providers.KotlinAnchorModuleProvider
import org.jetbrains.kotlin.analysis.providers.analysisMessageBus
@@ -19,6 +20,10 @@ import org.jetbrains.kotlin.analysis.providers.topics.*
* sessions in [LLFirSessionCache] and the cache has to be kept consistent.
*/
class LLFirSessionInvalidationService(private val project: Project) : Disposable {
private val sessionCache: LLFirSessionCache by lazy {
LLFirSessionCache.getInstance(project)
}
/**
* Subscribes to all [modification events][KotlinTopics] via the [analysisMessageBus].
*
@@ -53,7 +58,11 @@ class LLFirSessionInvalidationService(private val project: Project) : Disposable
)
busConnection.subscribe(
KotlinTopics.CODE_FRAGMENT_CONTEXT_MODIFICATION,
KotlinCodeFragmentContextModificationListener { module -> invalidateCodeFragments(module) }
KotlinCodeFragmentContextModificationListener { module -> invalidateContextualDanglingFileSessions(module) }
)
busConnection.subscribe(
PsiModificationTracker.TOPIC,
PsiModificationTracker.Listener { invalidateUnstableDanglingFileSessions() }
)
}
@@ -65,7 +74,6 @@ class LLFirSessionInvalidationService(private val project: Project) : Disposable
fun invalidate(module: KtModule) {
ApplicationManager.getApplication().assertWriteAccessAllowed()
val sessionCache = LLFirSessionCache.getInstance(project)
val didSessionExist = sessionCache.removeSession(module)
// We don't have to invalidate dependent sessions if the root session does not exist in the cache. It is true that sessions can be
@@ -105,11 +113,15 @@ class LLFirSessionInvalidationService(private val project: Project) : Disposable
anchorModules?.forEach(::invalidate)
}
LLFirSessionCache.getInstance(project).removeAllSessions(includeLibraryModules)
sessionCache.removeAllSessions(includeLibraryModules)
}
private fun invalidateCodeFragments(contextModule: KtModule) {
LLFirSessionCache.getInstance(project).removeContextualDanglingFileSessions(contextModule)
private fun invalidateContextualDanglingFileSessions(contextModule: KtModule) {
sessionCache.removeContextualDanglingFileSessions(contextModule)
}
private fun invalidateUnstableDanglingFileSessions() {
sessionCache.removeUnstableDanglingFileSessions()
}
override fun dispose() {
@@ -240,6 +240,13 @@ public interface KtDanglingFileModule : KtModule {
get() = "Temporary file"
}
/**
* True if the dangling file module supports partial invalidation on PSI modifications.
* Sessions for such modules can be cached for longer time.
*/
public val KtDanglingFileModule.isStable: Boolean
get() = file.isPhysical && file.viewProvider.isEventSystemEnabled
/**
* A set of sources which live outside the project content root. E.g, testdata files or source files of some other project.
*/