diff --git a/build-common/src/org/jetbrains/kotlin/incremental/IncrementalJvmCache.kt b/build-common/src/org/jetbrains/kotlin/incremental/IncrementalJvmCache.kt index 2bab98fe8a7..033ed64dbd6 100644 --- a/build-common/src/org/jetbrains/kotlin/incremental/IncrementalJvmCache.kt +++ b/build-common/src/org/jetbrains/kotlin/incremental/IncrementalJvmCache.kt @@ -39,12 +39,11 @@ import org.jetbrains.kotlin.resolve.jvm.JvmClassName import org.jetbrains.org.objectweb.asm.* import java.io.File import java.security.MessageDigest -import java.util.* val KOTLIN_CACHE_DIRECTORY_NAME = "kotlin" open class IncrementalJvmCache( - private val targetDataRoot: File, + targetDataRoot: File, targetOutputDir: File?, pathConverter: FileToPathConverter ) : AbstractIncrementalCache( @@ -114,32 +113,43 @@ open class IncrementalJvmCache( } open fun saveFileToCache(generatedClass: GeneratedJvmClass, changesCollector: ChangesCollector) { - val sourceFiles: Collection = generatedClass.sourceFiles - val kotlinClass: LocalFileKotlinClass = generatedClass.outputClass - val className = kotlinClass.className + saveClassToCache(KotlinClassInfo(generatedClass.outputClass), generatedClass.sourceFiles, changesCollector) + } + + /** + * Saves information about the given (kotlinc-generated) class to this cache, and stores changes between this class and its previous + * version into the given [ChangesCollector]. + * + * @param kotlinClassInfo A kotlin-generated class + * @param sourceFiles The source files that the given class was generated from, or `null` if this information is not available + * @param changesCollector A [ChangesCollector] + */ + fun saveClassToCache(kotlinClassInfo: KotlinClassInfo, sourceFiles: List?, changesCollector: ChangesCollector) { + val className = kotlinClassInfo.className dirtyOutputClassesMap.notDirty(className) - sourceFiles.forEach { + sourceFiles?.forEach { sourceToClassesMap.add(it, className) } - internalNameToSource[className.internalName] = sourceFiles + sourceFiles?.let { internalNameToSource[className.internalName] = it } - if (kotlinClass.classId.isLocal) return + if (kotlinClassInfo.classId.isLocal) return - val header = kotlinClass.classHeader - when (header.kind) { + when (kotlinClassInfo.classKind) { KotlinClassHeader.Kind.FILE_FACADE -> { - assert(sourceFiles.size == 1) { "Package part from several source files: $sourceFiles" } + if (sourceFiles != null) { + assert(sourceFiles.size == 1) { "Package part from several source files: $sourceFiles" } + } packagePartMap.addPackagePart(className) - protoMap.process(kotlinClass, changesCollector) - constantsMap.process(kotlinClass, changesCollector) - inlineFunctionsMap.process(kotlinClass, changesCollector) + protoMap.process(kotlinClassInfo, changesCollector) + constantsMap.process(kotlinClassInfo, changesCollector) + inlineFunctionsMap.process(kotlinClassInfo, changesCollector) } KotlinClassHeader.Kind.MULTIFILE_CLASS -> { - val partNames = kotlinClass.classHeader.data?.toList() - ?: throw AssertionError("Multifile class has no parts: ${kotlinClass.className}") + val partNames = kotlinClassInfo.classHeaderData?.toList() + ?: throw AssertionError("Multifile class has no parts: $className") multifileFacadeToParts[className] = partNames // When a class is replaced with a facade with the same name, // the class' proto wouldn't ever be deleted, @@ -154,25 +164,29 @@ open class IncrementalJvmCache( internalNameToSource.remove(className.internalName) // TODO NO_CHANGES? (delegates only) - constantsMap.process(kotlinClass, changesCollector) - inlineFunctionsMap.process(kotlinClass, changesCollector) + constantsMap.process(kotlinClassInfo, changesCollector) + inlineFunctionsMap.process(kotlinClassInfo, changesCollector) } KotlinClassHeader.Kind.MULTIFILE_CLASS_PART -> { - assert(sourceFiles.size == 1) { "Multifile class part from several source files: $sourceFiles" } + if (sourceFiles != null) { + assert(sourceFiles.size == 1) { "Multifile class part from several source files: $sourceFiles" } + } packagePartMap.addPackagePart(className) - partToMultifileFacade.set(className.internalName, header.multifileClassName!!) + partToMultifileFacade.set(className.internalName, kotlinClassInfo.multifileClassName!!) - protoMap.process(kotlinClass, changesCollector) - constantsMap.process(kotlinClass, changesCollector) - inlineFunctionsMap.process(kotlinClass, changesCollector) + protoMap.process(kotlinClassInfo, changesCollector) + constantsMap.process(kotlinClassInfo, changesCollector) + inlineFunctionsMap.process(kotlinClassInfo, changesCollector) } KotlinClassHeader.Kind.CLASS -> { - assert(sourceFiles.size == 1) { "Class is expected to have only one source file: $sourceFiles" } - addToClassStorage(kotlinClass, sourceFiles.first()) + if (sourceFiles != null) { + assert(sourceFiles.size == 1) { "Class is expected to have only one source file: $sourceFiles" } + addToClassStorage(kotlinClassInfo, sourceFiles.first()) + } - protoMap.process(kotlinClass, changesCollector) - constantsMap.process(kotlinClass, changesCollector) - inlineFunctionsMap.process(kotlinClass, changesCollector) + protoMap.process(kotlinClassInfo, changesCollector) + constantsMap.process(kotlinClassInfo, changesCollector) + inlineFunctionsMap.process(kotlinClassInfo, changesCollector) } KotlinClassHeader.Kind.UNKNOWN, KotlinClassHeader.Kind.SYNTHETIC_CLASS -> { } @@ -278,8 +292,8 @@ open class IncrementalJvmCache( private inner class ProtoMap(storageFile: File) : BasicStringMap(storageFile, ProtoMapValueExternalizer) { @Synchronized - fun process(kotlinClass: LocalFileKotlinClass, changesCollector: ChangesCollector) { - return put(kotlinClass, changesCollector) + fun process(kotlinClassInfo: KotlinClassInfo, changesCollector: ChangesCollector) { + return put(kotlinClassInfo, changesCollector) } // A module mapping (.kotlin_module file) is stored in a cache, @@ -295,19 +309,17 @@ open class IncrementalJvmCache( } @Synchronized - private fun put(kotlinClass: LocalFileKotlinClass, changesCollector: ChangesCollector) { - val header = kotlinClass.classHeader - - val key = kotlinClass.className.internalName + private fun put(kotlinClassInfo: KotlinClassInfo, changesCollector: ChangesCollector) { + val key = kotlinClassInfo.className.internalName val oldData = storage[key] val newData = ProtoMapValue( - header.kind != KotlinClassHeader.Kind.CLASS, - BitEncoding.decodeBytes(header.data!!), - header.strings!! + kotlinClassInfo.classKind != KotlinClassHeader.Kind.CLASS, + BitEncoding.decodeBytes(kotlinClassInfo.classHeaderData!!), + kotlinClassInfo.classHeaderStrings!! ) storage[key] = newData - val packageFqName = kotlinClass.className.packageFqName + val packageFqName = kotlinClassInfo.className.packageFqName changesCollector.collectProtoChanges(oldData?.toProtoData(packageFqName), newData.toProtoData(packageFqName), packageProtoKey = key) } @@ -368,31 +380,16 @@ open class IncrementalJvmCache( // todo: reuse code with InlineFunctionsMap? private inner class ConstantsMap(storageFile: File) : BasicStringMap>(storageFile, ConstantsMapExternalizer) { - private fun getConstantsMap(bytes: ByteArray): Map { - val result = HashMap() - - ClassReader(bytes).accept(object : ClassVisitor(Opcodes.API_VERSION) { - override fun visitField(access: Int, name: String, desc: String, signature: String?, value: Any?): FieldVisitor? { - val staticFinal = Opcodes.ACC_STATIC or Opcodes.ACC_FINAL or Opcodes.ACC_PRIVATE - if (value != null && access and staticFinal == Opcodes.ACC_STATIC or Opcodes.ACC_FINAL) { - result[name] = value - } - return null - } - }, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES) - - return result - } operator fun contains(className: JvmClassName): Boolean = className.internalName in storage @Synchronized - fun process(kotlinClass: LocalFileKotlinClass, changesCollector: ChangesCollector) { - val key = kotlinClass.className.internalName + fun process(kotlinClassInfo: KotlinClassInfo, changesCollector: ChangesCollector) { + val key = kotlinClassInfo.className.internalName val oldMap = storage[key] ?: emptyMap() - val newMap = getConstantsMap(kotlinClass.fileContents) + val newMap = kotlinClassInfo.constantsMap if (newMap.isNotEmpty()) { storage[key] = newMap } else { @@ -401,8 +398,18 @@ open class IncrementalJvmCache( for (const in oldMap.keys + newMap.keys) { //Constant can be declared via companion object or via const field declaration - changesCollector.collectMemberIfValueWasChanged(kotlinClass.scopeFqName(companion = true), const, oldMap[const], newMap[const]) - changesCollector.collectMemberIfValueWasChanged(kotlinClass.scopeFqName(companion = false), const, oldMap[const], newMap[const]) + changesCollector.collectMemberIfValueWasChanged( + kotlinClassInfo.scopeFqName(companion = true), + const, + oldMap[const], + newMap[const] + ) + changesCollector.collectMemberIfValueWasChanged( + kotlinClassInfo.scopeFqName(companion = false), + const, + oldMap[const], + newMap[const] + ) } } @@ -490,67 +497,20 @@ open class IncrementalJvmCache( value.dumpCollection() } - private fun addToClassStorage(kotlinClass: LocalFileKotlinClass, srcFile: File) { - val (nameResolver, proto) = JvmProtoBufUtil.readClassDataFrom(kotlinClass.classHeader.data!!, kotlinClass.classHeader.strings!!) + private fun addToClassStorage(classInfo: KotlinClassInfo, srcFile: File) { + val (nameResolver, proto) = JvmProtoBufUtil.readClassDataFrom(classInfo.classHeaderData!!, classInfo.classHeaderStrings!!) addToClassStorage(proto, nameResolver, srcFile) } private inner class InlineFunctionsMap(storageFile: File) : BasicStringMap>(storageFile, StringToLongMapExternalizer) { - private fun getInlineFunctionsMap(header: KotlinClassHeader, bytes: ByteArray): Map { - val inlineFunctions = inlineFunctionsJvmNames(header) - if (inlineFunctions.isEmpty()) return emptyMap() - - val result = HashMap() - var dummyVersion: Int = -1 - ClassReader(bytes).accept(object : ClassVisitor(Opcodes.API_VERSION) { - - override fun visit( - version: Int, - access: Int, - name: String?, - signature: String?, - superName: String?, - interfaces: Array? - ) { - super.visit(version, access, name, signature, superName, interfaces) - dummyVersion = version - } - - override fun visitMethod( - access: Int, - name: String, - desc: String, - signature: String?, - exceptions: Array? - ): MethodVisitor? { - val dummyClassWriter = ClassWriter(0) - dummyClassWriter.visit(dummyVersion, 0, "dummy", null, AsmTypes.OBJECT_TYPE.internalName, null) - - return object : MethodVisitor(Opcodes.API_VERSION, dummyClassWriter.visitMethod(0, name, desc, null, exceptions)) { - override fun visitEnd() { - val jvmName = name + desc - if (jvmName !in inlineFunctions) return - - val dummyBytes = dummyClassWriter.toByteArray()!! - - val hash = dummyBytes.md5() - result[jvmName] = hash - } - } - } - - }, 0) - - return result - } @Synchronized - fun process(kotlinClass: LocalFileKotlinClass, changesCollector: ChangesCollector) { - val key = kotlinClass.className.internalName + fun process(kotlinClassInfo: KotlinClassInfo, changesCollector: ChangesCollector) { + val key = kotlinClassInfo.className.internalName val oldMap = storage[key] ?: emptyMap() - val newMap = getInlineFunctionsMap(kotlinClass.classHeader, kotlinClass.fileContents) + val newMap = kotlinClassInfo.inlineFunctionsMap if (newMap.isNotEmpty()) { storage[key] = newMap } else { @@ -559,7 +519,7 @@ open class IncrementalJvmCache( for (fn in oldMap.keys + newMap.keys) { changesCollector.collectMemberIfValueWasChanged( - kotlinClass.scopeFqName(), + kotlinClassInfo.scopeFqName(), functionNameBySignature(fn), oldMap[fn], newMap[fn] @@ -602,13 +562,6 @@ sealed class ChangeInfo(val fqName: FqName) { } } -private fun LocalFileKotlinClass.scopeFqName(companion: Boolean = false) = when (classHeader.kind) { - KotlinClassHeader.Kind.CLASS -> { - className.fqNameForClassNameWithoutDollars.let { if (companion) it.child(DEFAULT_NAME_FOR_COMPANION_OBJECT) else it } - } - else -> className.packageFqName -} - fun ByteArray.md5(): Long { val d = MessageDigest.getInstance("MD5").digest(this)!! return ((d[0].toLong() and 0xFFL) @@ -640,3 +593,105 @@ fun , V> Map.dumpMap(dumpValue: (V) -> String): String = @TestOnly fun > Collection.dumpCollection(): String = "[${sorted().joinToString(", ", transform = Any::toString)}]" + +/** + * Minimal information about a kotlinc-generated class that will be used to compute recompilation-triggered changes to support incremental + * compilation (see [IncrementalJvmCache.saveClassToCache]). + * + * It's important that this class contain only the minimal required information, as it will be part of the classpath snapshot of the + * `KotlinCompile` task and the task needs to support compile avoidance. For example, this class should contain public method signatures, + * and should not contain private method signatures, or method implementations. + */ +class KotlinClassInfo private constructor( + val classId: ClassId, + val classKind: KotlinClassHeader.Kind, + val classHeaderData: Array?, + val classHeaderStrings: Array?, + @Suppress("SpellCheckingInspection") val multifileClassName: String?, + val constantsMap: LinkedHashMap, + val inlineFunctionsMap: LinkedHashMap +) { + + constructor(kotlinClass: LocalFileKotlinClass) : this( + kotlinClass.classId, + kotlinClass.classHeader.kind, + kotlinClass.classHeader.data, + kotlinClass.classHeader.strings, + kotlinClass.classHeader.multifileClassName, + getConstantsMap(kotlinClass.fileContents), + getInlineFunctionsMap(kotlinClass.classHeader, kotlinClass.fileContents) + ) + + val className: JvmClassName by lazy { JvmClassName.byClassId(classId) } + + fun scopeFqName(companion: Boolean = false) = when (classKind) { + KotlinClassHeader.Kind.CLASS -> { + className.fqNameForClassNameWithoutDollars.let { if (companion) it.child(DEFAULT_NAME_FOR_COMPANION_OBJECT) else it } + } + else -> className.packageFqName + } +} + +private fun getConstantsMap(bytes: ByteArray): LinkedHashMap { + val result = LinkedHashMap() + + ClassReader(bytes).accept(object : ClassVisitor(Opcodes.API_VERSION) { + override fun visitField(access: Int, name: String, desc: String, signature: String?, value: Any?): FieldVisitor? { + val staticFinal = Opcodes.ACC_STATIC or Opcodes.ACC_FINAL or Opcodes.ACC_PRIVATE + if (value != null && access and staticFinal == Opcodes.ACC_STATIC or Opcodes.ACC_FINAL) { + result[name] = value + } + return null + } + }, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES) + + return result +} + +private fun getInlineFunctionsMap(header: KotlinClassHeader, bytes: ByteArray): LinkedHashMap { + val inlineFunctions = inlineFunctionsJvmNames(header) + if (inlineFunctions.isEmpty()) return LinkedHashMap() + + val result = LinkedHashMap() + var dummyVersion: Int = -1 + ClassReader(bytes).accept(object : ClassVisitor(Opcodes.API_VERSION) { + + override fun visit( + version: Int, + access: Int, + name: String?, + signature: String?, + superName: String?, + interfaces: Array? + ) { + super.visit(version, access, name, signature, superName, interfaces) + dummyVersion = version + } + + override fun visitMethod( + access: Int, + name: String, + desc: String, + signature: String?, + exceptions: Array? + ): MethodVisitor { + val dummyClassWriter = ClassWriter(0) + dummyClassWriter.visit(dummyVersion, 0, "dummy", null, AsmTypes.OBJECT_TYPE.internalName, null) + + return object : MethodVisitor(Opcodes.API_VERSION, dummyClassWriter.visitMethod(0, name, desc, null, exceptions)) { + override fun visitEnd() { + val jvmName = name + desc + if (jvmName !in inlineFunctions) return + + val dummyBytes = dummyClassWriter.toByteArray()!! + + val hash = dummyBytes.md5() + result[jvmName] = hash + } + } + } + + }, 0) + + return result +} \ No newline at end of file