Skip to content

Commit 0b09be7

Browse files
hungvietnguyenSpace Team
authored and
Space Team
committed
New IC: Detect changes to class annotations
Both the new and old incremental compilation (IC) analysis rely on Kotlin class metadata to detect a change. However, Kotlin metadata currently doesn't contain info about annotations (KT-57919), so the IC will not be able to detect a change to them. With this commit, we'll fix the new IC such that it can detect a change to class annotations by not relying only on metadata. We currently scope this fix to the new IC (cross-module analysis) first. We'll fix this issue for within-module analysis later. Performance: There seems to be no performance impact from this change. Snapshotting the 400MB ideaIC-2022.1.4/app.jar takes 4.1s before and after this change. Test: Added ClasspathChangesComputerTest.testChangedAnnotations ^KT-58289: Fixed
1 parent 993925f commit 0b09be7

File tree

22 files changed

+315
-230
lines changed

22 files changed

+315
-230
lines changed

build-common/src/org/jetbrains/kotlin/incremental/IncrementalJvmCache.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ open class IncrementalJvmCache(
441441
storageFile: File,
442442
icContext: IncrementalCompilationContext,
443443
) :
444-
BasicStringMap<Map<String, Any>>(storageFile, MapExternalizer(StringExternalizer, ConstantValueExternalizer), icContext) {
444+
BasicStringMap<Map<String, Long>>(storageFile, MapExternalizer(StringExternalizer, LongExternalizer), icContext) {
445445

446446
operator fun contains(className: JvmClassName): Boolean =
447447
className.internalName in storage
@@ -451,7 +451,7 @@ open class IncrementalJvmCache(
451451
val key = kotlinClassInfo.className.internalName
452452
val oldMap = storage[key] ?: emptyMap()
453453

454-
val newMap = kotlinClassInfo.constantsMap
454+
val newMap = kotlinClassInfo.extraInfo.constantSnapshots
455455
if (newMap.isNotEmpty()) {
456456
storage[key] = newMap
457457
} else {
@@ -489,8 +489,8 @@ open class IncrementalJvmCache(
489489
storage.remove(className.internalName)
490490
}
491491

492-
override fun dumpValue(value: Map<String, Any>): String =
493-
value.dumpMap(Any::toString)
492+
override fun dumpValue(value: Map<String, Long>): String =
493+
value.dumpMap(Long::toString)
494494
}
495495

496496
private inner class PackagePartMap(
@@ -592,7 +592,7 @@ open class IncrementalJvmCache(
592592
val key = kotlinClassInfo.className.internalName
593593
val oldMap = storage[key] ?: emptyMap()
594594

595-
val newMap = kotlinClassInfo.inlineFunctionsAndAccessorsMap
595+
val newMap = kotlinClassInfo.extraInfo.inlineFunctionOrAccessorSnapshots
596596
if (newMap.isNotEmpty()) {
597597
storage[key] = newMap
598598
} else {

build-common/src/org/jetbrains/kotlin/incremental/KotlinClassInfo.kt

Lines changed: 142 additions & 92 deletions
Large diffs are not rendered by default.

build-common/src/org/jetbrains/kotlin/incremental/storage/externalizers.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,7 @@ class DelegateDataExternalizer<T>(
274274

275275
override fun read(input: DataInput): T {
276276
val typeIndex = input.readByte().toInt()
277-
@Suppress("UNCHECKED_CAST")
278-
return typesExternalizers[typeIndex].read(input) as T
277+
return typesExternalizers[typeIndex].read(input)
279278
}
280279
}
281280

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

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,13 @@ class ClassFileWithContentsProvider(
3434
val classFile: ClassFile,
3535
val contentsProvider: () -> ByteArray
3636
) {
37-
38-
fun loadContents(): ClassFileWithContents {
39-
val classContents = contentsProvider.invoke()
40-
val classInfo = BasicClassInfo.compute(classContents)
41-
return ClassFileWithContents(classFile, classContents, classInfo)
42-
}
37+
fun loadContents() = ClassFileWithContents(classFile, contentsProvider.invoke())
4338
}
4439

4540
/** Information about the location of a .class file ([ClassFile]) and its contents. */
4641
class ClassFileWithContents(
4742
@Suppress("unused") val classFile: ClassFile,
48-
val contents: ByteArray,
49-
val classInfo: BasicClassInfo
50-
)
43+
val contents: ByteArray
44+
) {
45+
val classInfo: BasicClassInfo = BasicClassInfo.compute(contents)
46+
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,22 @@ object ClasspathChangesComputer {
246246
// classes, and symbols in removed classes.
247247
incrementalJvmCache.clearCacheForRemovedClasses(changesCollector)
248248

249+
// IncrementalJvmCache currently doesn't use the `KotlinClassInfo.extraInfo.classSnapshotExcludingMembers` info when comparing
250+
// classes, so we need to do it here.
251+
// TODO(KT-58289): Ensure IncrementalJvmCache uses that info when comparing classes.
252+
val currentClassSnapshotsExcludingMembers = currentClassSnapshots
253+
.associate { it.classId to it.classMemberLevelSnapshot!!.extraInfo.classSnapshotExcludingMembers }
254+
.filter { it.value != null }
255+
val previousClassSnapshotsExcludingMembers = previousClassSnapshots
256+
.associate { it.classId to it.classMemberLevelSnapshot!!.extraInfo.classSnapshotExcludingMembers }
257+
.filter { it.value != null }
258+
previousClassSnapshotsExcludingMembers.keys.intersect(currentClassSnapshotsExcludingMembers.keys).forEach {
259+
if (previousClassSnapshotsExcludingMembers[it]!! != currentClassSnapshotsExcludingMembers[it]!!) {
260+
// `areSubclassesAffected = false` as we don't need to compute impacted symbols at this step
261+
changesCollector.collectSignature(fqName = it.asSingleFqName(), areSubclassesAffected = false)
262+
}
263+
}
264+
249265
// Get the changes and clean up
250266
val dirtyData = changesCollector.getChangedSymbols(DoNothingICReporter)
251267
workingDir.deleteRecursively()

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ package org.jetbrains.kotlin.incremental.classpathDiff
88
import com.intellij.util.containers.Interner
99
import com.intellij.util.io.DataExternalizer
1010
import org.jetbrains.kotlin.build.report.metrics.BuildPerformanceMetric
11-
import org.jetbrains.kotlin.incremental.ConstantValueExternalizer
1211
import org.jetbrains.kotlin.incremental.KotlinClassInfo
1312
import org.jetbrains.kotlin.incremental.storage.*
1413
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
@@ -162,8 +161,7 @@ internal object KotlinClassInfoExternalizer : DataExternalizer<KotlinClassInfo>
162161
ListExternalizer(StringExternalizer).save(output, info.classHeaderData.toList())
163162
ListExternalizer(StringExternalizer).save(output, info.classHeaderStrings.toList())
164163
NullableValueExternalizer(StringExternalizer).save(output, info.multifileClassName)
165-
MapExternalizer(StringExternalizer, ConstantValueExternalizer).save(output, info.constantsMap)
166-
MapExternalizer(InlineFunctionOrAccessorExternalizer, LongExternalizer).save(output, info.inlineFunctionsAndAccessorsMap)
164+
ExtraInfoExternalizer.save(output, info.extraInfo)
167165
}
168166

169167
override fun read(input: DataInput): KotlinClassInfo {
@@ -174,8 +172,24 @@ internal object KotlinClassInfoExternalizer : DataExternalizer<KotlinClassInfo>
174172
classHeaderData = ListExternalizer(StringExternalizer).read(input).toTypedArray(),
175173
classHeaderStrings = ListExternalizer(StringExternalizer).read(input).toTypedArray(),
176174
multifileClassName = NullableValueExternalizer(StringExternalizer).read(input),
177-
constantsMap = MapExternalizer(StringExternalizer, ConstantValueExternalizer).read(input),
178-
inlineFunctionsAndAccessorsMap = MapExternalizer(InlineFunctionOrAccessorExternalizer, LongExternalizer).read(input)
175+
extraInfo = ExtraInfoExternalizer.read(input)
176+
)
177+
}
178+
}
179+
180+
internal object ExtraInfoExternalizer : DataExternalizer<KotlinClassInfo.ExtraInfo> {
181+
182+
override fun save(output: DataOutput, info: KotlinClassInfo.ExtraInfo) {
183+
NullableValueExternalizer(LongExternalizer).save(output, info.classSnapshotExcludingMembers)
184+
MapExternalizer(StringExternalizer, LongExternalizer).save(output, info.constantSnapshots)
185+
MapExternalizer(InlineFunctionOrAccessorExternalizer, LongExternalizer).save(output, info.inlineFunctionOrAccessorSnapshots)
186+
}
187+
188+
override fun read(input: DataInput): KotlinClassInfo.ExtraInfo {
189+
return KotlinClassInfo.ExtraInfo(
190+
classSnapshotExcludingMembers = NullableValueExternalizer(LongExternalizer).read(input),
191+
constantSnapshots = MapExternalizer(StringExternalizer, LongExternalizer).read(input),
192+
inlineFunctionOrAccessorSnapshots = MapExternalizer(InlineFunctionOrAccessorExternalizer, LongExternalizer).read(input)
179193
)
180194
}
181195
}
@@ -200,7 +214,7 @@ private object JavaClassSnapshotExternalizer : DataExternalizer<JavaClassSnapsho
200214
}
201215
}
202216

203-
private object JavaClassMemberLevelSnapshotExternalizer : DataExternalizer<JavaClassMemberLevelSnapshot> {
217+
internal object JavaClassMemberLevelSnapshotExternalizer : DataExternalizer<JavaClassMemberLevelSnapshot> {
204218

205219
override fun save(output: DataOutput, snapshot: JavaClassMemberLevelSnapshot) {
206220
JavaElementSnapshotExternalizer.save(output, snapshot.classAbiExcludingMembers)

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

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,23 @@ import org.jetbrains.kotlin.build.report.metrics.BuildMetricsReporter
99
import org.jetbrains.kotlin.build.report.metrics.BuildTime
1010
import org.jetbrains.kotlin.build.report.metrics.DoNothingBuildMetricsReporter
1111
import org.jetbrains.kotlin.build.report.metrics.measure
12+
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.snapshotClass
13+
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.snapshotClassExcludingMembers
14+
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.snapshotField
15+
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.snapshotMethod
16+
import org.jetbrains.kotlin.incremental.ClassNodeSnapshotter.sortClassMembers
1217
import org.jetbrains.kotlin.incremental.DifferenceCalculatorForPackageFacade.Companion.getNonPrivateMembers
1318
import org.jetbrains.kotlin.incremental.KotlinClassInfo
1419
import org.jetbrains.kotlin.incremental.PackagePartProtoData
1520
import org.jetbrains.kotlin.incremental.classpathDiff.ClassSnapshotGranularity.CLASS_MEMBER_LEVEL
16-
import org.jetbrains.kotlin.incremental.md5
21+
import org.jetbrains.kotlin.incremental.hashToLong
1722
import org.jetbrains.kotlin.incremental.storage.toByteArray
1823
import org.jetbrains.kotlin.konan.file.use
1924
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader.Kind.*
2025
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
26+
import org.jetbrains.org.objectweb.asm.ClassReader
27+
import org.jetbrains.org.objectweb.asm.Opcodes
28+
import org.jetbrains.org.objectweb.asm.tree.ClassNode
2129
import java.io.Closeable
2230
import java.io.File
2331
import java.util.zip.ZipEntry
@@ -94,7 +102,7 @@ object ClassSnapshotter {
94102
snapshotKotlinClass(clazz, granularity)
95103
}
96104
else -> metrics.measure(BuildTime.SNAPSHOT_JAVA_CLASSES) {
97-
JavaClassSnapshotter.snapshot(clazz, granularity)
105+
snapshotJavaClass(clazz, granularity)
98106
}
99107
}
100108
}
@@ -120,11 +128,7 @@ object ClassSnapshotter {
120128
}
121129
}
122130

123-
/**
124-
* Computes a [KotlinClassSnapshot] of the given Kotlin class.
125-
*
126-
* (The caller must ensure that the given class is a Kotlin class.)
127-
*/
131+
/** Computes a [KotlinClassSnapshot] of the given Kotlin class. */
128132
private fun snapshotKotlinClass(classFile: ClassFileWithContents, granularity: ClassSnapshotGranularity): KotlinClassSnapshot {
129133
val kotlinClassInfo =
130134
KotlinClassInfo.createFrom(classFile.classInfo.classId, classFile.classInfo.kotlinClassHeader!!, classFile.contents)
@@ -145,13 +149,60 @@ object ClassSnapshotter {
145149
)
146150
MULTIFILE_CLASS -> MultifileClassKotlinClassSnapshot(
147151
classId, classAbiHash, classMemberLevelSnapshot,
148-
constantNames = kotlinClassInfo.constantsMap.keys
152+
constantNames = kotlinClassInfo.extraInfo.constantSnapshots.keys
149153
)
150154
SYNTHETIC_CLASS -> error("Unexpected class $classId with class kind ${SYNTHETIC_CLASS.name} (synthetic classes should have been removed earlier)")
151155
UNKNOWN -> error("Can't handle class $classId with class kind ${UNKNOWN.name}")
152156
}
153157
}
154158

159+
/** Computes a [JavaClassSnapshot] of the given Java class. */
160+
private fun snapshotJavaClass(classFile: ClassFileWithContents, granularity: ClassSnapshotGranularity): JavaClassSnapshot {
161+
// For incremental compilation, we only care about the ABI info of a class. There are 2 approaches:
162+
// 1. Collect ABI info directly
163+
// 2. Remove non-ABI info from the full class
164+
// Note that for incremental compilation to be correct, all ABI info must be collected exhaustively (now and in the future when
165+
// there are updates to Java/ASM), whereas it is acceptable if non-ABI info is not removed completely.
166+
// In the following, we will use the second approach as it is safer and easier.
167+
168+
val classNode = ClassNode()
169+
val classReader = ClassReader(classFile.contents)
170+
171+
// Note the `parsingOptions` passed to `classReader`:
172+
// - Pass SKIP_CODE as method bodies are not important
173+
// - Do not pass SKIP_DEBUG as debug info (e.g., method parameter names) may be important
174+
classReader.accept(classNode, ClassReader.SKIP_CODE)
175+
sortClassMembers(classNode)
176+
177+
// Remove private fields and methods
178+
fun Int.isPrivate() = (this and Opcodes.ACC_PRIVATE) != 0
179+
classNode.fields.removeIf { it.access.isPrivate() }
180+
classNode.methods.removeIf { it.access.isPrivate() }
181+
182+
// Snapshot the class
183+
val classMemberLevelSnapshot = if (granularity == CLASS_MEMBER_LEVEL) {
184+
JavaClassMemberLevelSnapshot(
185+
classAbiExcludingMembers = JavaElementSnapshot(classNode.name, snapshotClassExcludingMembers(classNode)),
186+
fieldsAbi = classNode.fields.map { JavaElementSnapshot(it.name, snapshotField(it)) },
187+
methodsAbi = classNode.methods.map { JavaElementSnapshot(it.name, snapshotMethod(it, classNode.version)) }
188+
)
189+
} else {
190+
null
191+
}
192+
val classAbiHash = if (granularity == CLASS_MEMBER_LEVEL) {
193+
JavaClassMemberLevelSnapshotExternalizer.toByteArray(classMemberLevelSnapshot!!).hashToLong()
194+
} else {
195+
snapshotClass(classNode)
196+
}
197+
198+
return JavaClassSnapshot(
199+
classId = classFile.classInfo.classId,
200+
classAbiHash = classAbiHash,
201+
classMemberLevelSnapshot = classMemberLevelSnapshot,
202+
supertypes = classFile.classInfo.supertypes
203+
)
204+
}
205+
155206
}
156207

