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:
committed by
teamcityserver
parent
dfaf195e1d
commit
f52be5f471
@@ -18,6 +18,7 @@ enum class BuildAttributeKind : Serializable {
|
||||
enum class BuildAttribute(val kind: BuildAttributeKind, val readableString: String) : Serializable {
|
||||
NO_BUILD_HISTORY(BuildAttributeKind.REBUILD_REASON, "Build history file 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"),
|
||||
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"),
|
||||
|
||||
+7
-3
@@ -10,9 +10,13 @@ import java.io.Serializable
|
||||
|
||||
@Suppress("Reformat")
|
||||
enum class BuildPerformanceMetric(val parent: BuildPerformanceMetric? = null, val readableString: String) : Serializable {
|
||||
OUTPUT_SIZE(readableString = "Total output size"),
|
||||
LOOKUP_SIZE(OUTPUT_SIZE, "Lookups size"),
|
||||
SNAPSHOT_SIZE(OUTPUT_SIZE, "ABI snapshot size"),
|
||||
CACHE_DIRECTORY_SIZE(readableString = "Total size of the cache directory"),
|
||||
LOOKUP_SIZE(CACHE_DIRECTORY_SIZE, "Lookups 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 {
|
||||
|
||||
@@ -27,6 +27,15 @@ enum class BuildTime(val parent: BuildTime? = null, val readableString: String)
|
||||
SET_UP_ABI_SNAPSHOTS(JAR_SNAPSHOT, "Set up ABI snapshot"),
|
||||
IC_ANALYZE_JAR_FILES(JAR_SNAPSHOT, "Analyze jar files"),
|
||||
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_FIND_HISTORY_FILES(IC_ANALYZE_CHANGES_IN_DEPENDENCIES, "Find 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"),
|
||||
NON_INCREMENTAL_ITERATION(INCREMENTAL_COMPILATION, "Non-incremental iteration"),
|
||||
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_INITIALIZATION(COMPILER_PERFORMANCE, "Compiler initialization time"),
|
||||
CODE_ANALYSIS(COMPILER_PERFORMANCE, "Compiler code analyse"),
|
||||
|
||||
@@ -5,70 +5,43 @@
|
||||
|
||||
package org.jetbrains.kotlin.incremental
|
||||
|
||||
import com.intellij.util.io.DataExternalizer
|
||||
import org.jetbrains.kotlin.incremental.storage.FqNameExternalizer
|
||||
import org.jetbrains.kotlin.incremental.storage.SetExternalizer
|
||||
import org.jetbrains.kotlin.incremental.storage.LookupSymbolExternalizer
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import java.io.*
|
||||
import org.jetbrains.kotlin.incremental.ClasspathChanges.ClasspathSnapshotEnabled
|
||||
import java.io.File
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* Changes to the classpath of the `KotlinCompile` task, used to compute the source files that need to be recompiled during an incremental
|
||||
* run.
|
||||
* Changes to the classpath of the `KotlinCompile` task, or information to compute them later by the Kotlin incremental compiler (see
|
||||
* [ClasspathSnapshotEnabled.ToBeComputedByIncrementalCompiler].
|
||||
*/
|
||||
sealed class ClasspathChanges : Serializable {
|
||||
|
||||
class Available() : ClasspathChanges() {
|
||||
sealed class ClasspathSnapshotEnabled : ClasspathChanges() {
|
||||
|
||||
companion object {
|
||||
private const val serialVersionUID = 0L
|
||||
}
|
||||
abstract val classpathSnapshotFiles: ClasspathSnapshotFiles
|
||||
|
||||
lateinit var lookupSymbols: Set<LookupSymbol> // Preferably ordered but not required
|
||||
private set
|
||||
class Empty(override val classpathSnapshotFiles: ClasspathSnapshotFiles) : ClasspathSnapshotEnabled()
|
||||
|
||||
lateinit var fqNames: Set<FqName> // Preferably ordered but not required
|
||||
private set
|
||||
class ToBeComputedByIncrementalCompiler(override val classpathSnapshotFiles: ClasspathSnapshotFiles) : ClasspathSnapshotEnabled()
|
||||
|
||||
constructor(lookupSymbols: Set<LookupSymbol>, fqNames: Set<FqName>) : this() {
|
||||
this.lookupSymbols = lookupSymbols
|
||||
this.fqNames = fqNames
|
||||
}
|
||||
class NotAvailableDueToMissingClasspathSnapshot(override val classpathSnapshotFiles: ClasspathSnapshotFiles) :
|
||||
ClasspathSnapshotEnabled()
|
||||
|
||||
private fun writeObject(output: ObjectOutputStream) {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
class NotAvailableForNonIncrementalRun(override val classpathSnapshotFiles: ClasspathSnapshotFiles) : ClasspathSnapshotEnabled()
|
||||
}
|
||||
|
||||
sealed class NotAvailable : ClasspathChanges() {
|
||||
object UnableToCompute : NotAvailable()
|
||||
object ForNonIncrementalRun : NotAvailable()
|
||||
object ClasspathSnapshotIsDisabled : NotAvailable()
|
||||
object ReservedForTestsOnly : NotAvailable()
|
||||
object ForJSCompiler : NotAvailable()
|
||||
}
|
||||
object ClasspathSnapshotDisabled : ClasspathChanges()
|
||||
|
||||
object NotAvailableForJSCompiler : ClasspathChanges()
|
||||
}
|
||||
|
||||
private object ClasspathChangesAvailableExternalizer : DataExternalizer<ClasspathChanges.Available> {
|
||||
class ClasspathSnapshotFiles(
|
||||
val currentClasspathEntrySnapshotFiles: List<File>,
|
||||
classpathSnapshotDir: File
|
||||
) : Serializable {
|
||||
|
||||
override fun save(output: DataOutput, classpathChanges: ClasspathChanges.Available) {
|
||||
SetExternalizer(LookupSymbolExternalizer).save(output, classpathChanges.lookupSymbols)
|
||||
SetExternalizer(FqNameExternalizer).save(output, classpathChanges.fqNames)
|
||||
}
|
||||
val shrunkPreviousClasspathSnapshotFile: File = File(classpathSnapshotDir, "shrunk-classpath-snapshot.bin")
|
||||
|
||||
override fun read(input: DataInput): ClasspathChanges.Available {
|
||||
return ClasspathChanges.Available(
|
||||
lookupSymbols = SetExternalizer(LookupSymbolExternalizer).read(input),
|
||||
fqNames = SetExternalizer(FqNameExternalizer).read(input)
|
||||
)
|
||||
companion object {
|
||||
private const val serialVersionUID = 0L
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ import java.util.*
|
||||
|
||||
open class LookupStorage(
|
||||
targetDataDir: File,
|
||||
pathConverter: FileToPathConverter
|
||||
pathConverter: FileToPathConverter,
|
||||
storeFullFqNames: Boolean = false
|
||||
) : BasicMapsOwner(targetDataDir) {
|
||||
val LOG = Logger.getInstance("#org.jetbrains.kotlin.jps.build.KotlinBuilder")
|
||||
|
||||
@@ -44,7 +45,7 @@ open class LookupStorage(
|
||||
private val countersFile = "counters".storageFile
|
||||
private val idToFile = registerMap(IdToFileMap("id-to-file".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
|
||||
private var size: Int = 0
|
||||
|
||||
@@ -18,7 +18,9 @@ package org.jetbrains.kotlin.incremental.storage
|
||||
|
||||
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 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.IOUtil
|
||||
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.FqName
|
||||
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
|
||||
import java.io.DataInput
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutput
|
||||
import java.io.*
|
||||
|
||||
/**
|
||||
* Storage versioning:
|
||||
* 0 - only name and value hashes 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 {
|
||||
val version = input.readByte()
|
||||
return when (version.toInt()) {
|
||||
@@ -51,14 +50,12 @@ object LookupSymbolKeyDescriptor : KeyDescriptor<LookupSymbolKey> {
|
||||
val second = input.readInt()
|
||||
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) {
|
||||
if (storeFullFqName) {
|
||||
if (storeFullFqNames) {
|
||||
output.writeByte(0)
|
||||
output.writeUTF(value.name)
|
||||
output.writeUTF(value.scope)
|
||||
@@ -74,18 +71,6 @@ object LookupSymbolKeyDescriptor : KeyDescriptor<LookupSymbolKey> {
|
||||
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> {
|
||||
|
||||
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> {
|
||||
override fun save(output: DataOutput, value: Int) = output.writeInt(value)
|
||||
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 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.LookupSymbol
|
||||
import org.jetbrains.kotlin.incremental.testingUtils.assertEqualDirectories
|
||||
@@ -48,7 +50,11 @@ class RelocatableCachesTest : TestWithWorkingDir() {
|
||||
private fun fillLookupStorage(projectRoot: File, reverseFiles: Boolean, reverseLookups: Boolean) {
|
||||
val storageRoot = projectRoot.storageRoot
|
||||
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 symbols = LinkedHashSet<LookupSymbol>()
|
||||
val lookups = MultiMap.createOrderedSet<LookupSymbol, String>()
|
||||
|
||||
@@ -15,6 +15,7 @@ dependencies {
|
||||
api(project(":compiler:cli-js"))
|
||||
api(project(":kotlin-build-common"))
|
||||
api(project(":daemon-common"))
|
||||
implementation("com.google.code.gson:gson:${rootProject.extra["versions.jar.gson"]}")
|
||||
compileOnly(intellijCoreDep()) { includeJars("intellij-core") }
|
||||
|
||||
testApi(commonDep("junit:junit"))
|
||||
|
||||
+9
-7
@@ -26,8 +26,8 @@ import java.io.File
|
||||
abstract class IncrementalCachesManager<PlatformCache : AbstractIncrementalCache<*>>(
|
||||
cachesRootDir: File,
|
||||
rootProjectDir: File?,
|
||||
protected val reporter: ICReporter
|
||||
) {
|
||||
protected val reporter: ICReporter,
|
||||
storeFullFqNamesInLookupCache: Boolean = false) {
|
||||
val pathConverter = IncrementalFileToPathConverter(rootProjectDir)
|
||||
private val caches = arrayListOf<BasicMapsOwner>()
|
||||
|
||||
@@ -43,7 +43,7 @@ abstract class IncrementalCachesManager<PlatformCache : AbstractIncrementalCache
|
||||
private val lookupCacheDir = File(cachesRootDir, "lookups").apply { mkdirs() }
|
||||
|
||||
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
|
||||
|
||||
@Synchronized
|
||||
@@ -79,8 +79,9 @@ class IncrementalJvmCachesManager(
|
||||
cacheDirectory: File,
|
||||
rootProjectDir: File?,
|
||||
outputDir: File,
|
||||
reporter: ICReporter
|
||||
) : IncrementalCachesManager<IncrementalJvmCache>(cacheDirectory, rootProjectDir, reporter) {
|
||||
reporter: ICReporter,
|
||||
storeFullFqNamesInLookupCache: Boolean = false
|
||||
) : IncrementalCachesManager<IncrementalJvmCache>(cacheDirectory, rootProjectDir, reporter, storeFullFqNamesInLookupCache) {
|
||||
private val jvmCacheDir = File(cacheDirectory, "jvm").apply { mkdirs() }
|
||||
override val platformCache = IncrementalJvmCache(jvmCacheDir, outputDir, pathConverter).apply { registerCache() }
|
||||
}
|
||||
@@ -89,8 +90,9 @@ class IncrementalJsCachesManager(
|
||||
cachesRootDir: File,
|
||||
rootProjectDir: File?,
|
||||
reporter: ICReporter,
|
||||
serializerProtocol: SerializerExtensionProtocol
|
||||
) : IncrementalCachesManager<IncrementalJsCache>(cachesRootDir, rootProjectDir, reporter) {
|
||||
serializerProtocol: SerializerExtensionProtocol,
|
||||
storeFullFqNamesInLookupCache: Boolean
|
||||
) : IncrementalCachesManager<IncrementalJsCache>(cachesRootDir, rootProjectDir, reporter, storeFullFqNamesInLookupCache) {
|
||||
private val jsCacheFile = File(cachesRootDir, "js").apply { mkdirs() }
|
||||
override val platformCache = IncrementalJsCache(jsCacheFile, pathConverter, serializerProtocol).apply { registerCache() }
|
||||
}
|
||||
+38
-32
@@ -19,9 +19,9 @@ package org.jetbrains.kotlin.incremental
|
||||
import org.jetbrains.kotlin.build.DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS
|
||||
import org.jetbrains.kotlin.build.GeneratedFile
|
||||
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.BuildPerformanceMetric
|
||||
import org.jetbrains.kotlin.build.report.metrics.BuildTime
|
||||
import org.jetbrains.kotlin.build.report.metrics.measure
|
||||
import org.jetbrains.kotlin.cli.common.*
|
||||
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 java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
abstract class IncrementalCompilerRunner<
|
||||
Args : CommonCompilerArguments,
|
||||
@@ -108,7 +106,7 @@ abstract class IncrementalCompilerRunner<
|
||||
caches.close(false)
|
||||
// todo: we can recompile all files incrementally (not cleaning caches), so rebuild won't propagate
|
||||
reporter.measure(BuildTime.CLEAR_OUTPUT_ON_REBUILD) {
|
||||
clearLocalStateOnRebuild(args)
|
||||
clearOutputsOnRebuild(args)
|
||||
}
|
||||
caches = createCacheManager(args, projectDir)
|
||||
if (providedChangedFiles == null) {
|
||||
@@ -134,7 +132,6 @@ abstract class IncrementalCompilerRunner<
|
||||
else -> providedChangedFiles
|
||||
}
|
||||
|
||||
|
||||
val compilationMode = sourcesToCompile(caches, changedFiles, args, messageCollector, classpathAbiSnapshot)
|
||||
|
||||
val exitCode = when (compilationMode) {
|
||||
@@ -162,7 +159,8 @@ abstract class IncrementalCompilerRunner<
|
||||
allSourceFiles,
|
||||
compilationMode,
|
||||
messageCollector,
|
||||
withSnapshot)
|
||||
withSnapshot
|
||||
)
|
||||
}
|
||||
}
|
||||
is CompilationMode.Rebuild -> {
|
||||
@@ -170,6 +168,8 @@ abstract class IncrementalCompilerRunner<
|
||||
}
|
||||
}
|
||||
|
||||
performWorkAfterCompilation(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,
|
||||
// but now JsKlib compiler reports invalid exit code.
|
||||
@@ -182,7 +182,7 @@ abstract class IncrementalCompilerRunner<
|
||||
)
|
||||
if (cacheDirectory.exists() && cacheDirectory.isDirectory()) {
|
||||
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)
|
||||
} finally {
|
||||
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 (outputDirsThatExist, regularOrNonExistentOutputFiles) = outputFiles.partition { it.isDirectory }
|
||||
val regularOutputFiles = regularOrNonExistentOutputFiles.filter { it.exists() }
|
||||
|
||||
reporter.reportVerbose { "Clearing output on rebuild" }
|
||||
for (file in sequenceOf(destinationDir, workingDir) + outputFiles.asSequence()) {
|
||||
val deleted: Boolean? = when {
|
||||
file.isDirectory -> {
|
||||
reporter.reportVerbose { " Deleting directory $file" }
|
||||
file.deleteRecursively()
|
||||
}
|
||||
file.isFile -> {
|
||||
reporter.reportVerbose { " Deleting $file" }
|
||||
file.delete()
|
||||
}
|
||||
else -> null
|
||||
// outputDirsThatExist may or may not contain destinationDir and workingDir.
|
||||
// Collect all of them so that we don't miss any output directories.
|
||||
// Use Set to avoid duplication.
|
||||
val allOutputDirs = setOf(destinationDir, workingDir) + outputDirsThatExist
|
||||
|
||||
reporter.reportVerbose { "Clearing outputs on rebuild" }
|
||||
allOutputDirs.forEach { dir ->
|
||||
reporter.reportVerbose { " Deleting contents of directory '${dir.path}'" }
|
||||
dir.listFiles()?.forEach {
|
||||
it.deleteRecursively()
|
||||
if (it.exists()) throw IOException("Could not delete '${it.path}'")
|
||||
}
|
||||
|
||||
if (deleted == false) {
|
||||
reporter.reportVerbose { " Could not delete $file" }
|
||||
}
|
||||
dir.mkdirs()
|
||||
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(
|
||||
@@ -393,9 +398,8 @@ abstract class IncrementalCompilerRunner<
|
||||
caches.platformCache.updateComplementaryFiles(dirtySources, expectActualTracker)
|
||||
caches.inputsCache.registerOutputForSourceFiles(generatedFiles)
|
||||
caches.lookupCache.update(lookupTracker, sourcesToCompile, removedKotlinSources)
|
||||
updateCaches(services, caches, generatedFiles, changesCollector)
|
||||
|
||||
}
|
||||
updateCaches(services, caches, generatedFiles, changesCollector)
|
||||
}
|
||||
if (compilationMode is CompilationMode.Rebuild) {
|
||||
if (withSnapshot) {
|
||||
abiSnapshot.protos.putAll(changesCollector.protoDataChanges())
|
||||
@@ -498,6 +502,8 @@ abstract class IncrementalCompilerRunner<
|
||||
BuildDiffsStorage.writeToFile(buildHistoryFile, BuildDiffsStorage(prevDiffs + newDiff), reporter)
|
||||
}
|
||||
|
||||
protected open fun performWorkAfterCompilation(caches: CacheManager) {}
|
||||
|
||||
companion object {
|
||||
const val DIRTY_SOURCES_FILE_NAME = "dirty-sources.txt"
|
||||
const val LAST_BUILD_INFO_FILE_NAME = "last-build.bin"
|
||||
|
||||
+8
-3
@@ -20,9 +20,8 @@ import org.jetbrains.kotlin.build.GeneratedFile
|
||||
import org.jetbrains.kotlin.build.report.BuildReporter
|
||||
import org.jetbrains.kotlin.build.report.ICReporter
|
||||
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.cli.common.*
|
||||
import org.jetbrains.kotlin.cli.common.ExitCode
|
||||
import org.jetbrains.kotlin.cli.common.arguments.K2JSCompilerArguments
|
||||
import org.jetbrains.kotlin.cli.common.arguments.isIrBackendEnabled
|
||||
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
|
||||
@@ -92,7 +91,13 @@ class IncrementalJsCompilerRunner(
|
||||
|
||||
override fun createCacheManager(args: K2JSCompilerArguments, projectDir: File?): IncrementalJsCachesManager {
|
||||
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 {
|
||||
|
||||
+75
-24
@@ -41,17 +41,18 @@ import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
|
||||
import org.jetbrains.kotlin.config.CompilerConfiguration
|
||||
import org.jetbrains.kotlin.config.IncrementalCompilation
|
||||
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.LookupTracker
|
||||
import org.jetbrains.kotlin.incremental.multiproject.EmptyModulesApiHistory
|
||||
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.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.kotlin.header.KotlinClassHeader
|
||||
import org.jetbrains.kotlin.load.kotlin.incremental.components.IncrementalCompilationComponents
|
||||
@@ -88,7 +89,7 @@ fun makeIncrementally(
|
||||
buildHistoryFile = buildHistoryFile,
|
||||
modulesApiHistory = EmptyModulesApiHistory,
|
||||
kotlinSourceFilesExtensions = kotlinExtensions,
|
||||
classpathChanges = ReservedForTestsOnly
|
||||
classpathChanges = ClasspathSnapshotDisabled
|
||||
)
|
||||
//TODO set properly
|
||||
compiler.compile(sourceFiles, args, messageCollector, providedChangedFiles = null)
|
||||
@@ -123,7 +124,7 @@ class IncrementalJvmCompilerRunner(
|
||||
outputFiles: Collection<File>,
|
||||
private val modulesApiHistory: ModulesApiHistory,
|
||||
override val kotlinSourceFilesExtensions: List<String> = DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS,
|
||||
private val classpathChanges: ClasspathChanges,
|
||||
private val classpathChanges: ClasspathChanges
|
||||
) : IncrementalCompilerRunner<K2JVMCompilerArguments, IncrementalJvmCachesManager>(
|
||||
workingDir,
|
||||
"caches-jvm",
|
||||
@@ -135,7 +136,13 @@ class IncrementalJvmCompilerRunner(
|
||||
IncrementalCompilation.isEnabledForJvm()
|
||||
|
||||
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 =
|
||||
args.destinationAsFile
|
||||
@@ -203,6 +210,10 @@ class IncrementalJvmCompilerRunner(
|
||||
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(
|
||||
caches: IncrementalJvmCachesManager,
|
||||
changedFiles: ChangedFiles.Known,
|
||||
@@ -212,26 +223,34 @@ class IncrementalJvmCompilerRunner(
|
||||
val dirtyFiles = DirtyFilesContainer(caches, reporter, kotlinSourceFilesExtensions)
|
||||
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) {
|
||||
// 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 ClasspathChanges.NotAvailable -> when (classpathChanges) {
|
||||
is UnableToCompute, is ClasspathSnapshotIsDisabled, is ReservedForTestsOnly -> {
|
||||
reporter.measure(BuildTime.IC_ANALYZE_CHANGES_IN_DEPENDENCIES) {
|
||||
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 ForNonIncrementalRun, is ForJSCompiler -> {
|
||||
error("Unexpected type for this code path: ${classpathChanges.javaClass.name}.")
|
||||
is Empty -> ChangesEither.Known(emptySet(), emptySet())
|
||||
is ToBeComputedByIncrementalCompiler -> reporter.measure(BuildTime.COMPUTE_CLASSPATH_CHANGES) {
|
||||
check(currentClasspathSnapshot == null)
|
||||
currentClasspathSnapshot = reporter.measure(BuildTime.LOAD_CURRENT_CLASSPATH_SNAPSHOT) {
|
||||
CachedClasspathSnapshotSerializer.load(classpathChanges.classpathSnapshotFiles.currentClasspathEntrySnapshotFiles)
|
||||
}
|
||||
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
|
||||
@@ -434,6 +453,38 @@ class IncrementalJvmCompilerRunner(
|
||||
reportPerformanceData(compiler.defaultPerformanceManager)
|
||||
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
|
||||
|
||||
+4
-4
@@ -5,12 +5,12 @@
|
||||
|
||||
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.name.ClassId
|
||||
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(
|
||||
|
||||
/** Set of changed classes, preferably ordered by not required. */
|
||||
@@ -73,7 +73,7 @@ class ChangeSet(
|
||||
changedTopLevelMembers + other.changedTopLevelMembers
|
||||
)
|
||||
|
||||
fun toClasspathChanges(): ClasspathChanges.Available {
|
||||
internal fun getChanges(): ChangesEither.Known {
|
||||
val lookupSymbols = mutableSetOf<LookupSymbol>()
|
||||
val fqNames = mutableSetOf<FqName>()
|
||||
|
||||
@@ -98,6 +98,6 @@ class ChangeSet(
|
||||
fqNames.add(changedPackage)
|
||||
}
|
||||
|
||||
return ClasspathChanges.Available(lookupSymbols, fqNames)
|
||||
return ChangesEither.Known(lookupSymbols, fqNames)
|
||||
}
|
||||
}
|
||||
|
||||
+161
-199
@@ -6,9 +6,14 @@
|
||||
package org.jetbrains.kotlin.incremental.classpathDiff
|
||||
|
||||
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.classpathDiff.ImpactAnalysis.computeImpactedSet
|
||||
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.supertypes
|
||||
import org.jetbrains.kotlin.name.ClassId
|
||||
@@ -18,156 +23,101 @@ import org.jetbrains.kotlin.serialization.deserialization.getClassId
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
/** Computes [ClasspathChanges] between two [ClasspathSnapshot]s .*/
|
||||
/** Computes changes between two [ClasspathSnapshot]s .*/
|
||||
object ClasspathChangesComputer {
|
||||
|
||||
fun compute(
|
||||
currentClasspathEntrySnapshotFiles: List<File>,
|
||||
previousClasspathEntrySnapshotFiles: List<File>,
|
||||
unchangedCurrentClasspathEntrySnapshotFiles: List<File>
|
||||
): ClasspathChanges {
|
||||
// To improve performance, we will compute changes for the changed snapshot files only, ignoring unchanged ones. (Duplicate classes
|
||||
// will make this a bit tricky, but it will be dealt with below.)
|
||||
// First, align unchanged snapshot files in the current classpath with unchanged snapshot files in the previous classpath. Gradle
|
||||
// has this information, but doesn't expose it, so we have to reconstruct it here.
|
||||
val unchangedCurrentToPreviousAlignment: Map<File, File> =
|
||||
alignUnchangedSnapshotFiles(unchangedCurrentClasspathEntrySnapshotFiles, previousClasspathEntrySnapshotFiles)
|
||||
|
||||
// Use sets to make presence checks faster
|
||||
val unchangedCurrentFiles: Set<File> = unchangedCurrentToPreviousAlignment.keys
|
||||
val unchangedPreviousFiles: Set<File> = unchangedCurrentToPreviousAlignment.values.toSet()
|
||||
|
||||
// We will split the current files into 2 groups:
|
||||
// 1a) Unchanged current files
|
||||
// 1b) Added files
|
||||
// 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)
|
||||
}
|
||||
/**
|
||||
* Computes changes between the current and previous [ClasspathSnapshot]s, plus unchanged elements that are impacted by the changes.
|
||||
*
|
||||
* NOTE: The original classpath may contain duplicate classes, but the shrunk classpath must not contain duplicate classes.
|
||||
*/
|
||||
fun computeChangedAndImpactedSet(
|
||||
currentClasspathSnapshot: ClasspathSnapshot,
|
||||
lookupStorageInPreviousRun: LookupStorage,
|
||||
shrunkPreviousClasspathSnapshotFile: File,
|
||||
metrics: BuildMetricsReporter
|
||||
): ChangeSet {
|
||||
val shrunkCurrentClasspathSnapshot = metrics.measure(BuildTime.SHRINK_CURRENT_CLASSPATH_SNAPSHOT) {
|
||||
ClasspathSnapshotShrinker.shrink(currentClasspathSnapshot, lookupStorageInPreviousRun, metrics)
|
||||
}
|
||||
val shrunkPreviousClasspathSnapshot = metrics.measure(BuildTime.LOAD_SHRUNK_PREVIOUS_CLASSPATH_SNAPSHOT) {
|
||||
ListExternalizer(ClassSnapshotWithHashExternalizer).loadFromFile(shrunkPreviousClasspathSnapshotFile)
|
||||
}
|
||||
return metrics.measure(BuildTime.COMPUTE_CHANGED_AND_IMPACTED_SET) {
|
||||
computeChangedAndImpactedSet(shrunkCurrentClasspathSnapshot, shrunkPreviousClasspathSnapshot, metrics)
|
||||
}
|
||||
|
||||
// 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
|
||||
* snapshot files of the previous build).
|
||||
* Computes changes between the current and previous lists of classes, plus unchanged elements that are impacted by the changes.
|
||||
*
|
||||
* 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
|
||||
* 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.
|
||||
* NOTE: Each list of classes must not contain duplicates.
|
||||
*/
|
||||
private fun alignUnchangedSnapshotFiles(unchangedCurrentSnapshotFiles: List<File>, previousSnapshotFiles: List<File>): Map<File, File> {
|
||||
val sizeToPreviousFiles: Map<Long, List<IndexedValue<File>>> = previousSnapshotFiles.withIndex().groupBy { it.value.length() }
|
||||
fun computeChangedAndImpactedSet(
|
||||
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
|
||||
return unchangedCurrentSnapshotFiles.associateWith { unchangedCurrentFile ->
|
||||
val candidates = (sizeToPreviousFiles[unchangedCurrentFile.length()] ?: emptyList()).filter { it.index >= startIndexToSearch }
|
||||
val unchangedPreviousFileWithIndex: IndexedValue<File> = if (candidates.size == 1) {
|
||||
// A matching file must exist, so if there is only one candidate, it is the one.
|
||||
candidates.single()
|
||||
} else {
|
||||
// If there are multiple matching files, select the first one. (Even if it doesn't match Gradle's alignment, it is still a
|
||||
// correct alignment.)
|
||||
val unchangedContents = unchangedCurrentFile.readBytes()
|
||||
candidates.firstOrNull { candidate ->
|
||||
unchangedContents.contentEquals(candidate.value.readBytes())
|
||||
} ?: error("Can't find previous snapshot file of unchanged current snapshot file '${unchangedCurrentFile.path}'")
|
||||
}
|
||||
startIndexToSearch = unchangedPreviousFileWithIndex.index + 1
|
||||
unchangedPreviousFileWithIndex.value
|
||||
val changedCurrentClasses: List<ClassSnapshot> = currentClasses.filter { (classId, currentClass) ->
|
||||
val previousClass = previousClasses[classId]
|
||||
previousClass == null || currentClass.hash != previousClass.hash
|
||||
}.map { it.value.classSnapshot }
|
||||
|
||||
val changedPreviousClasses: List<ClassSnapshot> = previousClasses.filter { (classId, previousClass) ->
|
||||
val currentClass = currentClasses[classId]
|
||||
currentClass == null || currentClass.hash != previousClass.hash
|
||||
}.map { it.value.classSnapshot }
|
||||
|
||||
val classChanges = metrics.measure(BuildTime.COMPUTE_CLASS_CHANGES) {
|
||||
computeClassChanges(changedCurrentClasses, changedPreviousClasses, metrics)
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns `true` if this snapshot file contains a duplicate class with another snapshot file in the given list. */
|
||||
@Suppress("unused", "UNUSED_PARAMETER")
|
||||
private fun File.containsDuplicatesWith(otherSnapshotFiles: List<File>): Boolean {
|
||||
// FIXME: Implement and optimize this method
|
||||
return false
|
||||
}
|
||||
if (classChanges.isEmpty()) {
|
||||
return classChanges
|
||||
}
|
||||
|
||||
fun compute(currentClasspathSnapshot: ClasspathSnapshot, previousClasspathSnapshot: ClasspathSnapshot): ClasspathChanges {
|
||||
val currentClassSnapshots = currentClasspathSnapshot.getNonDuplicateClassSnapshots()
|
||||
val previousClassSnapshots = previousClasspathSnapshot.getNonDuplicateClassSnapshots()
|
||||
|
||||
return computeClassChanges(currentClassSnapshots, previousClassSnapshots)
|
||||
return metrics.measure(BuildTime.COMPUTE_IMPACTED_SET) {
|
||||
computeImpactedSet(classChanges, previousClasses.map { it.value.classSnapshot })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
val classSnapshots = LinkedHashMap<String, ClassSnapshot>(classpathEntrySnapshots.sumOf { it.classSnapshots.size })
|
||||
for (classpathEntrySnapshot in classpathEntrySnapshots) {
|
||||
for ((unixStyleRelativePath, classSnapshot) in classpathEntrySnapshot.classSnapshots) {
|
||||
classSnapshots.putIfAbsent(unixStyleRelativePath, classSnapshot)
|
||||
fun computeClassChanges(
|
||||
currentClassSnapshots: List<ClassSnapshot>,
|
||||
previousClassSnapshots: List<ClassSnapshot>,
|
||||
metrics: BuildMetricsReporter
|
||||
): 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)
|
||||
|
||||
/**
|
||||
* Computes changes between two lists of [ClassSnapshot]s.
|
||||
*
|
||||
* 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
|
||||
val kotlinClassChanges = metrics.measure(BuildTime.COMPUTE_KOTLIN_CLASS_CHANGES) {
|
||||
computeChangesForProtoBasedSnapshots(currentProtoBasedSnapshots, previousProtoBasedSnapshots)
|
||||
}
|
||||
|
||||
// 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")
|
||||
val changeSet2 = JavaClassChangesComputer.compute(
|
||||
currentAsmBasedSnapshots as List<RegularJavaClassSnapshot>,
|
||||
previousAsmBasedSnapshots as List<RegularJavaClassSnapshot>
|
||||
)
|
||||
|
||||
val allChanges = changeSet1 + changeSet2
|
||||
if (allChanges.isEmpty()) {
|
||||
return allChanges.toClasspathChanges()
|
||||
val javaClassChanges = metrics.measure(BuildTime.COMPUTE_JAVA_CLASS_CHANGES) {
|
||||
JavaClassChangesComputer.compute(
|
||||
currentAsmBasedSnapshots as List<RegularJavaClassSnapshot>,
|
||||
previousAsmBasedSnapshots as List<RegularJavaClassSnapshot>
|
||||
)
|
||||
}
|
||||
|
||||
val impactedSet = computeImpactedSet(allChanges, previousNonEmptyClassSnapshots)
|
||||
|
||||
return impactedSet.toClasspathChanges()
|
||||
return kotlinClassChanges + javaClassChanges
|
||||
}
|
||||
|
||||
private fun computeChangesForProtoBasedSnapshots(
|
||||
@@ -203,7 +153,7 @@ object ClasspathChangesComputer {
|
||||
)
|
||||
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}")
|
||||
}
|
||||
}
|
||||
@@ -233,7 +183,7 @@ object ClasspathChangesComputer {
|
||||
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}")
|
||||
}
|
||||
}
|
||||
@@ -255,7 +205,7 @@ object ClasspathChangesComputer {
|
||||
|
||||
private fun DirtyData.normalize(currentClassSnapshots: List<ClassSnapshot>, previousClassSnapshots: List<ClassSnapshot>): ChangeSet {
|
||||
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 ->
|
||||
val fqName = classId.asSingleFqName()
|
||||
check(!fqNameToClassId.contains(fqName)) {
|
||||
@@ -264,68 +214,70 @@ object ClasspathChangesComputer {
|
||||
fqNameToClassId[fqName] = classId
|
||||
}
|
||||
|
||||
return ChangeSet.Collector().run {
|
||||
val changes = ChangeSet.Collector().run {
|
||||
dirtyLookupSymbols.forEach {
|
||||
fqNameToClassId[FqName(it.scope)]?.let { classIdOfScope ->
|
||||
// If scope is a class, lookup symbol is a class member and maybe inner class
|
||||
fqNameToClassId[FqName("${it.scope}.${it.name}")]?.let { innerClass ->
|
||||
addChangedClass(innerClass)
|
||||
} ?: addChangedClassMember(classIdOfScope, it.name)
|
||||
return@forEach
|
||||
val lookupSymbolFqName = if (it.scope.isEmpty()) FqName(it.name) else FqName("${it.scope}.${it.name}")
|
||||
val lookupSymbolClassId: ClassId? = fqNameToClassId[lookupSymbolFqName]
|
||||
if (lookupSymbolClassId != null) {
|
||||
addChangedClass(lookupSymbolClassId)
|
||||
} else {
|
||||
// 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()
|
||||
|
||||
// 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
|
||||
getChanges()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
internal 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.
|
||||
*
|
||||
* The returned set is also a [ChangeSet], which includes the given changes plus the impacted ones.
|
||||
*/
|
||||
fun computeImpactedSet(changes: ChangeSet, previousClassSnapshots: List<ClassSnapshot>): ChangeSet {
|
||||
val classIdToSubclasses = getClassIdToSubclassesMap(previousClassSnapshots)
|
||||
val impactedClassesResolver = { classId: ClassId -> classIdToSubclasses[classId] ?: emptySet() }
|
||||
|
||||
return ChangeSet.Collector().run {
|
||||
addChangedClasses(findSubclassesInclusive(changes.changedClasses, classIdToSubclasses))
|
||||
addChangedClasses(findImpactedClassesInclusive(changes.changedClasses, impactedClassesResolver))
|
||||
for ((changedClass, changedClassMembers) in changes.changedClassMembers) {
|
||||
findSubclassesInclusive(setOf(changedClass), classIdToSubclasses).forEach {
|
||||
findImpactedClassesInclusive(setOf(changedClass), impactedClassesResolver).forEach {
|
||||
addChangedClassMembers(it, changedClassMembers)
|
||||
}
|
||||
}
|
||||
@@ -337,7 +289,7 @@ private object ImpactAnalysis {
|
||||
}
|
||||
|
||||
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 classNameToClassIdResolver = { className: JvmClassName -> classNameToClassId[className] }
|
||||
|
||||
@@ -345,7 +297,7 @@ private object ImpactAnalysis {
|
||||
classSnapshots.forEach { classSnapshot ->
|
||||
val classId = classSnapshot.getClassId()
|
||||
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) {
|
||||
classIdToSubclasses.computeIfAbsent(supertype) { mutableSetOf() }.add(classId)
|
||||
}
|
||||
@@ -354,38 +306,17 @@ private object ImpactAnalysis {
|
||||
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
|
||||
* indirect subclasses.
|
||||
* Finds directly and transitively impacted classes of the given classes. The return set includes both the given classes and the
|
||||
* 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 toVisitClasses = classIds.toMutableSet()
|
||||
while (toVisitClasses.isNotEmpty()) {
|
||||
val nextToVisit = mutableSetOf<ClassId>()
|
||||
toVisitClasses.forEach {
|
||||
nextToVisit.addAll(classIdsToSubclasses[it] ?: emptyList())
|
||||
nextToVisit.addAll(impactedClassesResolver.invoke(it))
|
||||
}
|
||||
visitedClasses.addAll(toVisitClasses)
|
||||
toVisitClasses.clear()
|
||||
@@ -394,3 +325,34 @@ private object ImpactAnalysis {
|
||||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-2
@@ -7,6 +7,8 @@ package org.jetbrains.kotlin.incremental.classpathDiff
|
||||
|
||||
import org.jetbrains.kotlin.incremental.KotlinClassInfo
|
||||
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.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
|
||||
* 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,
|
||||
* 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. */
|
||||
class KotlinClassSnapshot(
|
||||
|
||||
+51
-38
@@ -10,32 +10,59 @@ import org.jetbrains.kotlin.incremental.JavaClassProtoMapValueExternalizer
|
||||
import org.jetbrains.kotlin.incremental.KotlinClassInfo
|
||||
import org.jetbrains.kotlin.incremental.storage.*
|
||||
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]. */
|
||||
object ClasspathSnapshotSerializer {
|
||||
object CachedClasspathSnapshotSerializer {
|
||||
private val cache = ConcurrentHashMap<File, ClasspathEntrySnapshot>()
|
||||
private const val RECOMMENDED_MAX_CACHE_SIZE = 100
|
||||
|
||||
fun load(classpathEntrySnapshotFiles: List<File>): ClasspathSnapshot {
|
||||
return ClasspathSnapshot(classpathEntrySnapshotFiles.map {
|
||||
ClasspathEntrySnapshotSerializer.load(it)
|
||||
})
|
||||
return ClasspathSnapshot(classpathEntrySnapshotFiles.map { snapshotFile ->
|
||||
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) {
|
||||
LinkedHashMapExternalizer(StringExternalizer, ClassSnapshotDataSerializer).save(output, snapshot.classSnapshots)
|
||||
LinkedHashMapExternalizer(StringExternalizer, ClassSnapshotWithHashExternalizer).save(output, snapshot.classSnapshots)
|
||||
}
|
||||
|
||||
override fun read(input: DataInput): 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) {
|
||||
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> {
|
||||
|
||||
override fun save(output: DataOutput, snapshot: KotlinClassSnapshot) {
|
||||
@@ -183,32 +225,3 @@ object ContentHashJavaClassSnapshotExternalizer : DataExternalizer<ContentHashJa
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+123
@@ -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()
|
||||
}
|
||||
+2
-2
@@ -38,10 +38,10 @@ object ClasspathEntrySnapshotter {
|
||||
}
|
||||
|
||||
val snapshots = try {
|
||||
ClassSnapshotter.snapshot(classes, protoBased)
|
||||
ClassSnapshotter.snapshot(classes, protoBased).map { it.addHash() }
|
||||
} catch (e: Throwable) {
|
||||
if (isKnownProblematicClasspathEntry(classpathEntry)) {
|
||||
classes.map { ContentHashJavaClassSnapshot(it.contents.md5()) }
|
||||
classes.map { ContentHashJavaClassSnapshot(it.contents.md5()).addHash() }
|
||||
} else throw e
|
||||
}
|
||||
|
||||
|
||||
+36
-16
@@ -5,11 +5,12 @@
|
||||
|
||||
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.snapshot
|
||||
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.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
@@ -46,7 +47,7 @@ class KotlinOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
|
||||
val sourceFile = SimpleKotlinClass(tmpDir)
|
||||
val previousSnapshot = sourceFile.compileAndSnapshot()
|
||||
val currentSnapshot = sourceFile.changePublicMethodSignature().compileAndSnapshot()
|
||||
val changes = ClasspathChangesComputer.computeClassChanges(listOf(currentSnapshot), listOf(previousSnapshot)).normalize()
|
||||
val changes = computeClassChanges(currentSnapshot, previousSnapshot)
|
||||
|
||||
Changes(
|
||||
lookupSymbols = setOf(
|
||||
@@ -62,7 +63,7 @@ class KotlinOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
|
||||
val sourceFile = SimpleKotlinClass(tmpDir)
|
||||
val previousSnapshot = sourceFile.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)
|
||||
}
|
||||
@@ -72,7 +73,7 @@ class KotlinOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
|
||||
val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testVariousAbiChanges/src/kotlin").canonicalFile
|
||||
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir)
|
||||
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir)
|
||||
val changes = ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot).normalize()
|
||||
val changes = computeClasspathChanges(currentSnapshot, previousSnapshot)
|
||||
|
||||
Changes(
|
||||
lookupSymbols = setOf(
|
||||
@@ -121,7 +122,7 @@ class KotlinOnlyClasspathChangesComputerTest : ClasspathChangesComputerTest() {
|
||||
File(testDataDir, "../ClasspathChangesComputerTest/testImpactAnalysis_KotlinOrJava/src/kotlin").canonicalFile
|
||||
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir)
|
||||
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir)
|
||||
val changes = ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot).normalize()
|
||||
val changes = computeClasspathChanges(currentSnapshot, previousSnapshot)
|
||||
|
||||
Changes(
|
||||
lookupSymbols = setOf(
|
||||
@@ -158,7 +159,7 @@ class JavaOnlyClasspathChangesComputerTest(private val protoBased: Boolean) : Cl
|
||||
val sourceFile = SimpleJavaClass(tmpDir)
|
||||
val previousSnapshot = sourceFile.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(
|
||||
lookupSymbols = setOf(
|
||||
@@ -174,7 +175,7 @@ class JavaOnlyClasspathChangesComputerTest(private val protoBased: Boolean) : Cl
|
||||
val sourceFile = SimpleJavaClass(tmpDir)
|
||||
val previousSnapshot = sourceFile.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)
|
||||
}
|
||||
@@ -184,7 +185,7 @@ class JavaOnlyClasspathChangesComputerTest(private val protoBased: Boolean) : Cl
|
||||
val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testVariousAbiChanges/src/java").canonicalFile
|
||||
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-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(
|
||||
lookupSymbols = setOf(
|
||||
@@ -229,7 +230,7 @@ class JavaOnlyClasspathChangesComputerTest(private val protoBased: Boolean) : Cl
|
||||
val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testImpactAnalysis_KotlinOrJava/src/java").canonicalFile
|
||||
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-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(
|
||||
lookupSymbols = setOf(
|
||||
@@ -259,7 +260,7 @@ class KotlinAndJavaClasspathChangesComputerTest : ClasspathSnapshotTestCommon()
|
||||
val classpathSourceDir = File(testDataDir, "../ClasspathChangesComputerTest/testImpactAnalysis_KotlinAndJava/src").canonicalFile
|
||||
val currentSnapshot = snapshotClasspath(File(classpathSourceDir, "current-classpath"), tmpDir)
|
||||
val previousSnapshot = snapshotClasspath(File(classpathSourceDir, "previous-classpath"), tmpDir)
|
||||
val changes = ClasspathChangesComputer.compute(currentSnapshot, previousSnapshot).normalize()
|
||||
val changes = computeClasspathChanges(currentSnapshot, previousSnapshot)
|
||||
|
||||
Changes(
|
||||
lookupSymbols = setOf(
|
||||
@@ -301,7 +302,7 @@ private fun snapshotClasspath(classpathSourceDir: File, tmpDir: TemporaryFolder,
|
||||
classpath.addAll(listOfNotNull(classFiles.firstOrNull()?.classRoot))
|
||||
|
||||
val relativePaths = classFiles.map { it.unixStyleRelativePath }
|
||||
val classSnapshots = classFiles.snapshotAll(protoBased)
|
||||
val classSnapshots = classFiles.snapshotAll(protoBased).map { it.addHash() }
|
||||
ClasspathEntrySnapshot(
|
||||
classSnapshots = relativePaths.zip(classSnapshots).toMap(LinkedHashMap())
|
||||
)
|
||||
@@ -309,12 +310,31 @@ private fun snapshotClasspath(classpathSourceDir: File, tmpDir: TemporaryFolder,
|
||||
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 fun ClasspathChanges.normalize(): Changes {
|
||||
this as ClasspathChanges.Available
|
||||
return Changes(lookupSymbols, fqNames.map { it.asString() }.toSet())
|
||||
private fun ChangeSet.normalize(): Changes {
|
||||
val changes: ChangesEither.Known = getChanges()
|
||||
return Changes(changes.lookupSymbols.toSet(), changes.fqNames.map { it.asString() }.toSet())
|
||||
}
|
||||
|
||||
private fun Changes.assertEquals(actual: Changes) {
|
||||
|
||||
+6
-4
@@ -6,6 +6,8 @@
|
||||
package org.jetbrains.kotlin.incremental.classpathDiff
|
||||
|
||||
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.Test
|
||||
|
||||
@@ -16,8 +18,8 @@ abstract class ClasspathSnapshotSerializerTest : ClasspathSnapshotTestCommon() {
|
||||
@Test
|
||||
open fun `test ClassSnapshotDataSerializer`() {
|
||||
val originalSnapshot = testSourceFile.compileAndSnapshot()
|
||||
val serializedSnapshot = ClassSnapshotDataSerializer.toByteArray(originalSnapshot)
|
||||
val deserializedSnapshot = ClassSnapshotDataSerializer.fromByteArray(serializedSnapshot)
|
||||
val serializedSnapshot = ClassSnapshotExternalizer.toByteArray(originalSnapshot)
|
||||
val deserializedSnapshot = ClassSnapshotExternalizer.fromByteArray(serializedSnapshot)
|
||||
|
||||
assertEquals(originalSnapshot.toGson(), deserializedSnapshot.toGson())
|
||||
}
|
||||
@@ -36,8 +38,8 @@ class JavaClassesClasspathSnapshotSerializerTest : ClasspathSnapshotSerializerTe
|
||||
val originalSnapshot = testSourceFile.compile().let {
|
||||
ClassSnapshotter.snapshot(listOf(ClassFileWithContents(it, it.readBytes())), includeDebugInfoInSnapshot = false)
|
||||
}.single()
|
||||
val serializedSnapshot = ClassSnapshotDataSerializer.toByteArray(originalSnapshot)
|
||||
val deserializedSnapshot = ClassSnapshotDataSerializer.fromByteArray(serializedSnapshot)
|
||||
val serializedSnapshot = ClassSnapshotExternalizer.toByteArray(originalSnapshot)
|
||||
val deserializedSnapshot = ClassSnapshotExternalizer.fromByteArray(serializedSnapshot)
|
||||
|
||||
assertEquals(originalSnapshot.toGson(), deserializedSnapshot.toGson())
|
||||
}
|
||||
|
||||
+9
@@ -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
|
||||
+1
-1
@@ -21,7 +21,7 @@ abstract class ClasspathSnapshotTestCommon {
|
||||
|
||||
companion object {
|
||||
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
|
||||
|
||||
+4
-3
@@ -9,8 +9,9 @@ import org.gradle.api.artifacts.transform.*
|
||||
import org.gradle.api.file.FileSystemLocation
|
||||
import org.gradle.api.provider.Provider
|
||||
import org.gradle.api.tasks.Classpath
|
||||
import org.jetbrains.kotlin.gradle.incremental.ClasspathEntrySnapshotSerializer
|
||||
import org.jetbrains.kotlin.gradle.incremental.ClasspathEntrySnapshotter
|
||||
import org.jetbrains.kotlin.incremental.classpathDiff.ClasspathEntrySnapshotExternalizer
|
||||
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). */
|
||||
@CacheableTransform
|
||||
@@ -25,6 +26,6 @@ abstract class ClasspathEntrySnapshotTransform : TransformAction<TransformParame
|
||||
val snapshotFile = outputs.file(classpathEntry.name.replace('.', '_') + "-snapshot.bin")
|
||||
|
||||
val snapshot = ClasspathEntrySnapshotter.snapshot(classpathEntry)
|
||||
ClasspathEntrySnapshotSerializer.save(snapshotFile, snapshot)
|
||||
ClasspathEntrySnapshotExternalizer.saveToFile(snapshotFile, snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
+26
-77
@@ -55,6 +55,9 @@ import org.jetbrains.kotlin.gradle.targets.js.ir.isProduceUnzippedKlib
|
||||
import org.jetbrains.kotlin.gradle.utils.*
|
||||
import org.jetbrains.kotlin.incremental.ChangedFiles
|
||||
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.library.impl.isKotlinLibrary
|
||||
import org.jetbrains.kotlin.statistics.BuildSessionLogger
|
||||
@@ -584,7 +587,6 @@ abstract class KotlinCompile @Inject constructor(
|
||||
)
|
||||
val classpathSnapshotDir = getClasspathSnapshotDir(task)
|
||||
task.classpathSnapshotProperties.classpathSnapshotDir.value(classpathSnapshotDir).disallowChanges()
|
||||
task.classpathSnapshotProperties.classpathSnapshotDirFileCollection.from(classpathSnapshotDir)
|
||||
} else {
|
||||
task.classpathSnapshotProperties.classpath.from(task.project.provider { task.classpath })
|
||||
}
|
||||
@@ -644,14 +646,6 @@ abstract class KotlinCompile @Inject constructor(
|
||||
@get:OutputDirectory
|
||||
@get:Optional // Set if useClasspathSnapshot == true
|
||||
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
|
||||
@@ -740,15 +734,10 @@ abstract class KotlinCompile @Inject constructor(
|
||||
val compilerRunner = compilerRunner.get()
|
||||
|
||||
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)
|
||||
IncrementalCompilationEnvironment(
|
||||
changedFiles = getChangedFiles(inputChanges, incrementalProps),
|
||||
classpathChanges = classpathChanges,
|
||||
classpathChanges = getClasspathChanges(inputChanges),
|
||||
workingDir = taskBuildDirectory.get().asFile,
|
||||
usePreciseJavaTracking = usePreciseJavaTracking,
|
||||
disableMultiModuleIC = disableMultiModuleIC,
|
||||
@@ -756,16 +745,9 @@ abstract class KotlinCompile @Inject constructor(
|
||||
)
|
||||
} else null
|
||||
|
||||
with(classpathSnapshotProperties) {
|
||||
if (isIncrementalCompilationEnabled() && useClasspathSnapshot.get()) {
|
||||
copyCurrentClasspathEntrySnapshotFiles()
|
||||
}
|
||||
}
|
||||
|
||||
val environment = GradleCompilerEnvironment(
|
||||
defaultCompilerClasspath, messageCollector, outputItemCollector,
|
||||
// The compiler runner should not manage (read, modify, or delete) classpathSnapshotDir
|
||||
outputFiles = allOutputFiles().minus(classpathSnapshotProperties.classpathSnapshotDirFileCollection),
|
||||
outputFiles = allOutputFiles(),
|
||||
reportingSettings = reportingSettings,
|
||||
incrementalCompilationEnvironment = icEnv,
|
||||
kotlinScriptExtensions = sourceFilesExtensions.get().toTypedArray()
|
||||
@@ -848,62 +830,29 @@ abstract class KotlinCompile @Inject constructor(
|
||||
return super.source(*sources)
|
||||
}
|
||||
|
||||
private fun getClasspathChanges(inputChanges: InputChanges): ClasspathChanges {
|
||||
val fileChanges = inputChanges.getFileChanges(classpathSnapshotProperties.classpathSnapshot).toList()
|
||||
return if (fileChanges.isEmpty()) {
|
||||
ClasspathChanges.Available(LinkedHashSet(), LinkedHashSet())
|
||||
} else {
|
||||
val previousClasspathEntrySnapshotFiles = getPreviousClasspathEntrySnapshotFiles()
|
||||
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
|
||||
// previous non-incremental run so the task action was skipped and the classpath snapshot directory was not populated (see
|
||||
// AbstractKotlinCompile.executeImpl).
|
||||
// We could improve this handling, but it's fine to return `UnableToCompute` here as it's likely that there are also no
|
||||
// source files to compile/recompile in this incremental run.
|
||||
ClasspathChanges.NotAvailable.UnableToCompute
|
||||
} else {
|
||||
val currentClasspathEntrySnapshotFiles = classpathSnapshotProperties.classpathSnapshot.files.toList()
|
||||
val changedCurrentFiles = fileChanges
|
||||
.filter { it.changeType == ChangeType.ADDED || it.changeType == ChangeType.MODIFIED }
|
||||
.map { it.file }.toSet()
|
||||
ClasspathChangesComputer.compute(
|
||||
currentClasspathEntrySnapshotFiles = currentClasspathEntrySnapshotFiles,
|
||||
previousClasspathEntrySnapshotFiles = previousClasspathEntrySnapshotFiles,
|
||||
unchangedCurrentClasspathEntrySnapshotFiles = currentClasspathEntrySnapshotFiles.filter { it !in changedCurrentFiles }
|
||||
)
|
||||
private fun getClasspathChanges(inputChanges: InputChanges): ClasspathChanges = when {
|
||||
!classpathSnapshotProperties.useClasspathSnapshot.get() -> ClasspathSnapshotDisabled
|
||||
else -> {
|
||||
val classpathSnapshotFiles = ClasspathSnapshotFiles(
|
||||
classpathSnapshotProperties.classpathSnapshot.files.toList(),
|
||||
classpathSnapshotProperties.classpathSnapshotDir.get().asFile
|
||||
)
|
||||
when {
|
||||
!inputChanges.isIncremental -> NotAvailableForNonIncrementalRun(classpathSnapshotFiles)
|
||||
inputChanges.getFileChanges(classpathSnapshotProperties.classpathSnapshot).none() -> Empty(classpathSnapshotFiles)
|
||||
!classpathSnapshotFiles.shrunkPreviousClasspathSnapshotFile.exists() -> {
|
||||
// When this happens, it means that the classpath snapshot in the previous run was not saved for some reason. It's
|
||||
// likely that there were no source files to compile, so the task action was skipped (see
|
||||
// AbstractKotlinCompile.executeImpl), and therefore the classpath snapshot was not saved.
|
||||
// Missing classpath snapshot will make this run non-incremental, but because there were no source files to compile in
|
||||
// the previous run, *all* source files in this run (if there are any) need to be compiled anyway, so being
|
||||
// non-incremental is actually okay.
|
||||
NotAvailableDueToMissingClasspathSnapshot(classpathSnapshotFiles)
|
||||
}
|
||||
else -> ToBeComputedByIncrementalCompiler(classpathSnapshotFiles)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -1152,7 +1101,7 @@ abstract class Kotlin2JsCompile @Inject constructor(
|
||||
logger.info(USING_JS_INCREMENTAL_COMPILATION_MESSAGE)
|
||||
IncrementalCompilationEnvironment(
|
||||
getChangedFiles(inputChanges, incrementalProps),
|
||||
ClasspathChanges.NotAvailable.ForJSCompiler,
|
||||
ClasspathChanges.NotAvailableForJSCompiler,
|
||||
taskBuildDirectory.get().asFile,
|
||||
multiModuleICSettings = multiModuleICSettings
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user