Skip to content

Commit

Permalink
FIR IDE: introduce memory leak checking in symbols test
Browse files Browse the repository at this point in the history
  • Loading branch information
darthorimar committed Nov 23, 2020
1 parent 11e94c1 commit 15277c0
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@ 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 =
KotlinFirOutOfBlockModificationTracker(project)

fun createModuleWithoutDependenciesOutOfBlockModificationTracker(module: Module): ModificationTracker =
KotlinFirOutOfBlockModuleModificationTracker(module)

@TestOnly
fun incrementModificationsCount() {
project.service<KotlinFirModificationTrackerService>().incrementModificationsCount()
}
}

fun Project.createProjectWideOutOfBlockModificationTracker() =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,9 @@ internal class KtFirAnalysisSessionProvider(project: Project) : KtAnalysisSessio
analysisSessionByModuleInfoCache.value.getOrPut(firModuleResolveState.moduleInfo) {
KtFirAnalysisSession.createAnalysisSessionByResolveState(firModuleResolveState)
}

@TestOnly
fun clearCaches() {
analysisSessionByModuleInfoCache.value.clear()
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@ 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) }
}

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 })
}
Expand Down
28 changes: 28 additions & 0 deletions idea/idea-frontend-fir/testData/symbolMemoryLeak/symbols.kt
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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()
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 15277c0

Please sign in to comment.