KT-45777: Refactor IncrementalJvmCache to support classpath snapshotting

Extract minimal required information into the new `KotlinClassInfo`
class. This info will be part of the classpath snapshot of the
`KotlinCompile` task in an upcoming change.

Bug: KT-45777
Test: Existing tests should pass (this is a refactoring-only change)
This commit is contained in:
Hung Nguyen
2021-07-19 15:41:06 +01:00
committed by nataliya.valtman
parent 19467861c9
commit f95c50064c
@@ -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<JvmClassName>(
@@ -114,32 +113,43 @@ open class IncrementalJvmCache(
}
open fun saveFileToCache(generatedClass: GeneratedJvmClass, changesCollector: ChangesCollector) {
val sourceFiles: Collection<File> = 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<File>?, 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<ProtoMapValue>(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<Map<String, Any>>(storageFile, ConstantsMapExternalizer) {
private fun getConstantsMap(bytes: ByteArray): Map<String, Any> {
val result = HashMap<String, Any>()
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<Map<String, Long>>(storageFile, StringToLongMapExternalizer) {
private fun getInlineFunctionsMap(header: KotlinClassHeader, bytes: ByteArray): Map<String, Long> {
val inlineFunctions = inlineFunctionsJvmNames(header)
if (inlineFunctions.isEmpty()) return emptyMap()
val result = HashMap<String, Long>()
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<out String>?
) {
super.visit(version, access, name, signature, superName, interfaces)
dummyVersion = version
}
override fun visitMethod(
access: Int,
name: String,
desc: String,
signature: String?,
exceptions: Array<out String>?
): 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 <K : Comparable<K>, V> Map<K, V>.dumpMap(dumpValue: (V) -> String): String =
@TestOnly
fun <T : Comparable<T>> Collection<T>.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<String>?,
val classHeaderStrings: Array<String>?,
@Suppress("SpellCheckingInspection") val multifileClassName: String?,
val constantsMap: LinkedHashMap<String, Any>,
val inlineFunctionsMap: LinkedHashMap<String, Long>
) {
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<String, Any> {
val result = LinkedHashMap<String, Any>()
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<String, Long> {
val inlineFunctions = inlineFunctionsJvmNames(header)
if (inlineFunctions.isEmpty()) return LinkedHashMap()
val result = LinkedHashMap<String, Long>()
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<out String>?
) {
super.visit(version, access, name, signature, superName, interfaces)
dummyVersion = version
}
override fun visitMethod(
access: Int,
name: String,
desc: String,
signature: String?,
exceptions: Array<out String>?
): 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
}