KT-45777: Move classpath diffing to incremental Kotlin compiler (2/2)

as we need access to the lookup tracker to compute classpath changes
more efficiently and reduce the size of the saved classpath snapshot.

The previous commit only changed the files' paths, this
commit actually updates the files' contents.

Note that classpath snapshotting still happens in Gradle artifact
transforms. (However, the previous commit also moved the code for
classpath snapshotting together with the code for classpath diffing as
they are closely related.)
This commit is contained in:
Hung Nguyen
2021-11-10 10:57:02 +00:00
committed by teamcityserver
parent dfaf195e1d
commit f52be5f471
25 changed files with 659 additions and 491 deletions
@@ -18,6 +18,7 @@ enum class BuildAttributeKind : Serializable {
enum class BuildAttribute(val kind: BuildAttributeKind, val readableString: String) : Serializable { enum class BuildAttribute(val kind: BuildAttributeKind, val readableString: String) : Serializable {
NO_BUILD_HISTORY(BuildAttributeKind.REBUILD_REASON, "Build history file not found"), NO_BUILD_HISTORY(BuildAttributeKind.REBUILD_REASON, "Build history file not found"),
NO_ABI_SNAPSHOT(BuildAttributeKind.REBUILD_REASON, "ABI snapshot not found"), NO_ABI_SNAPSHOT(BuildAttributeKind.REBUILD_REASON, "ABI snapshot not found"),
CLASSPATH_SNAPSHOT_NOT_FOUND(BuildAttributeKind.REBUILD_REASON, "Classpath snapshot not found"),
CACHE_CORRUPTION(BuildAttributeKind.REBUILD_REASON, "Cache corrupted"), CACHE_CORRUPTION(BuildAttributeKind.REBUILD_REASON, "Cache corrupted"),
UNKNOWN_CHANGES_IN_GRADLE_INPUTS(BuildAttributeKind.REBUILD_REASON, "Unknown Gradle changes"), UNKNOWN_CHANGES_IN_GRADLE_INPUTS(BuildAttributeKind.REBUILD_REASON, "Unknown Gradle changes"),
JAVA_CHANGE_UNTRACKED_FILE_IS_REMOVED(BuildAttributeKind.REBUILD_REASON, "Untracked Java file is removed"), JAVA_CHANGE_UNTRACKED_FILE_IS_REMOVED(BuildAttributeKind.REBUILD_REASON, "Untracked Java file is removed"),
@@ -10,9 +10,13 @@ import java.io.Serializable
@Suppress("Reformat") @Suppress("Reformat")
enum class BuildPerformanceMetric(val parent: BuildPerformanceMetric? = null, val readableString: String) : Serializable { enum class BuildPerformanceMetric(val parent: BuildPerformanceMetric? = null, val readableString: String) : Serializable {
OUTPUT_SIZE(readableString = "Total output size"), CACHE_DIRECTORY_SIZE(readableString = "Total size of the cache directory"),
LOOKUP_SIZE(OUTPUT_SIZE, "Lookups size"), LOOKUP_SIZE(CACHE_DIRECTORY_SIZE, "Lookups size"),
SNAPSHOT_SIZE(OUTPUT_SIZE, "ABI snapshot size"), SNAPSHOT_SIZE(CACHE_DIRECTORY_SIZE, "ABI snapshot size"),
// Metrics for the `kotlin.incremental.useClasspathSnapshot` feature
ORIGINAL_CLASSPATH_SNAPSHOT_SIZE(parent = null, "Size of the original classpath snapshot (before shrinking)"),
SHRUNK_CLASSPATH_SNAPSHOT_SIZE(parent = null, "Size of the shrunk classpath snapshot"),
; ;
companion object { companion object {
@@ -27,6 +27,15 @@ enum class BuildTime(val parent: BuildTime? = null, val readableString: String)
SET_UP_ABI_SNAPSHOTS(JAR_SNAPSHOT, "Set up ABI snapshot"), SET_UP_ABI_SNAPSHOTS(JAR_SNAPSHOT, "Set up ABI snapshot"),
IC_ANALYZE_JAR_FILES(JAR_SNAPSHOT, "Analyze jar files"), IC_ANALYZE_JAR_FILES(JAR_SNAPSHOT, "Analyze jar files"),
IC_CALCULATE_INITIAL_DIRTY_SET(INCREMENTAL_COMPILATION, "Init dirty symbols set"), IC_CALCULATE_INITIAL_DIRTY_SET(INCREMENTAL_COMPILATION, "Init dirty symbols set"),
COMPUTE_CLASSPATH_CHANGES(IC_CALCULATE_INITIAL_DIRTY_SET, "Compute classpath changes"),
LOAD_CURRENT_CLASSPATH_SNAPSHOT(COMPUTE_CLASSPATH_CHANGES, "Load current classpath snapshot"),
SHRINK_CURRENT_CLASSPATH_SNAPSHOT(COMPUTE_CLASSPATH_CHANGES, "Shrink current classpath snapshot"),
LOAD_SHRUNK_PREVIOUS_CLASSPATH_SNAPSHOT(COMPUTE_CLASSPATH_CHANGES, "Load shrunk previous classpath snapshot"),
COMPUTE_CHANGED_AND_IMPACTED_SET(COMPUTE_CLASSPATH_CHANGES, "Compute changed and impacted set"),
COMPUTE_CLASS_CHANGES(COMPUTE_CHANGED_AND_IMPACTED_SET, "Compute class changes"),
COMPUTE_KOTLIN_CLASS_CHANGES(COMPUTE_CLASS_CHANGES, "Compute Kotlin class changes"),
COMPUTE_JAVA_CLASS_CHANGES(COMPUTE_CLASS_CHANGES, "Compute Java class changes"),
COMPUTE_IMPACTED_SET(COMPUTE_CHANGED_AND_IMPACTED_SET, "Compute impacted set"),
IC_ANALYZE_CHANGES_IN_DEPENDENCIES(IC_CALCULATE_INITIAL_DIRTY_SET, "Analyze dependency changes"), IC_ANALYZE_CHANGES_IN_DEPENDENCIES(IC_CALCULATE_INITIAL_DIRTY_SET, "Analyze dependency changes"),
IC_FIND_HISTORY_FILES(IC_ANALYZE_CHANGES_IN_DEPENDENCIES, "Find history files"), IC_FIND_HISTORY_FILES(IC_ANALYZE_CHANGES_IN_DEPENDENCIES, "Find history files"),
IC_ANALYZE_HISTORY_FILES(IC_ANALYZE_CHANGES_IN_DEPENDENCIES, "Analyze history files"), IC_ANALYZE_HISTORY_FILES(IC_ANALYZE_CHANGES_IN_DEPENDENCIES, "Analyze history files"),
@@ -38,6 +47,14 @@ enum class BuildTime(val parent: BuildTime? = null, val readableString: String)
INCREMENTAL_ITERATION(INCREMENTAL_COMPILATION, "Incremental iteration"), INCREMENTAL_ITERATION(INCREMENTAL_COMPILATION, "Incremental iteration"),
NON_INCREMENTAL_ITERATION(INCREMENTAL_COMPILATION, "Non-incremental iteration"), NON_INCREMENTAL_ITERATION(INCREMENTAL_COMPILATION, "Non-incremental iteration"),
IC_WRITE_HISTORY_FILE(INCREMENTAL_COMPILATION, "Write history file"), IC_WRITE_HISTORY_FILE(INCREMENTAL_COMPILATION, "Write history file"),
SAVE_SHRUNK_CURRENT_CLASSPATH_SNAPSHOT_AFTER_COMPILATION(INCREMENTAL_COMPILATION, "Save shrunk current classpath snapshot after compilation"),
LOAD_CLASSPATH_SNAPSHOT(SAVE_SHRUNK_CURRENT_CLASSPATH_SNAPSHOT_AFTER_COMPILATION, "Load classpath snapshot"),
SHRINK_CLASSPATH_SNAPSHOT(SAVE_SHRUNK_CURRENT_CLASSPATH_SNAPSHOT_AFTER_COMPILATION, "Shrink classpath snapshot"),
GET_NON_DUPLICATE_CLASSES(SHRINK_CLASSPATH_SNAPSHOT, "Get non-duplicate classes"),
GET_LOOKUP_SYMBOLS(SHRINK_CLASSPATH_SNAPSHOT, "Get lookup symbols"),
FIND_REFERENCED_CLASSES(SHRINK_CLASSPATH_SNAPSHOT, "Find referenced classes"),
FIND_TRANSITIVELY_REFERENCED_CLASSES(SHRINK_CLASSPATH_SNAPSHOT, "Find transitively referenced classes"),
SAVE_SHRUNK_CLASSPATH_SNAPSHOT(SAVE_SHRUNK_CURRENT_CLASSPATH_SNAPSHOT_AFTER_COMPILATION, "Save shrunk classpath snapshot"),
COMPILER_PERFORMANCE(readableString = "Compiler time"), COMPILER_PERFORMANCE(readableString = "Compiler time"),
COMPILER_INITIALIZATION(COMPILER_PERFORMANCE, "Compiler initialization time"), COMPILER_INITIALIZATION(COMPILER_PERFORMANCE, "Compiler initialization time"),
CODE_ANALYSIS(COMPILER_PERFORMANCE, "Compiler code analyse"), CODE_ANALYSIS(COMPILER_PERFORMANCE, "Compiler code analyse"),
@@ -5,70 +5,43 @@
package org.jetbrains.kotlin.incremental package org.jetbrains.kotlin.incremental
import com.intellij.util.io.DataExternalizer import org.jetbrains.kotlin.incremental.ClasspathChanges.ClasspathSnapshotEnabled
import org.jetbrains.kotlin.incremental.storage.FqNameExternalizer import java.io.File
import org.jetbrains.kotlin.incremental.storage.SetExternalizer import java.io.Serializable
import org.jetbrains.kotlin.incremental.storage.LookupSymbolExternalizer
import org.jetbrains.kotlin.name.FqName
import java.io.*
/** /**
* Changes to the classpath of the `KotlinCompile` task, used to compute the source files that need to be recompiled during an incremental * Changes to the classpath of the `KotlinCompile` task, or information to compute them later by the Kotlin incremental compiler (see
* run. * [ClasspathSnapshotEnabled.ToBeComputedByIncrementalCompiler].
*/ */
sealed class ClasspathChanges : Serializable { sealed class ClasspathChanges : Serializable {
class Available() : ClasspathChanges() { sealed class ClasspathSnapshotEnabled : ClasspathChanges() {
companion object { abstract val classpathSnapshotFiles: ClasspathSnapshotFiles
private const val serialVersionUID = 0L
}
lateinit var lookupSymbols: Set<LookupSymbol> // Preferably ordered but not required class Empty(override val classpathSnapshotFiles: ClasspathSnapshotFiles) : ClasspathSnapshotEnabled()
private set
lateinit var fqNames: Set<FqName> // Preferably ordered but not required class ToBeComputedByIncrementalCompiler(override val classpathSnapshotFiles: ClasspathSnapshotFiles) : ClasspathSnapshotEnabled()
private set
constructor(lookupSymbols: Set<LookupSymbol>, fqNames: Set<FqName>) : this() { class NotAvailableDueToMissingClasspathSnapshot(override val classpathSnapshotFiles: ClasspathSnapshotFiles) :
this.lookupSymbols = lookupSymbols ClasspathSnapshotEnabled()
this.fqNames = fqNames
}
private fun writeObject(output: ObjectOutputStream) { class NotAvailableForNonIncrementalRun(override val classpathSnapshotFiles: ClasspathSnapshotFiles) : ClasspathSnapshotEnabled()
// Can't close DataOutputStream below as it will also close the underlying ObjectOutputStream, which is still in use.
ClasspathChangesAvailableExternalizer.save(DataOutputStream(output), this)
}
private fun readObject(input: ObjectInputStream) {
// Can't close DataInputStream below as it will also close the underlying ObjectInputStream, which is still in use.
ClasspathChangesAvailableExternalizer.read(DataInputStream(input)).also {
lookupSymbols = it.lookupSymbols
fqNames = it.fqNames
}
}
} }
sealed class NotAvailable : ClasspathChanges() { object ClasspathSnapshotDisabled : ClasspathChanges()
object UnableToCompute : NotAvailable()
object ForNonIncrementalRun : NotAvailable() object NotAvailableForJSCompiler : ClasspathChanges()
object ClasspathSnapshotIsDisabled : NotAvailable()
object ReservedForTestsOnly : NotAvailable()
object ForJSCompiler : NotAvailable()
}
} }
private object ClasspathChangesAvailableExternalizer : DataExternalizer<ClasspathChanges.Available> { class ClasspathSnapshotFiles(
val currentClasspathEntrySnapshotFiles: List<File>,
classpathSnapshotDir: File
) : Serializable {
override fun save(output: DataOutput, classpathChanges: ClasspathChanges.Available) { val shrunkPreviousClasspathSnapshotFile: File = File(classpathSnapshotDir, "shrunk-classpath-snapshot.bin")
SetExternalizer(LookupSymbolExternalizer).save(output, classpathChanges.lookupSymbols)
SetExternalizer(FqNameExternalizer).save(output, classpathChanges.fqNames)
}
override fun read(input: DataInput): ClasspathChanges.Available { companion object {
return ClasspathChanges.Available( private const val serialVersionUID = 0L
lookupSymbols = SetExternalizer(LookupSymbolExternalizer).read(input),
fqNames = SetExternalizer(FqNameExternalizer).read(input)
)
} }
} }
@@ -32,7 +32,8 @@ import java.util.*
open class LookupStorage( open class LookupStorage(
targetDataDir: File, targetDataDir: File,
pathConverter: FileToPathConverter pathConverter: FileToPathConverter,
storeFullFqNames: Boolean = false
) : BasicMapsOwner(targetDataDir) { ) : BasicMapsOwner(targetDataDir) {
val LOG = Logger.getInstance("#org.jetbrains.kotlin.jps.build.KotlinBuilder") val LOG = Logger.getInstance("#org.jetbrains.kotlin.jps.build.KotlinBuilder")
@@ -44,7 +45,7 @@ open class LookupStorage(
private val countersFile = "counters".storageFile private val countersFile = "counters".storageFile
private val idToFile = registerMap(IdToFileMap("id-to-file".storageFile, pathConverter)) private val idToFile = registerMap(IdToFileMap("id-to-file".storageFile, pathConverter))
private val fileToId = registerMap(FileToIdMap("file-to-id".storageFile, pathConverter)) private val fileToId = registerMap(FileToIdMap("file-to-id".storageFile, pathConverter))
val lookupMap = registerMap(LookupMap("lookups".storageFile)) val lookupMap = registerMap(LookupMap("lookups".storageFile, storeFullFqNames))
@Volatile @Volatile
private var size: Int = 0 private var size: Int = 0
@@ -18,7 +18,9 @@ package org.jetbrains.kotlin.incremental.storage
import java.io.File import java.io.File
class LookupMap(storage: File) : BasicMap<LookupSymbolKey, Collection<Int>>(storage, LookupSymbolKeyDescriptor, IntCollectionExternalizer) { class LookupMap(storage: File, storeFullFqNames: Boolean) :
BasicMap<LookupSymbolKey, Collection<Int>>(storage, LookupSymbolKeyDescriptor(storeFullFqNames), IntCollectionExternalizer) {
override fun dumpKey(key: LookupSymbolKey): String = key.toString() override fun dumpKey(key: LookupSymbolKey): String = key.toString()
override fun dumpValue(value: Collection<Int>): String = value.toString() override fun dumpValue(value: Collection<Int>): String = value.toString()
@@ -22,22 +22,21 @@ import com.intellij.util.io.DataExternalizer
import com.intellij.util.io.EnumeratorStringDescriptor import com.intellij.util.io.EnumeratorStringDescriptor
import com.intellij.util.io.IOUtil import com.intellij.util.io.IOUtil
import com.intellij.util.io.KeyDescriptor import com.intellij.util.io.KeyDescriptor
import org.jetbrains.kotlin.cli.common.CompilerSystemProperties
import org.jetbrains.kotlin.cli.common.toBooleanLenient
import org.jetbrains.kotlin.incremental.LookupSymbol
import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.resolve.jvm.JvmClassName import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import java.io.DataInput import java.io.*
import java.io.DataInputStream
import java.io.DataOutput
/** /**
* Storage versioning: * Storage versioning:
* 0 - only name and value hashes are saved * 0 - only name and value hashes are saved
* 1 - name and scope are saved * 1 - name and scope are saved
*/ */
object LookupSymbolKeyDescriptor : KeyDescriptor<LookupSymbolKey> { class LookupSymbolKeyDescriptor(
/** If `true`, original values are saved; if `false`, only hashes are saved. */
private val storeFullFqNames: Boolean = false
) : KeyDescriptor<LookupSymbolKey> {
override fun read(input: DataInput): LookupSymbolKey { override fun read(input: DataInput): LookupSymbolKey {
val version = input.readByte() val version = input.readByte()
return when (version.toInt()) { return when (version.toInt()) {
@@ -51,14 +50,12 @@ object LookupSymbolKeyDescriptor : KeyDescriptor<LookupSymbolKey> {
val second = input.readInt() val second = input.readInt()
LookupSymbolKey(first, second, "", "") LookupSymbolKey(first, second, "", "")
} }
else -> throw RuntimeException("Unknown version of LookupSymbolKeyDescriptor=${version}") else -> throw IllegalArgumentException("Unknown version of LookupSymbolKeyDescriptor=${version}")
} }
} }
private val storeFullFqName = CompilerSystemProperties.COMPILE_INCREMENTAL_WITH_CLASSPATH_SNAPSHOTS.value.toBooleanLenient() ?: false
override fun save(output: DataOutput, value: LookupSymbolKey) { override fun save(output: DataOutput, value: LookupSymbolKey) {
if (storeFullFqName) { if (storeFullFqNames) {
output.writeByte(0) output.writeByte(0)
output.writeUTF(value.name) output.writeUTF(value.name)
output.writeUTF(value.scope) output.writeUTF(value.scope)
@@ -74,18 +71,6 @@ object LookupSymbolKeyDescriptor : KeyDescriptor<LookupSymbolKey> {
override fun isEqual(val1: LookupSymbolKey, val2: LookupSymbolKey): Boolean = val1 == val2 override fun isEqual(val1: LookupSymbolKey, val2: LookupSymbolKey): Boolean = val1 == val2
} }
object LookupSymbolExternalizer : DataExternalizer<LookupSymbol> {
override fun save(output: DataOutput, lookupSymbol: LookupSymbol) {
output.writeString(lookupSymbol.name)
output.writeString(lookupSymbol.scope)
}
override fun read(input: DataInput): LookupSymbol {
return LookupSymbol(name = input.readString(), scope = input.readString())
}
}
object FqNameExternalizer : DataExternalizer<FqName> { object FqNameExternalizer : DataExternalizer<FqName> {
override fun save(output: DataOutput, fqName: FqName) { override fun save(output: DataOutput, fqName: FqName) {
@@ -226,6 +211,32 @@ object ConstantExternalizer : DataExternalizer<Any> {
} }
} }
fun <T> DataExternalizer<T>.saveToFile(file: File, value: T) {
return DataOutputStream(FileOutputStream(file).buffered()).use {
save(it, value)
}
}
fun <T> DataExternalizer<T>.loadFromFile(file: File): T {
return DataInputStream(FileInputStream(file).buffered()).use {
read(it)
}
}
fun <T> DataExternalizer<T>.toByteArray(value: T): ByteArray {
val byteArrayOutputStream = ByteArrayOutputStream()
DataOutputStream(byteArrayOutputStream.buffered()).use {
save(it, value)
}
return byteArrayOutputStream.toByteArray()
}
fun <T> DataExternalizer<T>.fromByteArray(byteArray: ByteArray): T {
return DataInputStream(ByteArrayInputStream(byteArray).buffered()).use {
read(it)
}
}
object IntExternalizer : DataExternalizer<Int> { object IntExternalizer : DataExternalizer<Int> {
override fun save(output: DataOutput, value: Int) = output.writeInt(value) override fun save(output: DataOutput, value: Int) = output.writeInt(value)
override fun read(input: DataInput): Int = input.readInt() override fun read(input: DataInput): Int = input.readInt()
@@ -7,6 +7,8 @@ package org.jetbrains.kotlin.incremental.storage
import com.intellij.util.containers.MultiMap import com.intellij.util.containers.MultiMap
import org.jetbrains.kotlin.TestWithWorkingDir import org.jetbrains.kotlin.TestWithWorkingDir
import org.jetbrains.kotlin.cli.common.CompilerSystemProperties
import org.jetbrains.kotlin.cli.common.toBooleanLenient
import org.jetbrains.kotlin.incremental.LookupStorage import org.jetbrains.kotlin.incremental.LookupStorage
import org.jetbrains.kotlin.incremental.LookupSymbol import org.jetbrains.kotlin.incremental.LookupSymbol
import org.jetbrains.kotlin.incremental.testingUtils.assertEqualDirectories import org.jetbrains.kotlin.incremental.testingUtils.assertEqualDirectories
@@ -48,7 +50,11 @@ class RelocatableCachesTest : TestWithWorkingDir() {
private fun fillLookupStorage(projectRoot: File, reverseFiles: Boolean, reverseLookups: Boolean) { private fun fillLookupStorage(projectRoot: File, reverseFiles: Boolean, reverseLookups: Boolean) {
val storageRoot = projectRoot.storageRoot val storageRoot = projectRoot.storageRoot
val fileToPathConverter = RelativeFileToPathConverter(projectRoot) val fileToPathConverter = RelativeFileToPathConverter(projectRoot)
val lookupStorage = LookupStorage(storageRoot, fileToPathConverter) val lookupStorage = LookupStorage(
storageRoot,
fileToPathConverter,
storeFullFqNames = CompilerSystemProperties.COMPILE_INCREMENTAL_WITH_CLASSPATH_SNAPSHOTS.value.toBooleanLenient() ?: false
)
val files = LinkedHashSet<String>() val files = LinkedHashSet<String>()
val symbols = LinkedHashSet<LookupSymbol>() val symbols = LinkedHashSet<LookupSymbol>()
val lookups = MultiMap.createOrderedSet<LookupSymbol, String>() val lookups = MultiMap.createOrderedSet<LookupSymbol, String>()
@@ -15,6 +15,7 @@ dependencies {
api(project(":compiler:cli-js")) api(project(":compiler:cli-js"))
api(project(":kotlin-build-common")) api(project(":kotlin-build-common"))
api(project(":daemon-common")) api(project(":daemon-common"))
implementation("com.google.code.gson:gson:${rootProject.extra["versions.jar.gson"]}")
compileOnly(intellijCoreDep()) { includeJars("intellij-core") } compileOnly(intellijCoreDep()) { includeJars("intellij-core") }
testApi(commonDep("junit:junit")) testApi(commonDep("junit:junit"))
@@ -26,8 +26,8 @@ import java.io.File
abstract class IncrementalCachesManager<PlatformCache : AbstractIncrementalCache<*>>( abstract class IncrementalCachesManager<PlatformCache : AbstractIncrementalCache<*>>(
cachesRootDir: File, cachesRootDir: File,
rootProjectDir: File?, rootProjectDir: File?,
protected val reporter: ICReporter protected val reporter: ICReporter,
) { storeFullFqNamesInLookupCache: Boolean = false) {
val pathConverter = IncrementalFileToPathConverter(rootProjectDir) val pathConverter = IncrementalFileToPathConverter(rootProjectDir)
private val caches = arrayListOf<BasicMapsOwner>() private val caches = arrayListOf<BasicMapsOwner>()
@@ -43,7 +43,7 @@ abstract class IncrementalCachesManager<PlatformCache : AbstractIncrementalCache
private val lookupCacheDir = File(cachesRootDir, "lookups").apply { mkdirs() } private val lookupCacheDir = File(cachesRootDir, "lookups").apply { mkdirs() }
val inputsCache: InputsCache = InputsCache(inputSnapshotsCacheDir, reporter, pathConverter).apply { registerCache() } val inputsCache: InputsCache = InputsCache(inputSnapshotsCacheDir, reporter, pathConverter).apply { registerCache() }
val lookupCache: LookupStorage = LookupStorage(lookupCacheDir, pathConverter).apply { registerCache() } val lookupCache: LookupStorage = LookupStorage(lookupCacheDir, pathConverter, storeFullFqNamesInLookupCache).apply { registerCache() }
abstract val platformCache: PlatformCache abstract val platformCache: PlatformCache
@Synchronized @Synchronized
@@ -79,8 +79,9 @@ class IncrementalJvmCachesManager(
cacheDirectory: File, cacheDirectory: File,
rootProjectDir: File?, rootProjectDir: File?,
outputDir: File, outputDir: File,
reporter: ICReporter reporter: ICReporter,
) : IncrementalCachesManager<IncrementalJvmCache>(cacheDirectory, rootProjectDir, reporter) { storeFullFqNamesInLookupCache: Boolean = false
) : IncrementalCachesManager<IncrementalJvmCache>(cacheDirectory, rootProjectDir, reporter, storeFullFqNamesInLookupCache) {
private val jvmCacheDir = File(cacheDirectory, "jvm").apply { mkdirs() } private val jvmCacheDir = File(cacheDirectory, "jvm").apply { mkdirs() }
override val platformCache = IncrementalJvmCache(jvmCacheDir, outputDir, pathConverter).apply { registerCache() } override val platformCache = IncrementalJvmCache(jvmCacheDir, outputDir, pathConverter).apply { registerCache() }
} }
@@ -89,8 +90,9 @@ class IncrementalJsCachesManager(
cachesRootDir: File, cachesRootDir: File,
rootProjectDir: File?, rootProjectDir: File?,
reporter: ICReporter, reporter: ICReporter,
serializerProtocol: SerializerExtensionProtocol serializerProtocol: SerializerExtensionProtocol,
) : IncrementalCachesManager<IncrementalJsCache>(cachesRootDir, rootProjectDir, reporter) { storeFullFqNamesInLookupCache: Boolean
) : IncrementalCachesManager<IncrementalJsCache>(cachesRootDir, rootProjectDir, reporter, storeFullFqNamesInLookupCache) {
private val jsCacheFile = File(cachesRootDir, "js").apply { mkdirs() } private val jsCacheFile = File(cachesRootDir, "js").apply { mkdirs() }
override val platformCache = IncrementalJsCache(jsCacheFile, pathConverter, serializerProtocol).apply { registerCache() } override val platformCache = IncrementalJsCache(jsCacheFile, pathConverter, serializerProtocol).apply { registerCache() }
} }
@@ -19,9 +19,9 @@ package org.jetbrains.kotlin.incremental
import org.jetbrains.kotlin.build.DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS import org.jetbrains.kotlin.build.DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS
import org.jetbrains.kotlin.build.GeneratedFile import org.jetbrains.kotlin.build.GeneratedFile
import org.jetbrains.kotlin.build.report.BuildReporter import org.jetbrains.kotlin.build.report.BuildReporter
import org.jetbrains.kotlin.build.report.metrics.BuildTime
import org.jetbrains.kotlin.build.report.metrics.BuildAttribute import org.jetbrains.kotlin.build.report.metrics.BuildAttribute
import org.jetbrains.kotlin.build.report.metrics.BuildPerformanceMetric import org.jetbrains.kotlin.build.report.metrics.BuildPerformanceMetric
import org.jetbrains.kotlin.build.report.metrics.BuildTime
import org.jetbrains.kotlin.build.report.metrics.measure import org.jetbrains.kotlin.build.report.metrics.measure
import org.jetbrains.kotlin.cli.common.* import org.jetbrains.kotlin.cli.common.*
import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments
@@ -39,8 +39,6 @@ import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.progress.CompilationCanceledStatus import org.jetbrains.kotlin.progress.CompilationCanceledStatus
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.*
import kotlin.collections.HashMap
abstract class IncrementalCompilerRunner< abstract class IncrementalCompilerRunner<
Args : CommonCompilerArguments, Args : CommonCompilerArguments,
@@ -108,7 +106,7 @@ abstract class IncrementalCompilerRunner<
caches.close(false) caches.close(false)
// todo: we can recompile all files incrementally (not cleaning caches), so rebuild won't propagate // todo: we can recompile all files incrementally (not cleaning caches), so rebuild won't propagate
reporter.measure(BuildTime.CLEAR_OUTPUT_ON_REBUILD) { reporter.measure(BuildTime.CLEAR_OUTPUT_ON_REBUILD) {
clearLocalStateOnRebuild(args) clearOutputsOnRebuild(args)
} }
caches = createCacheManager(args, projectDir) caches = createCacheManager(args, projectDir)
if (providedChangedFiles == null) { if (providedChangedFiles == null) {
@@ -134,7 +132,6 @@ abstract class IncrementalCompilerRunner<
else -> providedChangedFiles else -> providedChangedFiles
} }
val compilationMode = sourcesToCompile(caches, changedFiles, args, messageCollector, classpathAbiSnapshot) val compilationMode = sourcesToCompile(caches, changedFiles, args, messageCollector, classpathAbiSnapshot)
val exitCode = when (compilationMode) { val exitCode = when (compilationMode) {
@@ -162,7 +159,8 @@ abstract class IncrementalCompilerRunner<
allSourceFiles, allSourceFiles,
compilationMode, compilationMode,
messageCollector, messageCollector,
withSnapshot) withSnapshot
)
} }
} }
is CompilationMode.Rebuild -> { is CompilationMode.Rebuild -> {
@@ -170,6 +168,8 @@ abstract class IncrementalCompilerRunner<
} }
} }
performWorkAfterCompilation(caches)
if (!caches.close(flush = true)) throw RuntimeException("Could not flush caches") if (!caches.close(flush = true)) throw RuntimeException("Could not flush caches")
// Here we should analyze exit code of compiler. E.g. compiler failure should lead to caches rebuild, // Here we should analyze exit code of compiler. E.g. compiler failure should lead to caches rebuild,
// but now JsKlib compiler reports invalid exit code. // but now JsKlib compiler reports invalid exit code.
@@ -182,7 +182,7 @@ abstract class IncrementalCompilerRunner<
) )
if (cacheDirectory.exists() && cacheDirectory.isDirectory()) { if (cacheDirectory.exists() && cacheDirectory.isDirectory()) {
cacheDirectory.walkTopDown().filter { it.isFile }.map { it.length() }.sum().let { cacheDirectory.walkTopDown().filter { it.isFile }.map { it.length() }.sum().let {
reporter.addMetric(BuildPerformanceMetric.OUTPUT_SIZE, it) reporter.addMetric(BuildPerformanceMetric.CACHE_DIRECTORY_SIZE, it)
} }
} }
} }
@@ -193,37 +193,42 @@ abstract class IncrementalCompilerRunner<
rebuild(BuildAttribute.CACHE_CORRUPTION) rebuild(BuildAttribute.CACHE_CORRUPTION)
} finally { } finally {
if (cachesMayBeCorrupted) { if (cachesMayBeCorrupted) {
clearLocalStateOnRebuild(args) clearOutputsOnRebuild(args)
} }
} }
} }
private fun clearLocalStateOnRebuild(args: Args) { /**
* Deletes output files and contents of output directories on rebuild (including `@LocalState` files/directories).
*
* If the output directories do not yet exist, they will be created.
*/
private fun clearOutputsOnRebuild(args: Args) {
val destinationDir = destinationDir(args) val destinationDir = destinationDir(args)
val (outputDirsThatExist, regularOrNonExistentOutputFiles) = outputFiles.partition { it.isDirectory }
val regularOutputFiles = regularOrNonExistentOutputFiles.filter { it.exists() }
reporter.reportVerbose { "Clearing output on rebuild" } // outputDirsThatExist may or may not contain destinationDir and workingDir.
for (file in sequenceOf(destinationDir, workingDir) + outputFiles.asSequence()) { // Collect all of them so that we don't miss any output directories.
val deleted: Boolean? = when { // Use Set to avoid duplication.
file.isDirectory -> { val allOutputDirs = setOf(destinationDir, workingDir) + outputDirsThatExist
reporter.reportVerbose { " Deleting directory $file" }
file.deleteRecursively() reporter.reportVerbose { "Clearing outputs on rebuild" }
} allOutputDirs.forEach { dir ->
file.isFile -> { reporter.reportVerbose { " Deleting contents of directory '${dir.path}'" }
reporter.reportVerbose { " Deleting $file" } dir.listFiles()?.forEach {
file.delete() it.deleteRecursively()
} if (it.exists()) throw IOException("Could not delete '${it.path}'")
else -> null
} }
if (deleted == false) { dir.mkdirs()
reporter.reportVerbose { " Could not delete $file" } if (!dir.exists()) throw IOException("Could not create directory '${dir.path}'")
} }
regularOutputFiles.forEach { file ->
reporter.reportVerbose { " Deleting file '${file.path}'" }
file.delete()
if (file.exists()) throw IOException("Could not delete file '${file.path}'")
} }
if (destinationDir.exists()) throw IOException("Could not delete directory $destinationDir.")
if (workingDir.exists()) throw IOException("Could not delete internal caches in folder $workingDir")
destinationDir.mkdirs()
workingDir.mkdirs()
} }
private fun sourcesToCompile( private fun sourcesToCompile(
@@ -393,9 +398,8 @@ abstract class IncrementalCompilerRunner<
caches.platformCache.updateComplementaryFiles(dirtySources, expectActualTracker) caches.platformCache.updateComplementaryFiles(dirtySources, expectActualTracker)
caches.inputsCache.registerOutputForSourceFiles(generatedFiles) caches.inputsCache.registerOutputForSourceFiles(generatedFiles)
caches.lookupCache.update(lookupTracker, sourcesToCompile, removedKotlinSources) caches.lookupCache.update(lookupTracker, sourcesToCompile, removedKotlinSources)
updateCaches(services, caches, generatedFiles, changesCollector) updateCaches(services, caches, generatedFiles, changesCollector)
}
}
if (compilationMode is CompilationMode.Rebuild) { if (compilationMode is CompilationMode.Rebuild) {
if (withSnapshot) { if (withSnapshot) {
abiSnapshot.protos.putAll(changesCollector.protoDataChanges()) abiSnapshot.protos.putAll(changesCollector.protoDataChanges())
@@ -498,6 +502,8 @@ abstract class IncrementalCompilerRunner<
BuildDiffsStorage.writeToFile(buildHistoryFile, BuildDiffsStorage(prevDiffs + newDiff), reporter) BuildDiffsStorage.writeToFile(buildHistoryFile, BuildDiffsStorage(prevDiffs + newDiff), reporter)
} }
protected open fun performWorkAfterCompilation(caches: CacheManager) {}
companion object { companion object {
const val DIRTY_SOURCES_FILE_NAME = "dirty-sources.txt" const val DIRTY_SOURCES_FILE_NAME = "dirty-sources.txt"
const val LAST_BUILD_INFO_FILE_NAME = "last-build.bin" const val LAST_BUILD_INFO_FILE_NAME = "last-build.bin"
@@ -20,9 +20,8 @@ import org.jetbrains.kotlin.build.GeneratedFile
import org.jetbrains.kotlin.build.report.BuildReporter import org.jetbrains.kotlin.build.report.BuildReporter
import org.jetbrains.kotlin.build.report.ICReporter import org.jetbrains.kotlin.build.report.ICReporter
import org.jetbrains.kotlin.build.report.metrics.BuildAttribute import org.jetbrains.kotlin.build.report.metrics.BuildAttribute
import org.jetbrains.kotlin.build.report.metrics.BuildPerformanceMetric
import org.jetbrains.kotlin.build.report.metrics.DoNothingBuildMetricsReporter import org.jetbrains.kotlin.build.report.metrics.DoNothingBuildMetricsReporter
import org.jetbrains.kotlin.cli.common.* import org.jetbrains.kotlin.cli.common.ExitCode
import org.jetbrains.kotlin.cli.common.arguments.K2JSCompilerArguments import org.jetbrains.kotlin.cli.common.arguments.K2JSCompilerArguments
import org.jetbrains.kotlin.cli.common.arguments.isIrBackendEnabled import org.jetbrains.kotlin.cli.common.arguments.isIrBackendEnabled
import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.cli.common.messages.MessageCollector
@@ -92,7 +91,13 @@ class IncrementalJsCompilerRunner(
override fun createCacheManager(args: K2JSCompilerArguments, projectDir: File?): IncrementalJsCachesManager { override fun createCacheManager(args: K2JSCompilerArguments, projectDir: File?): IncrementalJsCachesManager {
val serializerProtocol = if (!args.isIrBackendEnabled()) JsSerializerProtocol else KlibMetadataSerializerProtocol val serializerProtocol = if (!args.isIrBackendEnabled()) JsSerializerProtocol else KlibMetadataSerializerProtocol
return IncrementalJsCachesManager(cacheDirectory, projectDir, reporter, serializerProtocol) return IncrementalJsCachesManager(
cacheDirectory,
projectDir,
reporter,
serializerProtocol,
storeFullFqNamesInLookupCache = withSnapshot
)
} }
override fun destinationDir(args: K2JSCompilerArguments): File { override fun destinationDir(args: K2JSCompilerArguments): File {
@@ -41,17 +41,18 @@ import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.config.CompilerConfiguration import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.IncrementalCompilation import org.jetbrains.kotlin.config.IncrementalCompilation
import org.jetbrains.kotlin.config.Services import org.jetbrains.kotlin.config.Services
import org.jetbrains.kotlin.incremental.ClasspathChanges.ClasspathSnapshotDisabled
import org.jetbrains.kotlin.incremental.ClasspathChanges.ClasspathSnapshotEnabled.*
import org.jetbrains.kotlin.incremental.ClasspathChanges.NotAvailableForJSCompiler
import org.jetbrains.kotlin.incremental.classpathDiff.*
import org.jetbrains.kotlin.incremental.components.ExpectActualTracker import org.jetbrains.kotlin.incremental.components.ExpectActualTracker
import org.jetbrains.kotlin.incremental.components.LookupTracker import org.jetbrains.kotlin.incremental.components.LookupTracker
import org.jetbrains.kotlin.incremental.multiproject.EmptyModulesApiHistory import org.jetbrains.kotlin.incremental.multiproject.EmptyModulesApiHistory
import org.jetbrains.kotlin.incremental.multiproject.ModulesApiHistory import org.jetbrains.kotlin.incremental.multiproject.ModulesApiHistory
import org.jetbrains.kotlin.incremental.storage.ListExternalizer
import org.jetbrains.kotlin.incremental.storage.saveToFile
import org.jetbrains.kotlin.incremental.util.BufferingMessageCollector import org.jetbrains.kotlin.incremental.util.BufferingMessageCollector
import org.jetbrains.kotlin.incremental.util.Either import org.jetbrains.kotlin.incremental.util.Either
import org.jetbrains.kotlin.incremental.ClasspathChanges.NotAvailable.UnableToCompute
import org.jetbrains.kotlin.incremental.ClasspathChanges.NotAvailable.ForJSCompiler
import org.jetbrains.kotlin.incremental.ClasspathChanges.NotAvailable.ReservedForTestsOnly
import org.jetbrains.kotlin.incremental.ClasspathChanges.NotAvailable.ForNonIncrementalRun
import org.jetbrains.kotlin.incremental.ClasspathChanges.NotAvailable.ClasspathSnapshotIsDisabled
import org.jetbrains.kotlin.load.java.JavaClassesTracker import org.jetbrains.kotlin.load.java.JavaClassesTracker
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCompilationComponents import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCompilationComponents
@@ -88,7 +89,7 @@ fun makeIncrementally(
buildHistoryFile = buildHistoryFile, buildHistoryFile = buildHistoryFile,
modulesApiHistory = EmptyModulesApiHistory, modulesApiHistory = EmptyModulesApiHistory,
kotlinSourceFilesExtensions = kotlinExtensions, kotlinSourceFilesExtensions = kotlinExtensions,
classpathChanges = ReservedForTestsOnly classpathChanges = ClasspathSnapshotDisabled
) )
//TODO set properly //TODO set properly
compiler.compile(sourceFiles, args, messageCollector, providedChangedFiles = null) compiler.compile(sourceFiles, args, messageCollector, providedChangedFiles = null)
@@ -123,7 +124,7 @@ class IncrementalJvmCompilerRunner(
outputFiles: Collection<File>, outputFiles: Collection<File>,
private val modulesApiHistory: ModulesApiHistory, private val modulesApiHistory: ModulesApiHistory,
override val kotlinSourceFilesExtensions: List<String> = DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS, override val kotlinSourceFilesExtensions: List<String> = DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS,
private val classpathChanges: ClasspathChanges, private val classpathChanges: ClasspathChanges
) : IncrementalCompilerRunner<K2JVMCompilerArguments, IncrementalJvmCachesManager>( ) : IncrementalCompilerRunner<K2JVMCompilerArguments, IncrementalJvmCachesManager>(
workingDir, workingDir,
"caches-jvm", "caches-jvm",
@@ -135,7 +136,13 @@ class IncrementalJvmCompilerRunner(
IncrementalCompilation.isEnabledForJvm() IncrementalCompilation.isEnabledForJvm()
override fun createCacheManager(args: K2JVMCompilerArguments, projectDir: File?): IncrementalJvmCachesManager = override fun createCacheManager(args: K2JVMCompilerArguments, projectDir: File?): IncrementalJvmCachesManager =
IncrementalJvmCachesManager(cacheDirectory, projectDir, File(args.destination), reporter) IncrementalJvmCachesManager(
cacheDirectory,
projectDir,
File(args.destination),
reporter,
storeFullFqNamesInLookupCache = withSnapshot || classpathChanges is ClasspathChanges.ClasspathSnapshotEnabled
)
override fun destinationDir(args: K2JVMCompilerArguments): File = override fun destinationDir(args: K2JVMCompilerArguments): File =
args.destinationAsFile args.destinationAsFile
@@ -203,6 +210,10 @@ class IncrementalJvmCompilerRunner(
return abiSnapshots return abiSnapshots
} }
// Used by `calculateSourcesToCompileImpl` and `performWorkAfterCompilation` methods below.
// Thread safety: There is no concurrent access to this variable.
private var currentClasspathSnapshot: ClasspathSnapshot? = null
private fun calculateSourcesToCompileImpl( private fun calculateSourcesToCompileImpl(
caches: IncrementalJvmCachesManager, caches: IncrementalJvmCachesManager,
changedFiles: ChangedFiles.Known, changedFiles: ChangedFiles.Known,
@@ -212,26 +223,34 @@ class IncrementalJvmCompilerRunner(
val dirtyFiles = DirtyFilesContainer(caches, reporter, kotlinSourceFilesExtensions) val dirtyFiles = DirtyFilesContainer(caches, reporter, kotlinSourceFilesExtensions)
initDirtyFiles(dirtyFiles, changedFiles) initDirtyFiles(dirtyFiles, changedFiles)
val lastBuildInfo = BuildInfo.read(lastBuildInfoFile) ?: return CompilationMode.Rebuild(BuildAttribute.NO_BUILD_HISTORY)
reporter.reportVerbose { "Last Kotlin Build info -- $lastBuildInfo" }
val classpathChanges = when (classpathChanges) { val classpathChanges = when (classpathChanges) {
// Note: classpathChanges is deserialized, so they are no longer singleton objects and need to be compared using `is` (not `==`) // Note: classpathChanges is deserialized, so they are no longer singleton objects and need to be compared using `is` (not `==`)
is ClasspathChanges.Available -> ChangesEither.Known(classpathChanges.lookupSymbols, classpathChanges.fqNames) is Empty -> ChangesEither.Known(emptySet(), emptySet())
is ClasspathChanges.NotAvailable -> when (classpathChanges) { is ToBeComputedByIncrementalCompiler -> reporter.measure(BuildTime.COMPUTE_CLASSPATH_CHANGES) {
is UnableToCompute, is ClasspathSnapshotIsDisabled, is ReservedForTestsOnly -> { check(currentClasspathSnapshot == null)
reporter.measure(BuildTime.IC_ANALYZE_CHANGES_IN_DEPENDENCIES) { currentClasspathSnapshot = reporter.measure(BuildTime.LOAD_CURRENT_CLASSPATH_SNAPSHOT) {
val scopes = caches.lookupCache.lookupMap.keys.map { it.scope.ifBlank { it.name } }.distinct() CachedClasspathSnapshotSerializer.load(classpathChanges.classpathSnapshotFiles.currentClasspathEntrySnapshotFiles)
getClasspathChanges(
args.classpathAsList, changedFiles, lastBuildInfo, modulesApiHistory, reporter, abiSnapshots, withSnapshot,
caches.platformCache, scopes
)
}
}
is ForNonIncrementalRun, is ForJSCompiler -> {
error("Unexpected type for this code path: ${classpathChanges.javaClass.name}.")
} }
ClasspathChangesComputer.computeChangedAndImpactedSet(
currentClasspathSnapshot!!,
caches.lookupCache,
classpathChanges.classpathSnapshotFiles.shrunkPreviousClasspathSnapshotFile,
reporter
).getChanges()
} }
is NotAvailableDueToMissingClasspathSnapshot -> ChangesEither.Unknown(BuildAttribute.CLASSPATH_SNAPSHOT_NOT_FOUND)
is NotAvailableForNonIncrementalRun -> ChangesEither.Unknown(BuildAttribute.UNKNOWN_CHANGES_IN_GRADLE_INPUTS)
is ClasspathSnapshotDisabled -> reporter.measure(BuildTime.IC_ANALYZE_CHANGES_IN_DEPENDENCIES) {
val lastBuildInfo = BuildInfo.read(lastBuildInfoFile) ?: return CompilationMode.Rebuild(BuildAttribute.IC_IS_NOT_ENABLED)
reporter.reportVerbose { "Last Kotlin Build info -- $lastBuildInfo" }
val scopes = caches.lookupCache.lookupMap.keys.map { it.scope.ifBlank { it.name } }.distinct()
getClasspathChanges(
args.classpathAsList, changedFiles, lastBuildInfo, modulesApiHistory, reporter, abiSnapshots, withSnapshot,
caches.platformCache, scopes
)
}
is NotAvailableForJSCompiler -> error("Unexpected type for this code path: ${classpathChanges.javaClass.name}.")
} }
@Suppress("UNUSED_VARIABLE") // for sealed when @Suppress("UNUSED_VARIABLE") // for sealed when
@@ -434,6 +453,38 @@ class IncrementalJvmCompilerRunner(
reportPerformanceData(compiler.defaultPerformanceManager) reportPerformanceData(compiler.defaultPerformanceManager)
return exitCode return exitCode
} }
override fun performWorkAfterCompilation(caches: IncrementalJvmCachesManager) {
if (classpathChanges is ClasspathChanges.ClasspathSnapshotEnabled) {
reporter.measure(BuildTime.SAVE_SHRUNK_CURRENT_CLASSPATH_SNAPSHOT_AFTER_COMPILATION) {
shrinkAndSaveClasspathSnapshot(classpathChanges, caches.lookupCache)
}
}
}
private fun shrinkAndSaveClasspathSnapshot(classpathChanges: ClasspathChanges.ClasspathSnapshotEnabled, lookupStorage: LookupStorage) {
val classpathSnapshot = currentClasspathSnapshot ?: reporter.measure(BuildTime.LOAD_CLASSPATH_SNAPSHOT) {
CachedClasspathSnapshotSerializer.load(classpathChanges.classpathSnapshotFiles.currentClasspathEntrySnapshotFiles)
}
val shrunkClasspathSnapshot = reporter.measure(BuildTime.SHRINK_CLASSPATH_SNAPSHOT) {
ClasspathSnapshotShrinker.shrink(classpathSnapshot, lookupStorage, reporter)
}
reporter.measure(BuildTime.SAVE_SHRUNK_CLASSPATH_SNAPSHOT) {
ListExternalizer(ClassSnapshotWithHashExternalizer).saveToFile(
classpathChanges.classpathSnapshotFiles.shrunkPreviousClasspathSnapshotFile,
shrunkClasspathSnapshot
)
}
reporter.addMetric(
BuildPerformanceMetric.ORIGINAL_CLASSPATH_SNAPSHOT_SIZE,
classpathChanges.classpathSnapshotFiles.currentClasspathEntrySnapshotFiles.sumOf { it.length() }
)
reporter.addMetric(
BuildPerformanceMetric.SHRUNK_CLASSPATH_SNAPSHOT_SIZE,
classpathChanges.classpathSnapshotFiles.shrunkPreviousClasspathSnapshotFile.length()
)
}
} }
var K2JVMCompilerArguments.destinationAsFile: File var K2JVMCompilerArguments.destinationAsFile: File
@@ -5,12 +5,12 @@
package org.jetbrains.kotlin.incremental.classpathDiff package org.jetbrains.kotlin.incremental.classpathDiff
import org.jetbrains.kotlin.incremental.ClasspathChanges import org.jetbrains.kotlin.incremental.ChangesEither
import org.jetbrains.kotlin.incremental.LookupSymbol import org.jetbrains.kotlin.incremental.LookupSymbol
import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.FqName
/** Intermediate data to compute [ClasspathChanges] (see [toClasspathChanges]). */ /** Set of classes, class members, and top-level members that are changed (or impacted by a change). */
class ChangeSet( class ChangeSet(
/** Set of changed classes, preferably ordered by not required. */ /** Set of changed classes, preferably ordered by not required. */
@@ -73,7 +73,7 @@ class ChangeSet(
changedTopLevelMembers + other.changedTopLevelMembers changedTopLevelMembers + other.changedTopLevelMembers
) )
fun toClasspathChanges(): ClasspathChanges.Available { internal fun getChanges(): ChangesEither.Known {
val lookupSymbols = mutableSetOf<LookupSymbol>() val lookupSymbols = mutableSetOf<LookupSymbol>()
val fqNames = mutableSetOf<FqName>() val fqNames = mutableSetOf<FqName>()
@@ -98,6 +98,6 @@ class ChangeSet(
fqNames.add(changedPackage) fqNames.add(changedPackage)
} }
return ClasspathChanges.Available(lookupSymbols, fqNames) return ChangesEither.Known(lookupSymbols, fqNames)
} }
} }
@@ -6,9 +6,14 @@
package org.jetbrains.kotlin.incremental.classpathDiff package org.jetbrains.kotlin.incremental.classpathDiff
import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.io.FileUtil
import org.jetbrains.kotlin.incremental.classpathDiff.ImpactAnalysis.computeImpactedSet import org.jetbrains.kotlin.build.report.metrics.BuildMetricsReporter
import org.jetbrains.kotlin.build.report.metrics.BuildTime
import org.jetbrains.kotlin.build.report.metrics.measure
import org.jetbrains.kotlin.incremental.* import org.jetbrains.kotlin.incremental.*
import org.jetbrains.kotlin.incremental.classpathDiff.ImpactAnalysis.computeImpactedSet
import org.jetbrains.kotlin.incremental.storage.FileToCanonicalPathConverter import org.jetbrains.kotlin.incremental.storage.FileToCanonicalPathConverter
import org.jetbrains.kotlin.incremental.storage.ListExternalizer
import org.jetbrains.kotlin.incremental.storage.loadFromFile
import org.jetbrains.kotlin.metadata.deserialization.TypeTable import org.jetbrains.kotlin.metadata.deserialization.TypeTable
import org.jetbrains.kotlin.metadata.deserialization.supertypes import org.jetbrains.kotlin.metadata.deserialization.supertypes
import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.ClassId
@@ -18,156 +23,101 @@ import org.jetbrains.kotlin.serialization.deserialization.getClassId
import java.io.File import java.io.File
import java.util.* import java.util.*
/** Computes [ClasspathChanges] between two [ClasspathSnapshot]s .*/ /** Computes changes between two [ClasspathSnapshot]s .*/
object ClasspathChangesComputer { object ClasspathChangesComputer {
fun compute( /**
currentClasspathEntrySnapshotFiles: List<File>, * Computes changes between the current and previous [ClasspathSnapshot]s, plus unchanged elements that are impacted by the changes.
previousClasspathEntrySnapshotFiles: List<File>, *
unchangedCurrentClasspathEntrySnapshotFiles: List<File> * NOTE: The original classpath may contain duplicate classes, but the shrunk classpath must not contain duplicate classes.
): ClasspathChanges { */
// To improve performance, we will compute changes for the changed snapshot files only, ignoring unchanged ones. (Duplicate classes fun computeChangedAndImpactedSet(
// will make this a bit tricky, but it will be dealt with below.) currentClasspathSnapshot: ClasspathSnapshot,
// First, align unchanged snapshot files in the current classpath with unchanged snapshot files in the previous classpath. Gradle lookupStorageInPreviousRun: LookupStorage,
// has this information, but doesn't expose it, so we have to reconstruct it here. shrunkPreviousClasspathSnapshotFile: File,
val unchangedCurrentToPreviousAlignment: Map<File, File> = metrics: BuildMetricsReporter
alignUnchangedSnapshotFiles(unchangedCurrentClasspathEntrySnapshotFiles, previousClasspathEntrySnapshotFiles) ): ChangeSet {
val shrunkCurrentClasspathSnapshot = metrics.measure(BuildTime.SHRINK_CURRENT_CLASSPATH_SNAPSHOT) {
// Use sets to make presence checks faster ClasspathSnapshotShrinker.shrink(currentClasspathSnapshot, lookupStorageInPreviousRun, metrics)
val unchangedCurrentFiles: Set<File> = unchangedCurrentToPreviousAlignment.keys }
val unchangedPreviousFiles: Set<File> = unchangedCurrentToPreviousAlignment.values.toSet() val shrunkPreviousClasspathSnapshot = metrics.measure(BuildTime.LOAD_SHRUNK_PREVIOUS_CLASSPATH_SNAPSHOT) {
ListExternalizer(ClassSnapshotWithHashExternalizer).loadFromFile(shrunkPreviousClasspathSnapshotFile)
// We will split the current files into 2 groups: }
// 1a) Unchanged current files return metrics.measure(BuildTime.COMPUTE_CHANGED_AND_IMPACTED_SET) {
// 1b) Added files computeChangedAndImpactedSet(shrunkCurrentClasspathSnapshot, shrunkPreviousClasspathSnapshot, metrics)
// We will split the previous files into 2 groups:
// 2a) Unchanged previous files
// 2b) Removed files
// If the classpath doesn't contain duplicate classes, comparing (1b) with (2b) would be enough.
// However, if the classpath contains duplicate classes, comparing (1b) with (2b) would not be enough.
// Therefore, to deal with duplicate classes while still being able to compare (1b) with (2b), we will find snapshot files in groups
// (1a) and (2a) that have duplicate classes with groups (1b) or (2b) and add them to groups (1b) and (2b). Duplicate classes in
// groups (1b) and (2b) will then be handled in a separate step (see ClasspathChangesComputer.getNonDuplicateClassSnapshots).
val addedFiles: List<File> = currentClasspathEntrySnapshotFiles.filter { it !in unchangedCurrentFiles }
val removedFiles: List<File> = previousClasspathEntrySnapshotFiles.filter { it !in unchangedPreviousFiles }
val adjustedAddedFiles = addedFiles.toMutableSet()
val adjustedRemovedFiles = removedFiles.toMutableSet()
unchangedCurrentToPreviousAlignment.forEach { (unchangedCurrentFile, unchangedPreviousFile) ->
if (unchangedCurrentFile.containsDuplicatesWith(addedFiles) || unchangedPreviousFile.containsDuplicatesWith(removedFiles)) {
adjustedAddedFiles.add(unchangedCurrentFile)
adjustedRemovedFiles.add(unchangedPreviousFile)
}
} }
// Keep the original order of added/removed files as it is important for the handling of duplicate classes.
val finalAddedFiles: List<File> = currentClasspathEntrySnapshotFiles.filter { it in adjustedAddedFiles }
val finalRemovedFiles: List<File> = previousClasspathEntrySnapshotFiles.filter { it in adjustedRemovedFiles }
val changedCurrentSnapshot = ClasspathSnapshotSerializer.load(finalAddedFiles)
val changedPreviousSnapshot = ClasspathSnapshotSerializer.load(finalRemovedFiles)
return compute(changedCurrentSnapshot, changedPreviousSnapshot)
} }
/** /**
* Maps the unchanged snapshot files of the current build to the unchanged snapshot files of the previous build (selected from all the * Computes changes between the current and previous lists of classes, plus unchanged elements that are impacted by the changes.
* snapshot files of the previous build).
* *
* Note that the unchanged files of the current build were detected by Gradle, so a mapping must exist for each of them, we only have to * NOTE: Each list of classes must not contain duplicates.
* find it.
*
* IMPORTANT: The alignment algorithm must use the same input normalization that is used for snapshot files in the Gradle task.
* Currently, snapshot files are annotated with `@Classpath` and are regular files (not jars), so (only) their contents and order
* matter.
*/ */
private fun alignUnchangedSnapshotFiles(unchangedCurrentSnapshotFiles: List<File>, previousSnapshotFiles: List<File>): Map<File, File> { fun computeChangedAndImpactedSet(
val sizeToPreviousFiles: Map<Long, List<IndexedValue<File>>> = previousSnapshotFiles.withIndex().groupBy { it.value.length() } currentClassSnapshots: List<ClassSnapshotWithHash>,
previousClassSnapshots: List<ClassSnapshotWithHash>,
metrics: BuildMetricsReporter
): ChangeSet {
val currentClasses: Map<ClassId, ClassSnapshotWithHash> = currentClassSnapshots.associateBy { it.classSnapshot.getClassId() }
val previousClasses: Map<ClassId, ClassSnapshotWithHash> = previousClassSnapshots.associateBy { it.classSnapshot.getClassId() }
var startIndexToSearch = 0 val changedCurrentClasses: List<ClassSnapshot> = currentClasses.filter { (classId, currentClass) ->
return unchangedCurrentSnapshotFiles.associateWith { unchangedCurrentFile -> val previousClass = previousClasses[classId]
val candidates = (sizeToPreviousFiles[unchangedCurrentFile.length()] ?: emptyList()).filter { it.index >= startIndexToSearch } previousClass == null || currentClass.hash != previousClass.hash
val unchangedPreviousFileWithIndex: IndexedValue<File> = if (candidates.size == 1) { }.map { it.value.classSnapshot }
// A matching file must exist, so if there is only one candidate, it is the one.
candidates.single() val changedPreviousClasses: List<ClassSnapshot> = previousClasses.filter { (classId, previousClass) ->
} else { val currentClass = currentClasses[classId]
// If there are multiple matching files, select the first one. (Even if it doesn't match Gradle's alignment, it is still a currentClass == null || currentClass.hash != previousClass.hash
// correct alignment.) }.map { it.value.classSnapshot }
val unchangedContents = unchangedCurrentFile.readBytes()
candidates.firstOrNull { candidate -> val classChanges = metrics.measure(BuildTime.COMPUTE_CLASS_CHANGES) {
unchangedContents.contentEquals(candidate.value.readBytes()) computeClassChanges(changedCurrentClasses, changedPreviousClasses, metrics)
} ?: error("Can't find previous snapshot file of unchanged current snapshot file '${unchangedCurrentFile.path}'")
}
startIndexToSearch = unchangedPreviousFileWithIndex.index + 1
unchangedPreviousFileWithIndex.value
} }
}
/** Returns `true` if this snapshot file contains a duplicate class with another snapshot file in the given list. */ if (classChanges.isEmpty()) {
@Suppress("unused", "UNUSED_PARAMETER") return classChanges
private fun File.containsDuplicatesWith(otherSnapshotFiles: List<File>): Boolean { }
// FIXME: Implement and optimize this method
return false
}
fun compute(currentClasspathSnapshot: ClasspathSnapshot, previousClasspathSnapshot: ClasspathSnapshot): ClasspathChanges { return metrics.measure(BuildTime.COMPUTE_IMPACTED_SET) {
val currentClassSnapshots = currentClasspathSnapshot.getNonDuplicateClassSnapshots() computeImpactedSet(classChanges, previousClasses.map { it.value.classSnapshot })
val previousClassSnapshots = previousClasspathSnapshot.getNonDuplicateClassSnapshots() }
return computeClassChanges(currentClassSnapshots, previousClassSnapshots)
} }
/** /**
* Returns all [ClassSnapshot]s in this [ClasspathSnapshot]. * Computes changes between the current and previous lists of classes. The returned result does not need to include elements that are
* impacted by the changes.
* *
* If there are duplicate classes on the classpath, retain only the first one to match the compiler's behavior. * NOTE: Each list of classes must not contain duplicates.
*/ */
private fun ClasspathSnapshot.getNonDuplicateClassSnapshots(): List<ClassSnapshot> { fun computeClassChanges(
val classSnapshots = LinkedHashMap<String, ClassSnapshot>(classpathEntrySnapshots.sumOf { it.classSnapshots.size }) currentClassSnapshots: List<ClassSnapshot>,
for (classpathEntrySnapshot in classpathEntrySnapshots) { previousClassSnapshots: List<ClassSnapshot>,
for ((unixStyleRelativePath, classSnapshot) in classpathEntrySnapshot.classSnapshots) { metrics: BuildMetricsReporter
classSnapshots.putIfAbsent(unixStyleRelativePath, classSnapshot) ): ChangeSet {
val asmBasedSnapshotPredicate: (ClassSnapshot) -> Boolean = {
when (it) {
is RegularJavaClassSnapshot -> true
is KotlinClassSnapshot, is ProtoBasedJavaClassSnapshot -> false
else -> error("Unexpected type (it should have been handled earlier): ${it.javaClass.name}")
} }
} }
return classSnapshots.values.toList() val (currentAsmBasedSnapshots, currentProtoBasedSnapshots) = currentClassSnapshots.partition(asmBasedSnapshotPredicate)
} val (previousAsmBasedSnapshots, previousProtoBasedSnapshots) = previousClassSnapshots.partition(asmBasedSnapshotPredicate)
/** val kotlinClassChanges = metrics.measure(BuildTime.COMPUTE_KOTLIN_CLASS_CHANGES) {
* Computes changes between two lists of [ClassSnapshot]s. computeChangesForProtoBasedSnapshots(currentProtoBasedSnapshots, previousProtoBasedSnapshots)
*
* Each list must not contain duplicate classes.
*/
fun computeClassChanges(currentClassSnapshots: List<ClassSnapshot>, previousClassSnapshots: List<ClassSnapshot>): ClasspathChanges {
if (currentClassSnapshots.any { it is ContentHashJavaClassSnapshot }
|| previousClassSnapshots.any { it is ContentHashJavaClassSnapshot }) {
return ClasspathChanges.NotAvailable.UnableToCompute
} }
// Ignore `EmptyJavaClassSnapshot`s as they don't impact the result
val currentNonEmptyClassSnapshots = currentClassSnapshots.filter { it !is EmptyJavaClassSnapshot }
val previousNonEmptyClassSnapshots = previousClassSnapshots.filter { it !is EmptyJavaClassSnapshot }
val (currentAsmBasedSnapshots, currentProtoBasedSnapshots) =
currentNonEmptyClassSnapshots.partition { it is RegularJavaClassSnapshot }
val (previousAsmBasedSnapshots, previousProtoBasedSnapshots) =
previousNonEmptyClassSnapshots.partition { it is RegularJavaClassSnapshot }
val changeSet1 = computeChangesForProtoBasedSnapshots(currentProtoBasedSnapshots, previousProtoBasedSnapshots)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val changeSet2 = JavaClassChangesComputer.compute( val javaClassChanges = metrics.measure(BuildTime.COMPUTE_JAVA_CLASS_CHANGES) {
currentAsmBasedSnapshots as List<RegularJavaClassSnapshot>, JavaClassChangesComputer.compute(
previousAsmBasedSnapshots as List<RegularJavaClassSnapshot> currentAsmBasedSnapshots as List<RegularJavaClassSnapshot>,
) previousAsmBasedSnapshots as List<RegularJavaClassSnapshot>
)
val allChanges = changeSet1 + changeSet2
if (allChanges.isEmpty()) {
return allChanges.toClasspathChanges()
} }
val impactedSet = computeImpactedSet(allChanges, previousNonEmptyClassSnapshots) return kotlinClassChanges + javaClassChanges
return impactedSet.toClasspathChanges()
} }
private fun computeChangesForProtoBasedSnapshots( private fun computeChangesForProtoBasedSnapshots(
@@ -203,7 +153,7 @@ object ClasspathChangesComputer {
) )
incrementalJvmCache.markDirty(JvmClassName.byClassId(previousSnapshot.serializedJavaClass.classId)) incrementalJvmCache.markDirty(JvmClassName.byClassId(previousSnapshot.serializedJavaClass.classId))
} }
is RegularJavaClassSnapshot, is ContentHashJavaClassSnapshot, is EmptyJavaClassSnapshot -> { is RegularJavaClassSnapshot, is EmptyJavaClassSnapshot, is ContentHashJavaClassSnapshot -> {
error("Unexpected type (it should have been handled earlier): ${previousSnapshot.javaClass.name}") error("Unexpected type (it should have been handled earlier): ${previousSnapshot.javaClass.name}")
} }
} }
@@ -233,7 +183,7 @@ object ClasspathChangesComputer {
collector = changesCollector collector = changesCollector
) )
} }
is RegularJavaClassSnapshot, is ContentHashJavaClassSnapshot, is EmptyJavaClassSnapshot -> { is RegularJavaClassSnapshot, is EmptyJavaClassSnapshot, is ContentHashJavaClassSnapshot -> {
error("Unexpected type (it should have been handled earlier): ${currentSnapshot.javaClass.name}") error("Unexpected type (it should have been handled earlier): ${currentSnapshot.javaClass.name}")
} }
} }
@@ -255,7 +205,7 @@ object ClasspathChangesComputer {
private fun DirtyData.normalize(currentClassSnapshots: List<ClassSnapshot>, previousClassSnapshots: List<ClassSnapshot>): ChangeSet { private fun DirtyData.normalize(currentClassSnapshots: List<ClassSnapshot>, previousClassSnapshots: List<ClassSnapshot>): ChangeSet {
val allClassIds = currentClassSnapshots.map { it.getClassId() }.toSet() + previousClassSnapshots.map { it.getClassId() } val allClassIds = currentClassSnapshots.map { it.getClassId() }.toSet() + previousClassSnapshots.map { it.getClassId() }
val fqNameToClassId = LinkedHashMap<FqName, ClassId>(allClassIds.size) val fqNameToClassId = HashMap<FqName, ClassId>(allClassIds.size)
allClassIds.forEach { classId -> allClassIds.forEach { classId ->
val fqName = classId.asSingleFqName() val fqName = classId.asSingleFqName()
check(!fqNameToClassId.contains(fqName)) { check(!fqNameToClassId.contains(fqName)) {
@@ -264,68 +214,70 @@ object ClasspathChangesComputer {
fqNameToClassId[fqName] = classId fqNameToClassId[fqName] = classId
} }
return ChangeSet.Collector().run { val changes = ChangeSet.Collector().run {
dirtyLookupSymbols.forEach { dirtyLookupSymbols.forEach {
fqNameToClassId[FqName(it.scope)]?.let { classIdOfScope -> val lookupSymbolFqName = if (it.scope.isEmpty()) FqName(it.name) else FqName("${it.scope}.${it.name}")
// If scope is a class, lookup symbol is a class member and maybe inner class val lookupSymbolClassId: ClassId? = fqNameToClassId[lookupSymbolFqName]
fqNameToClassId[FqName("${it.scope}.${it.name}")]?.let { innerClass -> if (lookupSymbolClassId != null) {
addChangedClass(innerClass) addChangedClass(lookupSymbolClassId)
} ?: addChangedClassMember(classIdOfScope, it.name) } else {
return@forEach // When lookupSymbolClassId == null, it means that either (1) the LookupSymbol does not refer to a class (it refers to
// a class member or a package-level member), or (2) it refers to a class outside allClassIds. In the following, we
// assume that (1) is the case.
// (2) should typically never happen. In the case that it happens, we will collect incorrect changes, but it's
// acceptable to collect more changes than necessary; also, we do not need to collect changes outside allClassIds, so
// we're not collecting fewer changes than required.
val scopeClassId: ClassId? = fqNameToClassId[FqName(it.scope)]
if (scopeClassId != null) {
addChangedClassMember(scopeClassId, it.name)
} else {
// Similarly, when scopeClassId == null, we assume that LookupSymbol.scope does not refer to a class outside
// allClassIds. It means that the LookupSymbol does not refer to a class member. Therefore, it must refer to a
// package-level member.
addChangedTopLevelMember(FqName(it.scope), it.name)
}
} }
// scope is a package, so changed symbol is a top-level member and maybe a class
val potentialClassFqName = if (it.scope.isEmpty()) FqName(it.name) else FqName("${it.scope}.${it.name}")
fqNameToClassId[potentialClassFqName]?.let { classId ->
// Lookup symbol is a class
addChangedClass(classId)
} ?: addChangedTopLevelMember(FqName(it.scope), it.name)
} }
val changes = getChanges() getChanges()
// dirtyClassesFqNames should be derived from dirtyLookupSymbols. Double-check that this is the case.
val changedFqNames: Set<FqName> =
changes.changedClasses.map { it.asSingleFqName() }.toSet() +
changes.changedClassMembers.keys.map { it.asSingleFqName() } +
changes.changedTopLevelMembers.keys
check(dirtyClassesFqNames.toSet() == changedFqNames) {
"Two sets differ:\n" +
"dirtyClassesFqNames: $dirtyClassesFqNames\n" +
"changedFqNames: $changedFqNames"
}
changes
} }
// DirtyData contains:
// 1. dirtyLookupSymbols => This contains all info we need (extracted above).
// 2. dirtyClassesFqNames => This should be derived from dirtyLookupSymbols.
// 3. dirtyClassesFqNamesForceRecompile => Should be irrelevant.
// Double-check that the assumption at bullet 2 above is correct.
val changedFqNames: Set<FqName> =
changes.changedClasses.map { it.asSingleFqName() }.toSet() +
changes.changedClassMembers.keys.map { it.asSingleFqName() } +
changes.changedTopLevelMembers.keys
check(dirtyClassesFqNames.toSet() == changedFqNames) {
"Two sets differ:\n" +
"dirtyClassesFqNames: $dirtyClassesFqNames\n" +
"changedFqNames: $changedFqNames"
}
return changes
} }
} }
private fun ClassSnapshot.getClassId(): ClassId { internal object ImpactAnalysis {
return when (this) {
is KotlinClassSnapshot -> classInfo.classId
is RegularJavaClassSnapshot -> classId
is ProtoBasedJavaClassSnapshot -> serializedJavaClass.classId
is EmptyJavaClassSnapshot, is ContentHashJavaClassSnapshot -> {
error("Unexpected type (it should have been handled earlier): ${javaClass.name}")
}
}
}
private object ImpactAnalysis {
/** /**
* Computes the set of classes/class members that are impacted by the given changes. * Computes the set of classes, class members, and top-level members that are impacted by the given changes.
* *
* For example, if a superclass has changed, any of its subclasses will be impacted even if it has not changed, and unchanged source * For example, if a superclass has changed, any of its subclasses will be impacted even if it has not changed because unchanged source
* files in the previous compilation that depended on the subclasses will need to be recompiled. * files in the previous compilation that depended on the subclasses will need to be recompiled.
* *
* The returned set is also a [ChangeSet], which includes the given changes plus the impacted ones. * The returned set is also a [ChangeSet], which includes the given changes plus the impacted ones.
*/ */
fun computeImpactedSet(changes: ChangeSet, previousClassSnapshots: List<ClassSnapshot>): ChangeSet { fun computeImpactedSet(changes: ChangeSet, previousClassSnapshots: List<ClassSnapshot>): ChangeSet {
val classIdToSubclasses = getClassIdToSubclassesMap(previousClassSnapshots) val classIdToSubclasses = getClassIdToSubclassesMap(previousClassSnapshots)
val impactedClassesResolver = { classId: ClassId -> classIdToSubclasses[classId] ?: emptySet() }
return ChangeSet.Collector().run { return ChangeSet.Collector().run {
addChangedClasses(findSubclassesInclusive(changes.changedClasses, classIdToSubclasses)) addChangedClasses(findImpactedClassesInclusive(changes.changedClasses, impactedClassesResolver))
for ((changedClass, changedClassMembers) in changes.changedClassMembers) { for ((changedClass, changedClassMembers) in changes.changedClassMembers) {
findSubclassesInclusive(setOf(changedClass), classIdToSubclasses).forEach { findImpactedClassesInclusive(setOf(changedClass), impactedClassesResolver).forEach {
addChangedClassMembers(it, changedClassMembers) addChangedClassMembers(it, changedClassMembers)
} }
} }
@@ -337,7 +289,7 @@ private object ImpactAnalysis {
} }
private fun getClassIdToSubclassesMap(classSnapshots: List<ClassSnapshot>): Map<ClassId, Set<ClassId>> { private fun getClassIdToSubclassesMap(classSnapshots: List<ClassSnapshot>): Map<ClassId, Set<ClassId>> {
val classIds = classSnapshots.map { it.getClassId() } val classIds: Set<ClassId> = classSnapshots.map { it.getClassId() }.toSet() // Use Set for presence check
val classNameToClassId = classIds.associateBy { JvmClassName.byClassId(it) } val classNameToClassId = classIds.associateBy { JvmClassName.byClassId(it) }
val classNameToClassIdResolver = { className: JvmClassName -> classNameToClassId[className] } val classNameToClassIdResolver = { className: JvmClassName -> classNameToClassId[className] }
@@ -345,7 +297,7 @@ private object ImpactAnalysis {
classSnapshots.forEach { classSnapshot -> classSnapshots.forEach { classSnapshot ->
val classId = classSnapshot.getClassId() val classId = classSnapshot.getClassId()
classSnapshot.getSupertypes(classNameToClassIdResolver).forEach { supertype -> classSnapshot.getSupertypes(classNameToClassIdResolver).forEach { supertype ->
// No need to collect supertypes outside the considered class snapshots (e.g., "java/lang/Object") // No need to collect supertypes outside the given set of classes (e.g., "java/lang/Object")
if (supertype in classIds) { if (supertype in classIds) {
classIdToSubclasses.computeIfAbsent(supertype) { mutableSetOf() }.add(classId) classIdToSubclasses.computeIfAbsent(supertype) { mutableSetOf() }.add(classId)
} }
@@ -354,38 +306,17 @@ private object ImpactAnalysis {
return classIdToSubclasses return classIdToSubclasses
} }
private fun ClassSnapshot.getSupertypes(classIdResolver: (JvmClassName) -> ClassId?): List<ClassId> {
return when (this) {
is RegularJavaClassSnapshot -> supertypes.mapNotNull {
// The following call returns null if supertype is outside the considered class snapshots (e.g., "java/lang/Object").
// Use `mapNotNull` as we don't need to collect those supertypes (see getClassIdToSubclassesMap).
classIdResolver.invoke(it)
}
is KotlinClassSnapshot -> supertypes.mapNotNull {
// Same as above
classIdResolver.invoke(it)
}
is ProtoBasedJavaClassSnapshot -> {
val (proto, nameResolver) = serializedJavaClass.toProtoData()
proto.supertypes(TypeTable(proto.typeTable)).map { nameResolver.getClassId(it.className) }
}
is EmptyJavaClassSnapshot, is ContentHashJavaClassSnapshot -> {
error("Unexpected type (it should have been handled earlier): ${javaClass.name}")
}
}
}
/** /**
* Finds direct and indirect subclasses of the given classes. The return set includes both the given classes and their direct and * Finds directly and transitively impacted classes of the given classes. The return set includes both the given classes and the
* indirect subclasses. * impacted classes.
*/ */
private fun findSubclassesInclusive(classIds: Set<ClassId>, classIdsToSubclasses: Map<ClassId, Set<ClassId>>): Set<ClassId> { fun findImpactedClassesInclusive(classIds: Set<ClassId>, impactedClassesResolver: (ClassId) -> Set<ClassId>): Set<ClassId> {
val visitedClasses = mutableSetOf<ClassId>() val visitedClasses = mutableSetOf<ClassId>()
val toVisitClasses = classIds.toMutableSet() val toVisitClasses = classIds.toMutableSet()
while (toVisitClasses.isNotEmpty()) { while (toVisitClasses.isNotEmpty()) {
val nextToVisit = mutableSetOf<ClassId>() val nextToVisit = mutableSetOf<ClassId>()
toVisitClasses.forEach { toVisitClasses.forEach {
nextToVisit.addAll(classIdsToSubclasses[it] ?: emptyList()) nextToVisit.addAll(impactedClassesResolver.invoke(it))
} }
visitedClasses.addAll(toVisitClasses) visitedClasses.addAll(toVisitClasses)
toVisitClasses.clear() toVisitClasses.clear()
@@ -394,3 +325,34 @@ private object ImpactAnalysis {
return visitedClasses return visitedClasses
} }
} }
internal fun ClassSnapshot.getClassId(): ClassId {
return when (this) {
is KotlinClassSnapshot -> classInfo.classId
is RegularJavaClassSnapshot -> classId
is ProtoBasedJavaClassSnapshot -> serializedJavaClass.classId
is EmptyJavaClassSnapshot, is ContentHashJavaClassSnapshot -> {
error("Unexpected type (it should have been handled earlier): ${javaClass.name}")
}
}
}
/**
* Returns the [ClassId]s of the supertypes of this class.
*
* @param classIdResolver Resolves the [ClassId] from the [JvmClassName] of a supertype. It may return null if the supertype is outside the
* considered set of classes (e.g., "java/lang/Object"). Those supertypes do not need to be included in the returned result.
*/
internal fun ClassSnapshot.getSupertypes(classIdResolver: (JvmClassName) -> ClassId?): List<ClassId> {
return when (this) {
is KotlinClassSnapshot -> supertypes.mapNotNull { classIdResolver.invoke(it) }
is RegularJavaClassSnapshot -> supertypes.mapNotNull { classIdResolver.invoke(it) }
is ProtoBasedJavaClassSnapshot -> {
val (proto, nameResolver) = serializedJavaClass.toProtoData()
proto.supertypes(TypeTable(proto.typeTable)).map { nameResolver.getClassId(it.className) }
}
is EmptyJavaClassSnapshot, is ContentHashJavaClassSnapshot -> {
error("Unexpected type (it should have been handled earlier): ${javaClass.name}")
}
}
}
@@ -7,6 +7,8 @@ package org.jetbrains.kotlin.incremental.classpathDiff
import org.jetbrains.kotlin.incremental.KotlinClassInfo import org.jetbrains.kotlin.incremental.KotlinClassInfo
import org.jetbrains.kotlin.incremental.SerializedJavaClass import org.jetbrains.kotlin.incremental.SerializedJavaClass
import org.jetbrains.kotlin.incremental.md5
import org.jetbrains.kotlin.incremental.storage.toByteArray
import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.resolve.jvm.JvmClassName import org.jetbrains.kotlin.resolve.jvm.JvmClassName
@@ -25,7 +27,7 @@ class ClasspathEntrySnapshot(
* Maps (Unix-style) relative paths of classes to their snapshots. The paths are relative to the containing classpath entry (directory * Maps (Unix-style) relative paths of classes to their snapshots. The paths are relative to the containing classpath entry (directory
* or jar). * or jar).
*/ */
val classSnapshots: LinkedHashMap<String, ClassSnapshot> val classSnapshots: LinkedHashMap<String, ClassSnapshotWithHash>
) )
/** /**
@@ -36,7 +38,14 @@ class ClasspathEntrySnapshot(
* `KotlinCompile` task and the task needs to support compile avoidance. For example, this class should contain public method signatures, * `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. * and should not contain private method signatures, or method implementations.
*/ */
sealed class ClassSnapshot sealed class ClassSnapshot {
/** Computes the hash of this [ClassSnapshot] and returns a [ClassSnapshotWithHash]. */
fun addHash() = ClassSnapshotWithHash(this, ClassSnapshotExternalizer.toByteArray(this).md5())
}
/** Contains a [ClassSnapshot] and its hash. */
class ClassSnapshotWithHash(val classSnapshot: ClassSnapshot, val hash: Long)
/** [ClassSnapshot] of a Kotlin class. */ /** [ClassSnapshot] of a Kotlin class. */
class KotlinClassSnapshot( class KotlinClassSnapshot(
@@ -10,32 +10,59 @@ import org.jetbrains.kotlin.incremental.JavaClassProtoMapValueExternalizer
import org.jetbrains.kotlin.incremental.KotlinClassInfo import org.jetbrains.kotlin.incremental.KotlinClassInfo
import org.jetbrains.kotlin.incremental.storage.* import org.jetbrains.kotlin.incremental.storage.*
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
import java.io.* import java.io.DataInput
import java.io.DataOutput
import java.io.File
import java.util.concurrent.ConcurrentHashMap
/** Utility to serialize a [ClasspathSnapshot]. */ /** Utility to serialize a [ClasspathSnapshot]. */
object ClasspathSnapshotSerializer { object CachedClasspathSnapshotSerializer {
private val cache = ConcurrentHashMap<File, ClasspathEntrySnapshot>()
private const val RECOMMENDED_MAX_CACHE_SIZE = 100
fun load(classpathEntrySnapshotFiles: List<File>): ClasspathSnapshot { fun load(classpathEntrySnapshotFiles: List<File>): ClasspathSnapshot {
return ClasspathSnapshot(classpathEntrySnapshotFiles.map { return ClasspathSnapshot(classpathEntrySnapshotFiles.map { snapshotFile ->
ClasspathEntrySnapshotSerializer.load(it) cache.computeIfAbsent(snapshotFile) {
}) ClasspathEntrySnapshotExternalizer.loadFromFile(it)
}
}).also {
handleCacheEviction(recentlyReferencedKeys = classpathEntrySnapshotFiles)
}
}
private fun handleCacheEviction(recentlyReferencedKeys: List<File>) {
if (cache.size > RECOMMENDED_MAX_CACHE_SIZE) {
// Remove old entries.
// Note:
// - The cache entries after eviction = recently-referenced entries + some other entries (so that
// size = RECOMMENDED_MAX_CACHE_SIZE)
// + Removed entries don't have to be the oldest (for simplicity).
// + If recentlyReferencedKeys.size > RECOMMENDED_MAX_CACHE_SIZE, all of them will be kept. The reason is that
// recently-referenced entries will likely be used again, so we keep them even if the cache is larger than recommended.
// - It's okay to have race condition in this method.
val oldKeys = cache.keys - recentlyReferencedKeys.toSet()
for (oldKey in oldKeys) {
cache.remove(oldKey)
if (cache.size <= RECOMMENDED_MAX_CACHE_SIZE) break
}
}
} }
} }
object ClasspathEntrySnapshotSerializer : DataSerializer<ClasspathEntrySnapshot> { object ClasspathEntrySnapshotExternalizer : DataExternalizer<ClasspathEntrySnapshot> {
override fun save(output: DataOutput, snapshot: ClasspathEntrySnapshot) { override fun save(output: DataOutput, snapshot: ClasspathEntrySnapshot) {
LinkedHashMapExternalizer(StringExternalizer, ClassSnapshotDataSerializer).save(output, snapshot.classSnapshots) LinkedHashMapExternalizer(StringExternalizer, ClassSnapshotWithHashExternalizer).save(output, snapshot.classSnapshots)
} }
override fun read(input: DataInput): ClasspathEntrySnapshot { override fun read(input: DataInput): ClasspathEntrySnapshot {
return ClasspathEntrySnapshot( return ClasspathEntrySnapshot(
classSnapshots = LinkedHashMapExternalizer(StringExternalizer, ClassSnapshotDataSerializer).read(input) classSnapshots = LinkedHashMapExternalizer(StringExternalizer, ClassSnapshotWithHashExternalizer).read(input)
) )
} }
} }
object ClassSnapshotDataSerializer : DataSerializer<ClassSnapshot> { object ClassSnapshotExternalizer : DataExternalizer<ClassSnapshot> {
override fun save(output: DataOutput, snapshot: ClassSnapshot) { override fun save(output: DataOutput, snapshot: ClassSnapshot) {
output.writeBoolean(snapshot is KotlinClassSnapshot) output.writeBoolean(snapshot is KotlinClassSnapshot)
@@ -55,6 +82,21 @@ object ClassSnapshotDataSerializer : DataSerializer<ClassSnapshot> {
} }
} }
object ClassSnapshotWithHashExternalizer : DataExternalizer<ClassSnapshotWithHash> {
override fun save(output: DataOutput, snapshot: ClassSnapshotWithHash) {
ClassSnapshotExternalizer.save(output, snapshot.classSnapshot)
LongExternalizer.save(output, snapshot.hash)
}
override fun read(input: DataInput): ClassSnapshotWithHash {
return ClassSnapshotWithHash(
classSnapshot = ClassSnapshotExternalizer.read(input),
hash = LongExternalizer.read(input)
)
}
}
object KotlinClassSnapshotExternalizer : DataExternalizer<KotlinClassSnapshot> { object KotlinClassSnapshotExternalizer : DataExternalizer<KotlinClassSnapshot> {
override fun save(output: DataOutput, snapshot: KotlinClassSnapshot) { override fun save(output: DataOutput, snapshot: KotlinClassSnapshot) {
@@ -183,32 +225,3 @@ object ContentHashJavaClassSnapshotExternalizer : DataExternalizer<ContentHashJa
return ContentHashJavaClassSnapshot(contentHash = LongExternalizer.read(input)) return ContentHashJavaClassSnapshot(contentHash = LongExternalizer.read(input))
} }
} }
interface DataSerializer<T> : DataExternalizer<T> {
fun save(file: File, value: T) {
return DataOutputStream(FileOutputStream(file).buffered()).use {
save(it, value)
}
}
fun load(file: File): T {
return DataInputStream(FileInputStream(file).buffered()).use {
read(it)
}
}
fun toByteArray(value: T): ByteArray {
val byteArrayOutputStream = ByteArrayOutputStream()
DataOutputStream(byteArrayOutputStream.buffered()).use {
save(it, value)
}
return byteArrayOutputStream.toByteArray()
}
fun fromByteArray(byteArray: ByteArray): T {
return DataInputStream(ByteArrayInputStream(byteArray).buffered()).use {
read(it)
}
}
}
@@ -0,0 +1,123 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.incremental.classpathDiff
import org.jetbrains.kotlin.build.report.metrics.BuildMetricsReporter
import org.jetbrains.kotlin.build.report.metrics.BuildTime
import org.jetbrains.kotlin.build.report.metrics.measure
import org.jetbrains.kotlin.incremental.LookupStorage
import org.jetbrains.kotlin.incremental.storage.LookupSymbolKey
import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
object ClasspathSnapshotShrinker {
/**
* Shrinks the given [ClasspathSnapshot] by retaining only classes that are referenced. Referencing info is stored in [LookupStorage].
*
* This method also removes duplicate classes and [EmptyJavaClassSnapshot]s first.
*/
fun shrink(
classpathSnapshot: ClasspathSnapshot,
lookupStorage: LookupStorage,
metrics: BuildMetricsReporter
): List<ClassSnapshotWithHash> {
val allClasses = metrics.measure(BuildTime.GET_NON_DUPLICATE_CLASSES) {
// It's important to remove duplicate classes first before removing `EmptyJavaClassSnapshot`s.
// For example, if jar1!/com/example/A.class is empty and jar2!/com/example/A.class is non-empty, incorrect order of the actions
// will lead to incorrect results.
classpathSnapshot.getNonDuplicateClassSnapshots().filter { it.classSnapshot !is EmptyJavaClassSnapshot }
}
val lookupSymbols = metrics.measure(BuildTime.GET_LOOKUP_SYMBOLS) {
lookupStorage.lookupMap.keys
}
val referencedClasses = metrics.measure(BuildTime.FIND_REFERENCED_CLASSES) {
findReferencedClasses(allClasses, lookupSymbols)
}
return metrics.measure(BuildTime.FIND_TRANSITIVELY_REFERENCED_CLASSES) {
findTransitivelyReferencedClasses(allClasses, referencedClasses)
}
}
/**
* Finds classes that are referenced. Referencing info is stored in [LookupStorage].
*
* Note: It's okay to over-approximate referenced classes.
*/
private fun findReferencedClasses(
allClasses: List<ClassSnapshotWithHash>,
lookupSymbols: Collection<LookupSymbolKey>
): List<ClassSnapshotWithHash> {
val potentialClassNamesOfReferencedClasses =
lookupSymbols.flatMap {
val lookupSymbolFqName = if (it.scope.isEmpty()) FqName(it.name) else FqName("${it.scope}.${it.name}")
listOf(
lookupSymbolFqName, // If LookupSymbol refers to a class, the class's FqName will be captured here.
FqName(it.scope) // If LookupSymbol refers to a class member, the class's FqName will be captured here.
)
}.toSet() // Use Set for presence check
val potentialPackageNamesOfReferencedPackageLevelMembers =
lookupSymbols.map {
FqName(it.scope) // If LookupSymbol refers to a package-level member, the package's FqName will be captured here.
}.toSet() // Use Set for presence check
return allClasses.filter {
val classId = it.classSnapshot.getClassId()
(classId.asSingleFqName() in potentialClassNamesOfReferencedClasses) ||
(it.classSnapshot is KotlinClassSnapshot
&& it.classSnapshot.classInfo.classKind != KotlinClassHeader.Kind.CLASS
&& classId.packageFqName in potentialPackageNamesOfReferencedPackageLevelMembers)
}
}
/**
* Finds classes that are transitively referenced. For example, if a subclass is referenced, its supertypes will potentially be
* referenced.
*
* The returned list includes the given referenced classes plus the transitively referenced ones.
*/
private fun findTransitivelyReferencedClasses(
allClasses: List<ClassSnapshotWithHash>,
referencedClasses: List<ClassSnapshotWithHash>
): List<ClassSnapshotWithHash> {
val classIdToClassSnapshot = allClasses.associateBy { it.classSnapshot.getClassId() }
val classIds: Set<ClassId> = classIdToClassSnapshot.keys // Use Set for presence check
val classNameToClassId = classIds.associateBy { JvmClassName.byClassId(it) }
val classNameToClassIdResolver = { className: JvmClassName -> classNameToClassId[className] }
val supertypesResolver = { classId: ClassId ->
// No need to collect supertypes outside the given set of classes (e.g., "java/lang/Object")
@Suppress("SimpleRedundantLet")
classIdToClassSnapshot[classId]?.let {
it.classSnapshot.getSupertypes(classNameToClassIdResolver).filter { supertype -> supertype in classIds }.toSet()
} ?: emptySet()
}
val referencedClassIds = referencedClasses.map { it.classSnapshot.getClassId() }.toSet()
val transitivelyReferencedClassIds: Set<ClassId> =
ImpactAnalysis.findImpactedClassesInclusive(referencedClassIds, supertypesResolver) // Use Set for presence check
return allClasses.filter { it.classSnapshot.getClassId() in transitivelyReferencedClassIds }
}
}
/**
* Returns all [ClassSnapshot]s in this [ClasspathSnapshot].
*
* If there are duplicate classes on the classpath, retain only the first one to match the compiler's behavior.
*/
internal fun ClasspathSnapshot.getNonDuplicateClassSnapshots(): List<ClassSnapshotWithHash> {
val classSnapshots = LinkedHashMap<String, ClassSnapshotWithHash>(classpathEntrySnapshots.sumOf { it.classSnapshots.size })
for (classpathEntrySnapshot in classpathEntrySnapshots) {
for ((unixStyleRelativePath, classSnapshot) in classpathEntrySnapshot.classSnapshots) {
classSnapshots.putIfAbsent(unixStyleRelativePath, classSnapshot)
}
}
return classSnapshots.values.toList()
}
@@ -38,10 +38,10 @@ object ClasspathEntrySnapshotter {
} }
val snapshots = try { val snapshots = try {
ClassSnapshotter.snapshot(classes, protoBased) ClassSnapshotter.snapshot(classes, protoBased).map { it.addHash() }
} catch (e: Throwable) { } catch (e: Throwable) {
if (isKnownProblematicClasspathEntry(classpathEntry)) { if (isKnownProblematicClasspathEntry(classpathEntry)) {
classes.map { ContentHashJavaClassSnapshot(it.contents.md5()) } classes.map { ContentHashJavaClassSnapshot(it.contents.md5()).addHash() }
} else throw e } else throw e
} }
@@ -5,11 +5,12 @@
package org.jetbrains.kotlin.incremental.classpathDiff package org.jetbrains.kotlin.incremental.classpathDiff
import org.jetbrains.kotlin.build.report.metrics.DoNothingBuildMetricsReporter
import org.jetbrains.kotlin.incremental.ChangesEither
import org.jetbrains.kotlin.incremental.LookupSymbol
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.compileAll import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.compileAll
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.snapshot import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.snapshot
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.snapshotAll import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.snapshotAll
import org.jetbrains.kotlin.incremental.ClasspathChanges
import org.jetbrains.kotlin.incremental.LookupSymbol
import org.jetbrains.kotlin.resolve.sam.SAM_LOOKUP_NAME import org.jetbrains.kotlin.resolve.sam.SAM_LOOKUP_NAME
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder import org.junit.rules.TemporaryFolder
@@ -46,7 +47,7 @@ class KotlinOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
val sourceFile = SimpleKotlinClass(tmpDir) val sourceFile = SimpleKotlinClass(tmpDir)
val previousSnapshot = sourceFile.compileAndSnapshot() val previousSnapshot = sourceFile.compileAndSnapshot()
val currentSnapshot = sourceFile.changePublicMethodSignature().compileAndSnapshot() val currentSnapshot = sourceFile.changePublicMethodSignature().compileAndSnapshot()
val changes = ClasspathChangesComputer.computeClassChanges(listOf(currentSnapshot), listOf(previousSnapshot)).normalize() val changes = computeClassChanges(currentSnapshot, previousSnapshot)
Changes( Changes(
lookupSymbols = setOf( lookupSymbols = setOf(
@@ -62,7 +63,7 @@ class KotlinOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
val sourceFile = SimpleKotlinClass(tmpDir) val sourceFile = SimpleKotlinClass(tmpDir)
val previousSnapshot = sourceFile.compileAndSnapshot() val previousSnapshot = sourceFile.compileAndSnapshot()
val currentSnapshot = sourceFile.changeMethodImplementation().compileAndSnapshot() val currentSnapshot = sourceFile.changeMethodImplementation().compileAndSnapshot()
val changes = ClasspathChangesComputer.computeClassChanges(listOf(currentSnapshot), listOf(previousSnapshot)).normalize() val changes = computeClassChanges(currentSnapshot, previousSnapshot)
Changes(emptySet(), emptySet()).assertEquals(changes) Changes(emptySet(), emptySet()).assertEquals(changes)
} }
@@ -72,7 +73,7 @@ class KotlinOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testVariousAbiChanges/src/kotlin").canonicalFile val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testVariousAbiChanges/src/kotlin").canonicalFile
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir) val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir)
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir) val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir)
val changes = ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot).normalize() val changes = computeClasspathChanges(currentSnapshot, previousSnapshot)
Changes( Changes(
lookupSymbols = setOf( lookupSymbols = setOf(
@@ -121,7 +122,7 @@ class KotlinOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
File(testDataDir, "../ClasspathChangesComputerTest/testImpactAnalysis_KotlinOrJava/src/kotlin").canonicalFile File(testDataDir, "../ClasspathChangesComputerTest/testImpactAnalysis_KotlinOrJava/src/kotlin").canonicalFile
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir) val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir)
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir) val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir)
val changes = ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot).normalize() val changes = computeClasspathChanges(currentSnapshot, previousSnapshot)
Changes( Changes(
lookupSymbols = setOf( lookupSymbols = setOf(
@@ -158,7 +159,7 @@ class JavaOnlyClasspathChangesComputerTest(private val protoBased: Boolean) : Cl
val sourceFile = SimpleJavaClass(tmpDir) val sourceFile = SimpleJavaClass(tmpDir)
val previousSnapshot = sourceFile.compile().snapshot(protoBased) val previousSnapshot = sourceFile.compile().snapshot(protoBased)
val currentSnapshot = sourceFile.changePublicMethodSignature().compile().snapshot(protoBased) val currentSnapshot = sourceFile.changePublicMethodSignature().compile().snapshot(protoBased)
val changes = ClasspathChangesComputer.computeClassChanges(listOf(currentSnapshot), listOf(previousSnapshot)).normalize() val changes = computeClassChanges(currentSnapshot, previousSnapshot)
Changes( Changes(
lookupSymbols = setOf( lookupSymbols = setOf(
@@ -174,7 +175,7 @@ class JavaOnlyClasspathChangesComputerTest(private val protoBased: Boolean) : Cl
val sourceFile = SimpleJavaClass(tmpDir) val sourceFile = SimpleJavaClass(tmpDir)
val previousSnapshot = sourceFile.compile().snapshot(protoBased) val previousSnapshot = sourceFile.compile().snapshot(protoBased)
val currentSnapshot = sourceFile.changeMethodImplementation().compile().snapshot(protoBased) val currentSnapshot = sourceFile.changeMethodImplementation().compile().snapshot(protoBased)
val changes = ClasspathChangesComputer.computeClassChanges(listOf(currentSnapshot), listOf(previousSnapshot)).normalize() val changes = computeClassChanges(currentSnapshot, previousSnapshot)
Changes(emptySet(), emptySet()).assertEquals(changes) Changes(emptySet(), emptySet()).assertEquals(changes)
} }
@@ -184,7 +185,7 @@ class JavaOnlyClasspathChangesComputerTest(private val protoBased: Boolean) : Cl
val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testVariousAbiChanges/src/java").canonicalFile val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testVariousAbiChanges/src/java").canonicalFile
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir, protoBased) val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir, protoBased)
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir, protoBased) val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir, protoBased)
val changes = ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot).normalize() val changes = computeClasspathChanges(currentSnapshot, previousSnapshot)
Changes( Changes(
lookupSymbols = setOf( lookupSymbols = setOf(
@@ -229,7 +230,7 @@ class JavaOnlyClasspathChangesComputerTest(private val protoBased: Boolean) : Cl
val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testImpactAnalysis_KotlinOrJava/src/java").canonicalFile val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testImpactAnalysis_KotlinOrJava/src/java").canonicalFile
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir, protoBased) val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir, protoBased)
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir, protoBased) val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir, protoBased)
val changes = ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot).normalize() val changes = computeClasspathChanges(currentSnapshot, previousSnapshot)
Changes( Changes(
lookupSymbols = setOf( lookupSymbols = setOf(
@@ -259,7 +260,7 @@ class KotlinAndJavaClasspathChangesComputerTest : ClasspathSnapshotTestCommon()
val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testImpactAnalysis_KotlinAndJava/src").canonicalFile val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testImpactAnalysis_KotlinAndJava/src").canonicalFile
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir) val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir)
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir) val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir)
val changes = ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot).normalize() val changes = computeClasspathChanges(currentSnapshot, previousSnapshot)
Changes( Changes(
lookupSymbols = setOf( lookupSymbols = setOf(
@@ -301,7 +302,7 @@ private fun snapshotClasspath(classpathSourceDir: File, tmpDir: TemporaryFolder,
classpath.addAll(listOfNotNull(classFiles.firstOrNull()?.classRoot)) classpath.addAll(listOfNotNull(classFiles.firstOrNull()?.classRoot))
val relativePaths = classFiles.map { it.unixStyleRelativePath } val relativePaths = classFiles.map { it.unixStyleRelativePath }
val classSnapshots = classFiles.snapshotAll(protoBased) val classSnapshots = classFiles.snapshotAll(protoBased).map { it.addHash() }
ClasspathEntrySnapshot( ClasspathEntrySnapshot(
classSnapshots = relativePaths.zip(classSnapshots).toMap(LinkedHashMap()) classSnapshots = relativePaths.zip(classSnapshots).toMap(LinkedHashMap())
) )
@@ -309,12 +310,31 @@ private fun snapshotClasspath(classpathSourceDir: File, tmpDir: TemporaryFolder,
return ClasspathSnapshot(classpathEntrySnapshots) return ClasspathSnapshot(classpathEntrySnapshots)
} }
/** Adapted version of [ClasspathChanges.Available] for readability in this test. */ private fun computeClasspathChanges(
currentClasspathSnapshot: ClasspathSnapshot,
previousClasspathSnapshot: ClasspathSnapshot
): Changes {
return ClasspathChangesComputer.computeChangedAndImpactedSet(
currentClasspathSnapshot.getNonDuplicateClassSnapshots(),
previousClasspathSnapshot.getNonDuplicateClassSnapshots(),
DoNothingBuildMetricsReporter
).normalize()
}
private fun computeClassChanges(currentClassSnapshot: ClassSnapshot, previousClassSnapshot: ClassSnapshot): Changes {
return ClasspathChangesComputer.computeClassChanges(
listOf(currentClassSnapshot),
listOf(previousClassSnapshot),
DoNothingBuildMetricsReporter
).normalize()
}
/** Adapted version of [ChangesEither.Known] for readability in this test. */
private data class Changes(val lookupSymbols: Set<LookupSymbol>, val fqNames: Set<String>) private data class Changes(val lookupSymbols: Set<LookupSymbol>, val fqNames: Set<String>)
private fun ClasspathChanges.normalize(): Changes { private fun ChangeSet.normalize(): Changes {
this as ClasspathChanges.Available val changes: ChangesEither.Known = getChanges()
return Changes(lookupSymbols, fqNames.map { it.asString() }.toSet()) return Changes(changes.lookupSymbols.toSet(), changes.fqNames.map { it.asString() }.toSet())
} }
private fun Changes.assertEquals(actual: Changes) { private fun Changes.assertEquals(actual: Changes) {
@@ -6,6 +6,8 @@
package org.jetbrains.kotlin.incremental.classpathDiff package org.jetbrains.kotlin.incremental.classpathDiff
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.readBytes import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotTestCommon.Util.readBytes
import org.jetbrains.kotlin.incremental.storage.fromByteArray
import org.jetbrains.kotlin.incremental.storage.toByteArray
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
@@ -16,8 +18,8 @@ abstract class ClasspathSnapshotSerializerTest : ClasspathSnapshotTestCommon() {
@Test @Test
open fun `test ClassSnapshotDataSerializer`() { open fun `test ClassSnapshotDataSerializer`() {
val originalSnapshot = testSourceFile.compileAndSnapshot() val originalSnapshot = testSourceFile.compileAndSnapshot()
val serializedSnapshot = ClassSnapshotDataSerializer.toByteArray(originalSnapshot) val serializedSnapshot = ClassSnapshotExternalizer.toByteArray(originalSnapshot)
val deserializedSnapshot = ClassSnapshotDataSerializer.fromByteArray(serializedSnapshot) val deserializedSnapshot = ClassSnapshotExternalizer.fromByteArray(serializedSnapshot)
assertEquals(originalSnapshot.toGson(), deserializedSnapshot.toGson()) assertEquals(originalSnapshot.toGson(), deserializedSnapshot.toGson())
} }
@@ -36,8 +38,8 @@ class JavaClassesClasspathSnapshotSerializerTest : ClasspathSnapshotSerializerTe
val originalSnapshot = testSourceFile.compile().let { val originalSnapshot = testSourceFile.compile().let {
ClassSnapshotter.snapshot(listOf(ClassFileWithContents(it, it.readBytes())), includeDebugInfoInSnapshot = false) ClassSnapshotter.snapshot(listOf(ClassFileWithContents(it, it.readBytes())), includeDebugInfoInSnapshot = false)
}.single() }.single()
val serializedSnapshot = ClassSnapshotDataSerializer.toByteArray(originalSnapshot) val serializedSnapshot = ClassSnapshotExternalizer.toByteArray(originalSnapshot)
val deserializedSnapshot = ClassSnapshotDataSerializer.fromByteArray(serializedSnapshot) val deserializedSnapshot = ClassSnapshotExternalizer.fromByteArray(serializedSnapshot)
assertEquals(originalSnapshot.toGson(), deserializedSnapshot.toGson()) assertEquals(originalSnapshot.toGson(), deserializedSnapshot.toGson())
} }
@@ -0,0 +1,9 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.incremental.classpathDiff
// TODO: Write this test
abstract class ClasspathSnapshotShrinkerTest
@@ -21,7 +21,7 @@ abstract class ClasspathSnapshotTestCommon {
companion object { companion object {
val testDataDir = val testDataDir =
File("libraries/tools/kotlin-gradle-plugin/src/testData/org/jetbrains/kotlin/gradle/incremental/ClasspathSnapshotTestCommon") File("compiler/incremental-compilation-impl/testData/org/jetbrains/kotlin/incremental/classpathDiff/ClasspathSnapshotTestCommon")
} }
@get:Rule @get:Rule
@@ -9,8 +9,9 @@ import org.gradle.api.artifacts.transform.*
import org.gradle.api.file.FileSystemLocation import org.gradle.api.file.FileSystemLocation
import org.gradle.api.provider.Provider import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Classpath import org.gradle.api.tasks.Classpath
import org.jetbrains.kotlin.gradle.incremental.ClasspathEntrySnapshotSerializer import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathEntrySnapshotExternalizer
import org.jetbrains.kotlin.gradle.incremental.ClasspathEntrySnapshotter import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathEntrySnapshotter
import org.jetbrains.kotlin.incremental.storage.saveToFile
/** Transform to create a snapshot of a classpath entry (directory or jar). */ /** Transform to create a snapshot of a classpath entry (directory or jar). */
@CacheableTransform @CacheableTransform
@@ -25,6 +26,6 @@ abstract class ClasspathEntrySnapshotTransform : TransformAction<TransformParame
val snapshotFile = outputs.file(classpathEntry.name.replace('.', '_') + "-snapshot.bin") val snapshotFile = outputs.file(classpathEntry.name.replace('.', '_') + "-snapshot.bin")
val snapshot = ClasspathEntrySnapshotter.snapshot(classpathEntry) val snapshot = ClasspathEntrySnapshotter.snapshot(classpathEntry)
ClasspathEntrySnapshotSerializer.save(snapshotFile, snapshot) ClasspathEntrySnapshotExternalizer.saveToFile(snapshotFile, snapshot)
} }
} }
@@ -55,6 +55,9 @@ import org.jetbrains.kotlin.gradle.targets.js.ir.isProduceUnzippedKlib
import org.jetbrains.kotlin.gradle.utils.* import org.jetbrains.kotlin.gradle.utils.*
import org.jetbrains.kotlin.incremental.ChangedFiles import org.jetbrains.kotlin.incremental.ChangedFiles
import org.jetbrains.kotlin.incremental.ClasspathChanges import org.jetbrains.kotlin.incremental.ClasspathChanges
import org.jetbrains.kotlin.incremental.ClasspathChanges.ClasspathSnapshotDisabled
import org.jetbrains.kotlin.incremental.ClasspathChanges.ClasspathSnapshotEnabled.*
import org.jetbrains.kotlin.incremental.ClasspathSnapshotFiles
import org.jetbrains.kotlin.incremental.IncrementalCompilerRunner import org.jetbrains.kotlin.incremental.IncrementalCompilerRunner
import org.jetbrains.kotlin.library.impl.isKotlinLibrary import org.jetbrains.kotlin.library.impl.isKotlinLibrary
import org.jetbrains.kotlin.statistics.BuildSessionLogger import org.jetbrains.kotlin.statistics.BuildSessionLogger
@@ -584,7 +587,6 @@ abstract class KotlinCompile @Inject constructor(
) )
val classpathSnapshotDir = getClasspathSnapshotDir(task) val classpathSnapshotDir = getClasspathSnapshotDir(task)
task.classpathSnapshotProperties.classpathSnapshotDir.value(classpathSnapshotDir).disallowChanges() task.classpathSnapshotProperties.classpathSnapshotDir.value(classpathSnapshotDir).disallowChanges()
task.classpathSnapshotProperties.classpathSnapshotDirFileCollection.from(classpathSnapshotDir)
} else { } else {
task.classpathSnapshotProperties.classpath.from(task.project.provider { task.classpath }) task.classpathSnapshotProperties.classpath.from(task.project.provider { task.classpath })
} }
@@ -644,14 +646,6 @@ abstract class KotlinCompile @Inject constructor(
@get:OutputDirectory @get:OutputDirectory
@get:Optional // Set if useClasspathSnapshot == true @get:Optional // Set if useClasspathSnapshot == true
abstract val classpathSnapshotDir: DirectoryProperty abstract val classpathSnapshotDir: DirectoryProperty
/**
* [FileCollection] containing a single file which is [classpathSnapshotDir], used when a [FileCollection] is required instead of a
* [DirectoryProperty].
*/
// Set if useClasspathSnapshot == true
@get:Internal
abstract val classpathSnapshotDirFileCollection: ConfigurableFileCollection
} }
@get:Internal @get:Internal
@@ -740,15 +734,10 @@ abstract class KotlinCompile @Inject constructor(
val compilerRunner = compilerRunner.get() val compilerRunner = compilerRunner.get()
val icEnv = if (isIncrementalCompilationEnabled()) { val icEnv = if (isIncrementalCompilationEnabled()) {
val classpathChanges = when {
!classpathSnapshotProperties.useClasspathSnapshot.get() -> ClasspathChanges.NotAvailable.ClasspathSnapshotIsDisabled
inputChanges.isIncremental -> getClasspathChanges(inputChanges)
else -> ClasspathChanges.NotAvailable.ForNonIncrementalRun
}
logger.info(USING_JVM_INCREMENTAL_COMPILATION_MESSAGE) logger.info(USING_JVM_INCREMENTAL_COMPILATION_MESSAGE)
IncrementalCompilationEnvironment( IncrementalCompilationEnvironment(
changedFiles = getChangedFiles(inputChanges, incrementalProps), changedFiles = getChangedFiles(inputChanges, incrementalProps),
classpathChanges = classpathChanges, classpathChanges = getClasspathChanges(inputChanges),
workingDir = taskBuildDirectory.get().asFile, workingDir = taskBuildDirectory.get().asFile,
usePreciseJavaTracking = usePreciseJavaTracking, usePreciseJavaTracking = usePreciseJavaTracking,
disableMultiModuleIC = disableMultiModuleIC, disableMultiModuleIC = disableMultiModuleIC,
@@ -756,16 +745,9 @@ abstract class KotlinCompile @Inject constructor(
) )
} else null } else null
with(classpathSnapshotProperties) {
if (isIncrementalCompilationEnabled() && useClasspathSnapshot.get()) {
copyCurrentClasspathEntrySnapshotFiles()
}
}
val environment = GradleCompilerEnvironment( val environment = GradleCompilerEnvironment(
defaultCompilerClasspath, messageCollector, outputItemCollector, defaultCompilerClasspath, messageCollector, outputItemCollector,
// The compiler runner should not manage (read, modify, or delete) classpathSnapshotDir outputFiles = allOutputFiles(),
outputFiles = allOutputFiles().minus(classpathSnapshotProperties.classpathSnapshotDirFileCollection),
reportingSettings = reportingSettings, reportingSettings = reportingSettings,
incrementalCompilationEnvironment = icEnv, incrementalCompilationEnvironment = icEnv,
kotlinScriptExtensions = sourceFilesExtensions.get().toTypedArray() kotlinScriptExtensions = sourceFilesExtensions.get().toTypedArray()
@@ -848,62 +830,29 @@ abstract class KotlinCompile @Inject constructor(
return super.source(*sources) return super.source(*sources)
} }
private fun getClasspathChanges(inputChanges: InputChanges): ClasspathChanges { private fun getClasspathChanges(inputChanges: InputChanges): ClasspathChanges = when {
val fileChanges = inputChanges.getFileChanges(classpathSnapshotProperties.classpathSnapshot).toList() !classpathSnapshotProperties.useClasspathSnapshot.get() -> ClasspathSnapshotDisabled
return if (fileChanges.isEmpty()) { else -> {
ClasspathChanges.Available(LinkedHashSet(), LinkedHashSet()) val classpathSnapshotFiles = ClasspathSnapshotFiles(
} else { classpathSnapshotProperties.classpathSnapshot.files.toList(),
val previousClasspathEntrySnapshotFiles = getPreviousClasspathEntrySnapshotFiles() classpathSnapshotProperties.classpathSnapshotDir.get().asFile
if (previousClasspathEntrySnapshotFiles.isEmpty()) { )
// When this happens, it means that either the previous classpath was empty or there were no source files to compile in the when {
// previous non-incremental run so the task action was skipped and the classpath snapshot directory was not populated (see !inputChanges.isIncremental -> NotAvailableForNonIncrementalRun(classpathSnapshotFiles)
// AbstractKotlinCompile.executeImpl). inputChanges.getFileChanges(classpathSnapshotProperties.classpathSnapshot).none() -> Empty(classpathSnapshotFiles)
// We could improve this handling, but it's fine to return `UnableToCompute` here as it's likely that there are also no !classpathSnapshotFiles.shrunkPreviousClasspathSnapshotFile.exists() -> {
// source files to compile/recompile in this incremental run. // When this happens, it means that the classpath snapshot in the previous run was not saved for some reason. It's
ClasspathChanges.NotAvailable.UnableToCompute // likely that there were no source files to compile, so the task action was skipped (see
} else { // AbstractKotlinCompile.executeImpl), and therefore the classpath snapshot was not saved.
val currentClasspathEntrySnapshotFiles = classpathSnapshotProperties.classpathSnapshot.files.toList() // Missing classpath snapshot will make this run non-incremental, but because there were no source files to compile in
val changedCurrentFiles = fileChanges // the previous run, *all* source files in this run (if there are any) need to be compiled anyway, so being
.filter { it.changeType == ChangeType.ADDED || it.changeType == ChangeType.MODIFIED } // non-incremental is actually okay.
.map { it.file }.toSet() NotAvailableDueToMissingClasspathSnapshot(classpathSnapshotFiles)
ClasspathChangesComputer.compute( }
currentClasspathEntrySnapshotFiles = currentClasspathEntrySnapshotFiles, else -> ToBeComputedByIncrementalCompiler(classpathSnapshotFiles)
previousClasspathEntrySnapshotFiles = previousClasspathEntrySnapshotFiles,
unchangedCurrentClasspathEntrySnapshotFiles = currentClasspathEntrySnapshotFiles.filter { it !in changedCurrentFiles }
)
} }
} }
} }
/**
* Copies classpath entry snapshot files of the current build to `classpathSnapshotDir`. They will be used in the next build (see
* [getPreviousClasspathEntrySnapshotFiles]).
*
* To preserve their order, we put them in subdirectories with names being their indices, as shown below:
* classpathSnapshotDir/0/a-snapshot.bin
* classpathSnapshotDir/1/b-snapshot.bin
* ...
* classpathSnapshotDir/N-1/z-snapshot.bin
*/
private fun copyCurrentClasspathEntrySnapshotFiles() {
val snapshotFiles = classpathSnapshotProperties.classpathSnapshot.files.toList()
val classpathSnapshotDir = classpathSnapshotProperties.classpathSnapshotDir.get().asFile
classpathSnapshotDir.deleteRecursively()
classpathSnapshotDir.mkdirs()
snapshotFiles.forEachIndexed { index, snapshotFile ->
snapshotFile.copyTo(File("$classpathSnapshotDir/$index/${snapshotFile.name}"))
}
}
/**
* Returns classpath entry snapshot files of the previous build, stored in `classpathSnapshotDir` (see
* [copyCurrentClasspathEntrySnapshotFiles]).
*/
private fun getPreviousClasspathEntrySnapshotFiles(): List<File> {
val classpathSnapshotDir = classpathSnapshotProperties.classpathSnapshotDir.get().asFile
val subDirs = classpathSnapshotDir.listFiles()!!.sortedBy { it.name.toInt() }
return subDirs.map { it.listFiles()!!.single() }
}
} }
@CacheableTask @CacheableTask
@@ -1152,7 +1101,7 @@ abstract class Kotlin2JsCompile @Inject constructor(
logger.info(USING_JS_INCREMENTAL_COMPILATION_MESSAGE) logger.info(USING_JS_INCREMENTAL_COMPILATION_MESSAGE)
IncrementalCompilationEnvironment( IncrementalCompilationEnvironment(
getChangedFiles(inputChanges, incrementalProps), getChangedFiles(inputChanges, incrementalProps),
ClasspathChanges.NotAvailable.ForJSCompiler, ClasspathChanges.NotAvailableForJSCompiler,
taskBuildDirectory.get().asFile, taskBuildDirectory.get().asFile,
multiModuleICSettings = multiModuleICSettings multiModuleICSettings = multiModuleICSettings
) )