157208
private sealed interface DirectoryOrJarReader : Closeable {
@@ -227,9 +278,3 @@ private class JarReader(jar: File) : DirectoryOrJarReader {
227278
zipFile.close()
228279
}
229280
}
230-
231-
internal fun ByteArray.hashToLong(): Long {
232-
// Note: The returned type `Long` is 64-bit, but we currently don't have a good 64-bit hash function.
233-
// The method below uses `md5` which is 128-bit and converts it to `Long`.
234-
return md5()
235-
}

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

Lines changed: 0 additions & 95 deletions
This file was deleted.

compiler/incremental-compilation-impl/test/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathChangesComputerTest.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,9 @@ class KotlinOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
259259
/** Regression test for KT-55021. */
260260
@Test
261261
fun testRenameFileFacade() {
262+
// Check that classpath changes computation doesn't fail.
263+
// Ideally, the returned changes should be empty (renaming a file facade alone shouldn't cause any `LookupSymbol`s to change), but
264+
// it is currently not the case. However, this is just a small efficiency, not a serious bug.
262265
val changes = computeClasspathChanges(File(testDataDir, "KotlinOnly/testRenameFileFacade/src"), tmpDir)
263266
Changes(
264267
lookupSymbols = setOf(
@@ -269,6 +272,20 @@ class KotlinOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
269272
).assertEquals(changes)
270273
}
271274

275+
/** Regression test for KT-58289.*/
276+
@Test
277+
fun testChangedAnnotations() {
278+
val changes = computeClasspathChanges(File(testDataDir, "KotlinOnly/testChangedAnnotations/src"), tmpDir)
279+
Changes(
280+
lookupSymbols = setOf(
281+
LookupSymbol(name = "SomeClassWithChangedAnnotation", scope = "com.example"),
282+
),
283+
fqNames = setOf(
284+
"com.example.SomeClassWithChangedAnnotation",
285+
)
286+
).assertEquals(changes)
287+
}
288+
272289
/** Tests [SupertypesInheritorsImpact]. */
273290
@Test
274291
override fun testImpactComputation_SupertypesInheritors() {

0 commit comments

Comments
 (0)