FIR IDE: introduce memory leak checking in symbols test

This commit is contained in:
Ilya Kirillov
2020-11-05 08:53:18 +03:00
parent 11e94c1de1
commit 15277c0974
9 changed files with 175 additions and 10 deletions
@@ -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<String>) {
testClass<AbstractSymbolFromLibraryPointerRestoreTest> {
model("resoreSymbolFromLibrary", extension = "txt")
}
testClass<AbstractMemoryLeakInSymbolsTest> {
model("symbolMemoryLeak")
}
}
testGroup("idea/idea-frontend-fir/idea-fir-low-level-api/tests", "idea/testData") {
@@ -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
@@ -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<KotlinFirModificationTrackerService>().incrementModificationsCount()
}
}
fun Project.createProjectWideOutOfBlockModificationTracker() =
@@ -42,4 +42,9 @@ internal class KtFirAnalysisSessionProvider(project: Project) : KtAnalysisSessio
analysisSessionByModuleInfoCache.value.getOrPut(firModuleResolveState.moduleInfo) {
KtFirAnalysisSession.createAnalysisSessionByResolveState(firModuleResolveState)
}
@TestOnly
fun clearCaches() {
analysisSessionByModuleInfoCache.value.clear()
}
}
@@ -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"
}
@@ -21,9 +21,9 @@ internal class FirRefWithValidityCheck<D : FirDeclaration>(fir: D, resolveState:
inline fun <R> 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<D : FirDeclaration>(fir: D, resolveState:
inline fun <R> 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 })
}
@@ -0,0 +1,28 @@
fun <R> x(p: R): Int {
}
class Y<T> {
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
}
@@ -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<KtDeclaration>().map { it.getSymbol() }
}
}
invalidateAllCaches(ktFile)
System.gc()
val leakedSymbols = executeOnPooledThreadInReadAction {
symbols.map { it.hasNoFirElementLeak() }.filterIsInstance<LeakCheckResult.Leak>()
}
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<KotlinFirOutOfBlockModificationTrackerFactory>().incrementModificationsCount()
(project.service<KtAnalysisSessionProvider>() 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()
}
}
@@ -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");
}
}