[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:
+8
@@ -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)
|
||||
|
||||
|
||||
+18
-3
@@ -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,
|
||||
|
||||
+1
@@ -14,4 +14,5 @@ internal class LLFirDanglingFileSession @PrivateSessionConstructor constructor(
|
||||
ktModule: KtDanglingFileModule,
|
||||
override val moduleComponents: LLFirModuleResolveComponents,
|
||||
builtinTypes: BuiltinTypes,
|
||||
val modificationStamp: Long
|
||||
) : LLFirResolvableModuleSession(ktModule, builtinTypes)
|
||||
|
||||
+42
-12
@@ -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.
|
||||
|
||||
+17
-5
@@ -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() {
|
||||
|
||||
+7
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user