From c502cae4374a5de1b5166373a66f6be378f39817 Mon Sep 17 00:00:00 2001 From: Alexander Korepanov Date: Mon, 19 Dec 2022 11:55:52 +0100 Subject: [PATCH] [JS IR IC] Calculate symbol hashes on demand Optimization: often we don't need all hashes for all available symbols. This patch allows the calculation of the symbol hash only on demand. This patch also allows cycles for the inline function dependency graph in the IC infrastructure. Note that the inline function cycles may crash the inliner, however it is not the IC infrastructure responsibility to check the cycles. --- .../kotlin/ir/backend/js/ic/CacheUpdater.kt | 5 - .../ir/backend/js/ic/HashCalculatorForIC.kt | 2 +- .../js/ic/IdSignatureHashCalculator.kt | 195 ++++++++---------- .../InvalidationTestGenerated.java | 5 + .../inlineFunctionCircleUsage/lib1/l1a.0.kt | 6 + .../inlineFunctionCircleUsage/lib1/l1a.2.kt | 6 + .../inlineFunctionCircleUsage/lib1/l1b.0.kt | 7 + .../inlineFunctionCircleUsage/lib1/l1b.1.kt | 7 + .../lib1/module.info | 15 ++ .../inlineFunctionCircleUsage/main/m.kt | 7 + .../main/module.info | 6 + .../inlineFunctionCircleUsage/project.info | 5 + 12 files changed, 155 insertions(+), 111 deletions(-) create mode 100644 js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/lib1/l1a.0.kt create mode 100644 js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/lib1/l1a.2.kt create mode 100644 js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/lib1/l1b.0.kt create mode 100644 js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/lib1/l1b.1.kt create mode 100644 js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/lib1/module.info create mode 100644 js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/main/m.kt create mode 100644 js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/main/module.info create mode 100644 js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/project.info diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/ic/CacheUpdater.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/ic/CacheUpdater.kt index 2e13aa9d149..25a2fe4a63f 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/ic/CacheUpdater.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/ic/CacheUpdater.kt @@ -493,8 +493,6 @@ class CacheUpdater( val stdlibDirtyFiles = dirtyFiles[stdlibFile] ?: return - signatureHashCalculator.updateInlineFunctionTransitiveHashes(listOf(stdlibIr)) - val updatedMetadata = KotlinSourceFileMutableMap() val idSignatureToFile = updatedMetadata.getExportedSignaturesAndAddMetadata(linker, stdlibIr, stdlibFile, stdlibDirtyFiles) @@ -636,9 +634,6 @@ class CacheUpdater( var lastDirtyFiles: KotlinSourceFileMap = dirtyFileExports while (true) { - stopwatch.startNext("Dependencies ($iterations) - calculating transitive hashes for inline functions") - updater.signatureHashCalculator.updateInlineFunctionTransitiveHashes(loadedIr.loadedFragments.values) - stopwatch.startNext("Dependencies ($iterations) - updating a dependency graph") val dirtyMetadata = updater.rebuildDirtySourceMetadata(loadedIr.linker, loadedIr.loadedFragments, lastDirtyFiles) diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/ic/HashCalculatorForIC.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/ic/HashCalculatorForIC.kt index 94405309830..642f73aea16 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/ic/HashCalculatorForIC.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/ic/HashCalculatorForIC.kt @@ -112,7 +112,7 @@ internal fun CompilerConfiguration.configHashForIC() = HashCalculatorForIC().app update(languageVersionSettings.toString()) }.finalize() -internal fun IrSimpleFunction.irSimpleFunctionHashForIC() = HashCalculatorForIC().also { +internal fun IrFunction.irFunctionHashForIC() = HashCalculatorForIC().also { it.update(this) }.finalize() diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/ic/IdSignatureHashCalculator.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/ic/IdSignatureHashCalculator.kt index f7ca006d9b1..5179b3fc3d3 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/ic/IdSignatureHashCalculator.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/ic/IdSignatureHashCalculator.kt @@ -14,129 +14,114 @@ import org.jetbrains.kotlin.ir.util.IdSignature import org.jetbrains.kotlin.ir.util.isFakeOverride import org.jetbrains.kotlin.ir.util.render import org.jetbrains.kotlin.ir.util.resolveFakeOverride -import org.jetbrains.kotlin.ir.visitors.IrElementVisitor import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid import org.jetbrains.kotlin.ir.visitors.acceptVoid - +import org.jetbrains.kotlin.utils.addToStdlib.ifTrue internal class IdSignatureHashCalculator { - private val flatHashes = hashMapOf() - private val inlineFunctionCallGraph = hashMapOf>() - private val processingFunctions = hashSetOf() - private val functionTransitiveHashes = hashMapOf() - private val fileAnnotationHashes = hashMapOf() + private val idSignatureSources = hashMapOf() + private val idSignatureHashes = hashMapOf() - private val allIdSignatureHashes = hashMapOf() + private val fileAnnotationHashes = hashMapOf() + private val inlineFunctionFlatHashes = hashMapOf() + + private val inlineFunctionDepends = hashMapOf>() private val IrFile.annotationsHash: ICHash get() = fileAnnotationHashes.getOrPut(this, ::irAnnotationContainerHashForIC) - private inner class FlatHashCalculator : IrElementVisitorVoid { - private var fileAnnotationsHash = ICHash() - - override fun visitElement(element: IrElement) { - element.acceptChildrenVoid(this) - } - - override fun visitFile(declaration: IrFile, data: Nothing?) { - fileAnnotationsHash = declaration.annotationsHash - declaration.acceptChildrenVoid(this) - } - - override fun visitSimpleFunction(declaration: IrSimpleFunction) { - if (declaration.isInline) { - if (declaration in flatHashes) { - return - } - - val inlineFunctionFlatHash = if (declaration.isFakeOverride) { - declaration.resolveFakeOverride()?.irSimpleFunctionHashForIC() - ?: icError("can not resolve fake override for ${declaration.render()}") - } else { - declaration.irSimpleFunctionHashForIC() - } - - flatHashes[declaration] = fileAnnotationsHash.combineWith(inlineFunctionFlatHash) - } - // go deeper since local inline special declarations (like a reference adaptor) may appear - declaration.acceptChildrenVoid(this) - } - } - - private inner class InlineFunctionCallGraphBuilder : IrElementVisitor> { - override fun visitElement(element: IrElement, data: MutableSet) { - element.acceptChildren(this, data) - } - - override fun visitSimpleFunction(declaration: IrSimpleFunction, data: MutableSet) { - if (declaration in inlineFunctionCallGraph) { - return - } - val newGraph = mutableSetOf() - inlineFunctionCallGraph[declaration] = newGraph - declaration.acceptChildren(this, newGraph) - } - - override fun visitCall(expression: IrCall, data: MutableSet) { - val callee = expression.symbol.owner - if (callee.isInline) { - data += callee - } - expression.acceptChildren(this, data) - } - - override fun visitFunctionReference(expression: IrFunctionReference, data: MutableSet) { - val reference = expression.symbol.owner - if (reference.isInline) { - // this if is fine, because fake overrides are not inlined as function reference calls even as inline function args - if (!reference.isFakeOverride) { - data += reference - } - } - expression.acceptChildren(this, data) - } - } - - private fun getInlineFunctionTransitiveHash(f: IrFunction): ICHash = functionTransitiveHashes.getOrPut(f) { - if (!processingFunctions.add(f)) { - icError("inline circle through function ${f.render()} detected") - } - val callees = inlineFunctionCallGraph[f] ?: icError("inline function is missed in inline graph ${f.render()}") - val flatHash = flatHashes[f] ?: icError("no flat hash for ${f.render()}") - var functionInlineHash = flatHash - for (callee in callees) { - functionInlineHash = functionInlineHash.combineWith(getInlineFunctionTransitiveHash(callee)) - } - processingFunctions.remove(f) - f.symbol.signature?.let { allIdSignatureHashes[it] = functionInlineHash } - functionInlineHash - } - - private fun updateTransitiveHashesByCallGraph() { - for ((f, callees) in inlineFunctionCallGraph.entries) { - if (f.isInline) { - getInlineFunctionTransitiveHash(f) + private val IrFunction.inlineFunctionFlatHash: ICHash + get() = inlineFunctionFlatHashes.getOrPut(this) { + val flatHash = if (isFakeOverride && this is IrSimpleFunction) { + resolveFakeOverride()?.irFunctionHashForIC() + ?: icError("can not resolve fake override for ${render()}") } else { - callees.forEach(::getInlineFunctionTransitiveHash) + irFunctionHashForIC() } + symbol.calculateSymbolHash().combineWith(flatHash) } + + private val IrFunction.inlineDepends: Collection + get() = inlineFunctionDepends.getOrPut(this) { + val usedInlineFunctions = linkedSetOf() + + acceptVoid(object : IrElementVisitorVoid { + override fun visitElement(element: IrElement) { + element.acceptChildrenVoid(this) + } + + override fun visitCall(expression: IrCall) { + val callee = expression.symbol.owner + if (callee.isInline) { + usedInlineFunctions += callee + } + expression.acceptChildrenVoid(this) + } + + override fun visitFunctionReference(expression: IrFunctionReference) { + val reference = expression.symbol.owner + if (reference.isInline) { + // this if is fine, because fake overrides are not inlined as function reference calls even as inline function args + if (!reference.isFakeOverride) { + usedInlineFunctions += reference + } + } + expression.acceptChildrenVoid(this) + } + }) + + usedInlineFunctions + } + + private fun IrSymbol.calculateSymbolHash(): ICHash { + var srcIrFile = signature?.let { sig -> idSignatureSources[sig]?.srcIrFile } + if (srcIrFile == null) { + var parentDeclaration = (owner as? IrDeclaration)?.parent + while (parentDeclaration is IrDeclaration) { + parentDeclaration = parentDeclaration.parent + } + srcIrFile = parentDeclaration as? IrFile + } + + val fileAnnotationsHash = srcIrFile?.annotationsHash ?: ICHash() + return fileAnnotationsHash.combineWith(irSymbolHashForIC()) } - fun updateInlineFunctionTransitiveHashes(fragments: Collection) { - fragments.forEach { it.acceptVoid(FlatHashCalculator()) } - fragments.forEach { it.acceptChildren(InlineFunctionCallGraphBuilder(), mutableSetOf()) } - updateTransitiveHashesByCallGraph() + private fun IrFunction.calculateInlineFunctionTransitiveHash(): ICHash { + var transitiveHash = inlineFunctionFlatHash + val transitiveDepends = hashSetOf(this) + val newDependsStack = transitiveDepends.toMutableList() + + while (newDependsStack.isNotEmpty()) { + newDependsStack.removeLast().inlineDepends.forEach { inlineFunction -> + if (transitiveDepends.add(inlineFunction)) { + newDependsStack += inlineFunction + transitiveHash = transitiveHash.combineWith(inlineFunction.inlineFunctionFlatHash) + } + } + } + + return transitiveHash } fun addAllSignatureSymbols(idSignatureToFile: Map) { - for ((signature, signatureSrc) in idSignatureToFile) { - if (signature !in allIdSignatureHashes) { - val fileAnnotationsHash = signatureSrc.srcIrFile.annotationsHash - allIdSignatureHashes[signature] = fileAnnotationsHash.combineWith(signatureSrc.symbol.irSymbolHashForIC()) - } - } + idSignatureSources += idSignatureToFile } - operator fun get(signature: IdSignature) = allIdSignatureHashes[signature] + operator fun get(signature: IdSignature): ICHash? { + val hash = idSignatureHashes[signature] + if (hash != null) { + return hash + } + + val signatureSymbol = idSignatureSources[signature]?.symbol ?: return null + + val signatureHash = (signatureSymbol.owner as? IrFunction)?.let { function -> + function.isInline.ifTrue { function.calculateInlineFunctionTransitiveHash() } + } ?: signatureSymbol.calculateSymbolHash() + + idSignatureHashes[signature] = signatureHash + return signatureHash + } } diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/incremental/InvalidationTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/incremental/InvalidationTestGenerated.java index d6a9b5d9b0a..740dc517ab2 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/incremental/InvalidationTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/incremental/InvalidationTestGenerated.java @@ -205,6 +205,11 @@ public class InvalidationTestGenerated extends AbstractInvalidationTest { runTest("js/js.translator/testData/incremental/invalidation/inlineFunctionAsParam/"); } + @TestMetadata("inlineFunctionCircleUsage") + public void testInlineFunctionCircleUsage() throws Exception { + runTest("js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/"); + } + @TestMetadata("inlineFunctionDefaultParams") public void testInlineFunctionDefaultParams() throws Exception { runTest("js/js.translator/testData/incremental/invalidation/inlineFunctionDefaultParams/"); diff --git a/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/lib1/l1a.0.kt b/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/lib1/l1a.0.kt new file mode 100644 index 00000000000..ea7876904f4 --- /dev/null +++ b/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/lib1/l1a.0.kt @@ -0,0 +1,6 @@ +inline fun funA(flag: Boolean = true): Int { + if (flag) { + return funB() + } + return 2 +} diff --git a/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/lib1/l1a.2.kt b/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/lib1/l1a.2.kt new file mode 100644 index 00000000000..3ce25eff57b --- /dev/null +++ b/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/lib1/l1a.2.kt @@ -0,0 +1,6 @@ +inline fun funA(flag: Boolean = false): Int { + if (flag) { + return funB() + } + return 2 +} diff --git a/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/lib1/l1b.0.kt b/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/lib1/l1b.0.kt new file mode 100644 index 00000000000..58b10bb2867 --- /dev/null +++ b/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/lib1/l1b.0.kt @@ -0,0 +1,7 @@ +inline fun funB(): Int { + val f = ::funA + if (false) { + return f(false) + } + return 0 +} diff --git a/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/lib1/l1b.1.kt b/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/lib1/l1b.1.kt new file mode 100644 index 00000000000..e97fbc2d1dd --- /dev/null +++ b/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/lib1/l1b.1.kt @@ -0,0 +1,7 @@ +inline fun funB(): Int { + val f = ::funA + if (false) { + return f(false) + } + return 1 +} diff --git a/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/lib1/module.info b/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/lib1/module.info new file mode 100644 index 00000000000..0e688c3a216 --- /dev/null +++ b/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/lib1/module.info @@ -0,0 +1,15 @@ +STEP 0: + modifications: + U : l1a.0.kt -> l1a.kt + U : l1b.0.kt -> l1b.kt + added file: l1a.kt, l1b.kt +STEP 1: + modifications: + U : l1b.1.kt -> l1b.kt + modified ir: l1b.kt + updated imports: l1a.kt +STEP 2: + modifications: + U : l1a.2.kt -> l1a.kt + modified ir: l1a.kt + updated imports: l1b.kt diff --git a/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/main/m.kt b/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/main/m.kt new file mode 100644 index 00000000000..20cacb21a8a --- /dev/null +++ b/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/main/m.kt @@ -0,0 +1,7 @@ +fun box(stepId: Int): String { + val x = funA() + if (x != stepId) { + return "Fail: $x != $stepId" + } + return "OK" +} diff --git a/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/main/module.info b/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/main/module.info new file mode 100644 index 00000000000..86cd47f6b2c --- /dev/null +++ b/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/main/module.info @@ -0,0 +1,6 @@ +STEP 0: + dependencies: lib1 + added file: m.kt +STEP 1..2: + dependencies: lib1 + updated imports: m.kt diff --git a/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/project.info b/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/project.info new file mode 100644 index 00000000000..912abf292a5 --- /dev/null +++ b/js/js.translator/testData/incremental/invalidation/inlineFunctionCircleUsage/project.info @@ -0,0 +1,5 @@ +MODULES: lib1, main + +STEP 0..2: + libs: lib1, main + dirty js: lib1, main