diff --git a/generators/tests/org/jetbrains/kotlin/generators/tests/GenerateTests.kt b/generators/tests/org/jetbrains/kotlin/generators/tests/GenerateTests.kt index 7163b3ef307..f2c688a2d36 100644 --- a/generators/tests/org/jetbrains/kotlin/generators/tests/GenerateTests.kt +++ b/generators/tests/org/jetbrains/kotlin/generators/tests/GenerateTests.kt @@ -89,10 +89,7 @@ import org.jetbrains.kotlin.idea.folding.AbstractKotlinFoldingTest import org.jetbrains.kotlin.idea.frontend.api.fir.AbstractResolveCallTest import org.jetbrains.kotlin.idea.fir.low.level.api.trackers.AbstractProjectWideOutOfBlockKotlinModificationTrackerTest import org.jetbrains.kotlin.idea.frontend.api.scopes.AbstractMemberScopeByFqNameTest -import org.jetbrains.kotlin.idea.frontend.api.symbols.AbstractSymbolFromLibraryPointerRestoreTest -import org.jetbrains.kotlin.idea.frontend.api.symbols.AbstractSymbolsByFqNameBuildingTest -import org.jetbrains.kotlin.idea.frontend.api.symbols.AbstractSymbolFromSourcePointerRestoreTest -import org.jetbrains.kotlin.idea.frontend.api.symbols.AbstractSymbolsByPsiBuildingTest +import org.jetbrains.kotlin.idea.frontend.api.symbols.* import org.jetbrains.kotlin.idea.hierarchy.AbstractHierarchyTest import org.jetbrains.kotlin.idea.hierarchy.AbstractHierarchyWithLibTest import org.jetbrains.kotlin.idea.highlighter.* @@ -1020,6 +1017,10 @@ fun main(args: Array) { testClass { model("resoreSymbolFromLibrary", extension = "txt") } + + testClass { + model("symbolMemoryLeak") + } } testGroup("idea/idea-frontend-fir/idea-fir-low-level-api/tests", "idea/testData") { diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/trackers/KotlinFirOutOfBlockModificationTracker.kt b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/trackers/KotlinFirOutOfBlockModificationTracker.kt index 3a39f7f7250..0b253e5ec27 100644 --- a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/trackers/KotlinFirOutOfBlockModificationTracker.kt +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/trackers/KotlinFirOutOfBlockModificationTracker.kt @@ -8,6 +8,7 @@ package org.jetbrains.kotlin.idea.fir.low.level.api.trackers import com.intellij.ProjectTopics import com.intellij.lang.ASTNode import com.intellij.openapi.Disposable +import com.intellij.openapi.components.service import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ModuleRootEvent @@ -18,6 +19,7 @@ import com.intellij.pom.event.PomModelEvent import com.intellij.pom.event.PomModelListener import com.intellij.pom.tree.TreeAspect import com.intellij.pom.tree.events.TreeChangeEvent +import org.jetbrains.annotations.TestOnly import org.jetbrains.kotlin.idea.KotlinLanguage import org.jetbrains.kotlin.idea.fir.low.level.api.element.builder.getNonLocalContainingInBodyDeclarationWith import org.jetbrains.kotlin.idea.fir.low.level.api.file.structure.FileElementFactory @@ -52,6 +54,11 @@ internal class KotlinFirModificationTrackerService(project: Project) : Disposabl moduleModificationsState.increaseModificationCountForAllModules() } + @TestOnly + fun incrementModificationsCount() { + increaseModificationCountForAllModules() + } + private inner class Listener : PomModelListener { override fun isAspectChangeInteresting(aspect: PomModelAspect): Boolean = treeAspect == aspect diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/trackers/KotlinFirOutOfBlockModificationTrackerFactory.kt b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/trackers/KotlinFirOutOfBlockModificationTrackerFactory.kt index 0444cf44ff9..28d090422e4 100644 --- a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/trackers/KotlinFirOutOfBlockModificationTrackerFactory.kt +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/trackers/KotlinFirOutOfBlockModificationTrackerFactory.kt @@ -9,6 +9,7 @@ import com.intellij.openapi.components.service import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project import com.intellij.openapi.util.ModificationTracker +import org.jetbrains.annotations.TestOnly class KotlinFirOutOfBlockModificationTrackerFactory(private val project: Project) { fun createProjectWideOutOfBlockModificationTracker(): ModificationTracker = @@ -16,6 +17,11 @@ class KotlinFirOutOfBlockModificationTrackerFactory(private val project: Project fun createModuleWithoutDependenciesOutOfBlockModificationTracker(module: Module): ModificationTracker = KotlinFirOutOfBlockModuleModificationTracker(module) + + @TestOnly + fun incrementModificationsCount() { + project.service().incrementModificationsCount() + } } fun Project.createProjectWideOutOfBlockModificationTracker() = diff --git a/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/KtFirAnalysisSessionProvider.kt b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/KtFirAnalysisSessionProvider.kt index 7474896a19b..01c28dc1a79 100644 --- a/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/KtFirAnalysisSessionProvider.kt +++ b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/KtFirAnalysisSessionProvider.kt @@ -42,4 +42,9 @@ internal class KtFirAnalysisSessionProvider(project: Project) : KtAnalysisSessio analysisSessionByModuleInfoCache.value.getOrPut(firModuleResolveState.moduleInfo) { KtFirAnalysisSession.createAnalysisSessionByResolveState(firModuleResolveState) } + + @TestOnly + fun clearCaches() { + analysisSessionByModuleInfoCache.value.clear() + } } \ No newline at end of file diff --git a/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/utils/EntityWasGarbageCollectedException.kt b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/utils/EntityWasGarbageCollectedException.kt new file mode 100644 index 00000000000..0f70622df82 --- /dev/null +++ b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/utils/EntityWasGarbageCollectedException.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.frontend.api.fir.utils + +internal class EntityWasGarbageCollectedException(entity: String) : IllegalStateException() { + override val message: String = "$entity was garbage collected while KtAnalysisSession session is still valid" +} \ No newline at end of file diff --git a/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/utils/FirRefWithValidityCheck.kt b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/utils/FirRefWithValidityCheck.kt index a32a49740b2..da29a8264ea 100644 --- a/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/utils/FirRefWithValidityCheck.kt +++ b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/utils/FirRefWithValidityCheck.kt @@ -21,9 +21,9 @@ internal class FirRefWithValidityCheck(fir: D, resolveState: inline fun withFir(phase: FirResolvePhase = FirResolvePhase.RAW_FIR, crossinline action: (fir: D) -> R): R { token.assertIsValid() val fir = firWeakRef.get() - ?: error("FirElement was garbage collected while analysis session is still valid") - val resolveState = - resolveStateWeakRef.get() ?: error("FirModuleResolveState was garbage collected while analysis session is still valid") + ?: throw EntityWasGarbageCollectedException("FirElement") + val resolveState = resolveStateWeakRef.get() + ?: throw EntityWasGarbageCollectedException("FirModuleResolveState") LowLevelFirApiFacade.resolvedFirToPhase(fir, phase, resolveState) return resolveState.withFirDeclaration(fir) { action(it) } } @@ -31,9 +31,9 @@ internal class FirRefWithValidityCheck(fir: D, resolveState: inline fun withFirResolvedToBodyResolve(action: (fir: D) -> R): R { token.assertIsValid() val fir = firWeakRef.get() - ?: error("FirElement was garbage collected while analysis session is still valid") - val resolveState = - resolveStateWeakRef.get() ?: error("FirModuleResolveState was garbage collected while analysis session is still valid") + ?: throw EntityWasGarbageCollectedException("FirElement") + val resolveState = resolveStateWeakRef.get() + ?: throw EntityWasGarbageCollectedException("FirModuleResolveState") LowLevelFirApiFacade.resolvedFirToPhase(fir, FirResolvePhase.BODY_RESOLVE, resolveState) return action(resolveState.withFirDeclaration(fir) { it }) } diff --git a/idea/idea-frontend-fir/testData/symbolMemoryLeak/symbols.kt b/idea/idea-frontend-fir/testData/symbolMemoryLeak/symbols.kt new file mode 100644 index 00000000000..442c10530d5 --- /dev/null +++ b/idea/idea-frontend-fir/testData/symbolMemoryLeak/symbols.kt @@ -0,0 +1,28 @@ +fun x(p: R): Int { + +} + +class Y { + fun a() = 1 +} + +var z: Int + get = 10 + set(value) {} + +object Q + +val z: String = "" + +fun yyy() { +// val q = 10 +// fun aaa() {} +// +// class F {} +} + +typealias Str = String + +interface I { + fun str(): String +} diff --git a/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/frontend/api/symbols/AbstractMemoryLeakInSymbolsTest.kt b/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/frontend/api/symbols/AbstractMemoryLeakInSymbolsTest.kt new file mode 100644 index 00000000000..ead4321986f --- /dev/null +++ b/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/frontend/api/symbols/AbstractMemoryLeakInSymbolsTest.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.frontend.api.symbols + +import com.intellij.openapi.components.service +import com.intellij.openapi.util.io.FileUtil +import junit.framework.Assert +import org.jetbrains.kotlin.idea.executeOnPooledThreadInReadAction +import org.jetbrains.kotlin.idea.fir.low.level.api.trackers.KotlinFirOutOfBlockModificationTrackerFactory +import org.jetbrains.kotlin.idea.frontend.api.InvalidWayOfUsingAnalysisSession +import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSessionProvider +import org.jetbrains.kotlin.idea.frontend.api.analyze +import org.jetbrains.kotlin.idea.frontend.api.fir.KtFirAnalysisSessionProvider +import org.jetbrains.kotlin.idea.frontend.api.fir.symbols.KtFirSymbol +import org.jetbrains.kotlin.idea.frontend.api.fir.utils.EntityWasGarbageCollectedException +import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType +import java.io.File + +abstract class AbstractMemoryLeakInSymbolsTest : KotlinLightCodeInsightFixtureTestCase() { + override fun isFirPlugin() = true + + protected fun doTest(path: String) { + val testDataFile = File(path) + val ktFile = myFixture.configureByText(testDataFile.name, FileUtil.loadFile(testDataFile)) as KtFile + val symbols = executeOnPooledThreadInReadAction { + analyze(ktFile) { + ktFile.collectDescendantsOfType().map { it.getSymbol() } + } + } + + invalidateAllCaches(ktFile) + System.gc() + + val leakedSymbols = executeOnPooledThreadInReadAction { + symbols.map { it.hasNoFirElementLeak() }.filterIsInstance() + } + if (leakedSymbols.isNotEmpty()) { + Assert.fail( + """The following symbols leaked (${leakedSymbols.size}/${symbols.size}) + ${leakedSymbols.joinToString(separator = "\n") { it.symbol }} + """.trimIndent() + ) + } + } + + @OptIn(InvalidWayOfUsingAnalysisSession::class) + private fun invalidateAllCaches(ktFile: KtFile) { + project.service().incrementModificationsCount() + (project.service() as KtFirAnalysisSessionProvider).clearCaches() + executeOnPooledThreadInReadAction { analyze(ktFile) {} } + } + + private fun KtSymbol.hasNoFirElementLeak(): LeakCheckResult { + require(this is KtFirSymbol<*>) + return try { + firRef.withFir { LeakCheckResult.Leak(this::class.simpleName!!) } + } catch (_: EntityWasGarbageCollectedException) { + LeakCheckResult.NoLeak + } + } + + private sealed class LeakCheckResult { + object NoLeak : LeakCheckResult() + data class Leak(val symbol: String) : LeakCheckResult() + } +} + diff --git a/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/frontend/api/symbols/MemoryLeakInSymbolsTestGenerated.java b/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/frontend/api/symbols/MemoryLeakInSymbolsTestGenerated.java new file mode 100644 index 00000000000..ed8ef8de4a2 --- /dev/null +++ b/idea/idea-frontend-fir/tests/org/jetbrains/kotlin/idea/frontend/api/symbols/MemoryLeakInSymbolsTestGenerated.java @@ -0,0 +1,35 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.frontend.api.symbols; + +import com.intellij.testFramework.TestDataPath; +import org.jetbrains.kotlin.test.JUnit3RunnerWithInners; +import org.jetbrains.kotlin.test.KotlinTestUtils; +import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.runner.RunWith; + +import java.io.File; +import java.util.regex.Pattern; + +/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */ +@SuppressWarnings("all") +@TestMetadata("idea/idea-frontend-fir/testData/symbolMemoryLeak") +@TestDataPath("$PROJECT_ROOT") +@RunWith(JUnit3RunnerWithInners.class) +public class MemoryLeakInSymbolsTestGenerated extends AbstractMemoryLeakInSymbolsTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, this, testDataFilePath); + } + + public void testAllFilesPresentInSymbolMemoryLeak() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("idea/idea-frontend-fir/testData/symbolMemoryLeak"), Pattern.compile("^(.+)\\.kt$"), null, true); + } + + @TestMetadata("symbols.kt") + public void testSymbols() throws Exception { + runTest("idea/idea-frontend-fir/testData/symbolMemoryLeak/symbols.kt"); + } +}