Skip to content

Commit e16f5a8

Browse files
mazhukinevgeniySpace Team
authored and
Space Team
committed
[IC] Update inline function snapshotting
This is a more robust version with extra tests and better support for non-lambda local classes ^KT-75883
1 parent 5f12d8b commit e16f5a8

File tree

13 files changed

+248
-74
lines changed

13 files changed

+248
-74
lines changed

build-common/src/org/jetbrains/kotlin/incremental/impl/ExtraClassInfoGenerator.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,16 @@ open class ExtraClassInfoGenerator() {
2525
return classNode
2626
}
2727

28-
protected open fun calculateInlineMethodHash(methodSignature: JvmMemberSignature.Method, ownMethodHash: Long): Long {
28+
/**
29+
* @param methodSignature well-typed method signature. doesn't include the containing class' internal name
30+
* @param inlinedClassPrefix - includes class internal name and method name. example value is "com/bar/OuterClass$InnerClass$calculate"
31+
* @param ownMethodHash - a basic intuition is that it's based on bytecode and debug info
32+
*/
33+
protected open fun calculateInlineMethodHash(
34+
methodSignature: JvmMemberSignature.Method,
35+
inlinedClassPrefix: String,
36+
ownMethodHash: Long
37+
): Long {
2938
return ownMethodHash
3039
}
3140

@@ -89,8 +98,9 @@ open class ExtraClassInfoGenerator() {
8998
// class metadata (also in the source file), but not in the bytecode. However, we can safely ignore those
9099
// inline functions/accessors because they are not declared in the bytecode and therefore can't be referenced.
91100
val methodSignature = JvmMemberSignature.Method(name = methodNode.name, desc = methodNode.desc)
101+
val innerClassPrefix = "${classNode.name}\$${methodNode.name}"
92102
var methodHash = snapshotMethod(methodNode, classNode.version)
93-
inlineFunctionsAndAccessors[methodSignature]!! to calculateInlineMethodHash(methodSignature, methodHash)
103+
inlineFunctionsAndAccessors[methodSignature]!! to calculateInlineMethodHash(methodSignature, innerClassPrefix, methodHash)
94104
}
95105

96106
return ExtraInfo(classSnapshotExcludingMembers, constantSnapshots, inlineFunctionOrAccessorSnapshots)

build-common/src/org/jetbrains/kotlin/incremental/impl/InstanceOwnerRecordingClassVisitor.kt

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,26 @@ class InstanceOwnerRecordingClassVisitor(
3232

3333
return object : MethodVisitor(Opcodes.ASM9, super.visitMethod(access, name, descriptor, signature, exceptions)) {
3434
override fun visitFieldInsn(opcode: Int, owner: String?, name: String?, descriptor: String?) {
35-
if (owner != null && opcode == Opcodes.GETSTATIC && name == "INSTANCE") {
36-
val jvmClassName = JvmClassName.byInternalName(owner)
37-
methodToUsedClassesMap?.getOrPut(methodSignature) { mutableSetOf() }?.add(jvmClassName)
38-
allUsedClassesSet?.add(jvmClassName)
35+
if (opcode == Opcodes.GETSTATIC && name == "INSTANCE" && owner?.contains("$") == true) {
36+
storeUsage(owner)
3937
}
4038
super.visitFieldInsn(opcode, owner, name, descriptor)
4139
}
40+
41+
override fun visitTypeInsn(opcode: Int, type: String?) {
42+
if (opcode == Opcodes.NEW && type?.contains("$") == true) {
43+
// here we might be instantiating a class from another inline function in the same module
44+
// better safe than sorry, so:
45+
storeUsage(type)
46+
}
47+
super.visitTypeInsn(opcode, type)
48+
}
49+
50+
private fun storeUsage(internalName: String) {
51+
val jvmClassName = JvmClassName.byInternalName(internalName)
52+
methodToUsedClassesMap?.getOrPut(methodSignature) { mutableSetOf() }?.add(jvmClassName)
53+
allUsedClassesSet?.add(jvmClassName)
54+
}
4255
}
4356
}
4457
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
val result = calculateA()
2+
3+
fun main(args: Array<String>) {
4+
println(result)
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
inline fun calculateA(): Int {
2+
return calculateB()
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
inline fun calculateB(): Int {
2+
val calculator = object {
3+
fun calc(): Int = 42
4+
}
5+
return calculator.calc()
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
inline fun calculateB(): Int {
2+
val calculator = object {
3+
fun calc(): Int = 42 + 3
4+
}
5+
return calculator.calc()
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
val result = calculateA()
2+
3+
fun main(args: Array<String>) {
4+
println(result)
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
inline fun calculateA(): Int {
2+
return calculateB()
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
interface NiceType {
2+
fun calc(): Int
3+
}
4+
5+
inline fun calculateB(): Int {
6+
val calculator = object : NiceType {
7+
override fun calc(): Int = 42
8+
}
9+
return calculator.calc()
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
interface NiceType {
2+
fun calc(): Int
3+
}
4+
5+
inline fun calculateB(): Int {
6+
val calculator = object : NiceType {
7+
override fun calc(): Int = 42 + 3
8+
}
9+
return calculator.calc()
10+
}

compiler/build-tools/kotlin-build-tools-api-tests/src/testCrossModuleIncrementalChanges/kotlin/InlinedLambdaChangeTest.kt

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import org.jetbrains.kotlin.buildtools.api.tests.compilation.util.compile
1313
import org.jetbrains.kotlin.buildtools.api.tests.compilation.util.execute
1414
import org.jetbrains.kotlin.buildtools.api.tests.compilation.util.moduleWithInlineSnapshotting
1515
import org.jetbrains.kotlin.test.TestMetadata
16-
import org.junit.jupiter.api.Disabled
1716
import org.junit.jupiter.api.DisplayName
1817

1918

@@ -190,9 +189,51 @@ class InlinedLambdaChangeTest : BaseCompilationTest() {
190189

191190
lib.replaceFileWithVersion("inlinedB.kt", "changeLambdaInB")
192191

193-
// It so happens that we don't need to recompile A.
194-
// As far as IC is concerned, the compiler can do whatever it wants:
195-
// the main criteria for these tests is that the expected output is generated by the built & executed app.
192+
// Because of a compiler optimization, we don't need to recompile A:
193+
// Inside the lib module inlinedA directly uses a local class from inlinedB without copying it.
194+
// It's ok. The main criteria for these tests is that the expected output is generated by the built & executed app.
195+
lib.compile(expectedDirtySet = setOf("inlinedB.kt"))
196+
app.compile(expectedDirtySet = setOf("callSite.kt"))
197+
app.execute(mainClass = "CallSiteKt", exactOutput = WITH_NEW_LAMBDA_BODY)
198+
}
199+
}
200+
201+
@DefaultStrategyAgnosticCompilationTest
202+
@DisplayName("Changes in anonymous object inside inline function B affect call site of inline function A that calls B - happy version")
203+
@TestMetadata("ic-scenarios/inline-local-class/nested-inline-as-anonymous-object/lib")
204+
fun testNestedInlineTrulyAnonymousObject(strategyConfig: CompilerExecutionStrategyConfiguration) {
205+
scenario(strategyConfig) {
206+
val lib = module("ic-scenarios/inline-local-class/nested-inline-as-anonymous-object/lib")
207+
val app = moduleWithInlineSnapshotting(
208+
"ic-scenarios/inline-local-class/nested-inline-as-anonymous-object/app",
209+
dependencies = listOf(lib),
210+
)
211+
212+
app.execute(mainClass = "CallSiteKt", exactOutput = INITIAL_OUTPUT)
213+
214+
lib.replaceFileWithVersion("inlinedB.kt", "changeObjectInB")
215+
216+
lib.compile(expectedDirtySet = setOf("inlinedB.kt"))
217+
app.compile(expectedDirtySet = setOf("callSite.kt"))
218+
app.execute(mainClass = "CallSiteKt", exactOutput = WITH_NEW_LAMBDA_BODY)
219+
}
220+
}
221+
222+
@DefaultStrategyAgnosticCompilationTest
223+
@DisplayName("Changes in anonymous object inside inline function B affect call site of inline function A that calls B")
224+
@TestMetadata("ic-scenarios/inline-local-class/nested-inline-as-typed-object/lib")
225+
fun testNestedInlineAnonymousObject(strategyConfig: CompilerExecutionStrategyConfiguration) {
226+
scenario(strategyConfig) {
227+
val lib = module("ic-scenarios/inline-local-class/nested-inline-as-typed-object/lib")
228+
val app = moduleWithInlineSnapshotting(
229+
"ic-scenarios/inline-local-class/nested-inline-as-typed-object/app",
230+
dependencies = listOf(lib),
231+
)
232+
233+
app.execute(mainClass = "CallSiteKt", exactOutput = INITIAL_OUTPUT)
234+
235+
lib.replaceFileWithVersion("inlinedB.kt", "changeObjectInB")
236+
196237
lib.compile(expectedDirtySet = setOf("inlinedB.kt"))
197238
app.compile(expectedDirtySet = setOf("callSite.kt"))
198239
app.execute(mainClass = "CallSiteKt", exactOutput = WITH_NEW_LAMBDA_BODY)
@@ -284,7 +325,6 @@ class InlinedLambdaChangeTest : BaseCompilationTest() {
284325
}
285326
}
286327

287-
@Disabled("KT-75883 - here callable's code creates a new object so there's no INSTANCE")
288328
@DefaultStrategyAgnosticCompilationTest
289329
@DisplayName("Recompilation of call site affected by an anonymous object - slightly evil")
290330
@TestMetadata("ic-scenarios/inline-local-class/inline-anonymous-object-evil/lib")
@@ -301,7 +341,7 @@ class InlinedLambdaChangeTest : BaseCompilationTest() {
301341
lib.replaceFileWithVersion("SomeClass.kt", "withOverload")
302342

303343
lib.compile(expectedDirtySet = setOf("SomeClass.kt", "callable.kt"))
304-
app.compile(expectedDirtySet = setOf())
344+
app.compile(expectedDirtySet = setOf("callSite.kt"))
305345
app.execute(mainClass = "CallSiteKt", exactOutput = WITH_NEW_LAMBDA_BODY)
306346
}
307347
}

compiler/incremental-compilation-impl/src/org/jetbrains/kotlin/incremental/classpathDiff/impl/ClassListSnapshotter.kt

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,7 @@ internal class ClassListSnapshotterWithInlinedClassSupport(
142142

143143

144144
override fun snapshot(): List<ClassSnapshot> {
145-
return classes.map {
146-
val mapped = makeOrReuseClassSnapshot(it)
147-
148-
mapped
149-
}
145+
return classes.map { makeOrReuseClassSnapshot(it) }
150146
}
151147

152148
private fun makeOrReuseClassSnapshot(classFile: ClassFileWithContentsProvider): ClassSnapshot {
@@ -164,9 +160,7 @@ internal class ClassListSnapshotterWithInlinedClassSupport(
164160
private fun makeOrReuseClassSnapshot(descriptor: ClassDescriptorForProcessing, classFileWithContents: ClassFileWithContents): ClassSnapshot {
165161
descriptor.snapshot?.let { return it }
166162

167-
// loading is an expensive part of ClassListSnapshotter, so it's worth trying to minimize it.
168-
// this part of the implementation would be updated by KT-75883
169-
val loadedClasses = mutableListOf<Pair<ClassDescriptorForProcessing, ClassFileWithContents>>()
163+
// TODO consider reusing loaded classes across the pipeline - it should be benefitial but it makes the process more complicated
170164

171165
val snapshot = if (isInaccessible(classFileWithContents)) {
172166
InaccessibleClassSnapshot
@@ -177,13 +171,7 @@ internal class ClassListSnapshotterWithInlinedClassSupport(
177171
* so inlinedSnapshots calculation must not directly call regular snapshotting to prevent infinite loops
178172
*/
179173
val extraInfo = ExtraInfoGeneratorWithInlinedClassSnapshotting(
180-
classMultiHashProvider = object : ClassMultiHashProvider {
181-
override fun searchAndGetFullAbiHashOfUsedClasses(rootClasses: Set<JvmClassName>): Long {
182-
val outcome = inlinedClassSnapshotter.searchAndGetFullAbiHashOfUsedClasses(rootClasses)
183-
loadedClasses.addAll(outcome.loadedClasses)
184-
return outcome.calculatedHash
185-
}
186-
},
174+
classMultiHashProvider = inlinedClassSnapshotter as ClassMultiHashProvider,
187175
).getExtraInfo(
188176
classFileWithContents.classInfo.kotlinClassHeader!!,
189177
classFileWithContents.contents,
@@ -204,9 +192,6 @@ internal class ClassListSnapshotterWithInlinedClassSupport(
204192

205193
descriptor.snapshot = snapshot
206194

207-
for ((descriptor, contents) in loadedClasses) {
208-
makeOrReuseClassSnapshot(descriptor, contents)
209-
}
210195
return snapshot
211196
}
212197

0 commit comments

Comments
 (0)