Snapshot compile tasks outputs and restoring them on compilation error.
It happens Gradle has low performance on passing 'Array<Byte>' or 'List<Byte>' via properties api. In this change, task outputs approach was changed to snapshot pre-execution state by coping outputs to the new directory and coping them back on incremental compilation error. ^KT-46406 Fixed
This commit is contained in:
+34
-21
@@ -6,8 +6,8 @@
|
|||||||
package org.jetbrains.kotlin.compilerRunner
|
package org.jetbrains.kotlin.compilerRunner
|
||||||
|
|
||||||
import org.gradle.api.GradleException
|
import org.gradle.api.GradleException
|
||||||
import org.gradle.api.file.ConfigurableFileCollection
|
import org.gradle.api.file.*
|
||||||
import org.gradle.api.provider.MapProperty
|
import org.gradle.api.logging.Logging
|
||||||
import org.gradle.api.provider.Property
|
import org.gradle.api.provider.Property
|
||||||
import org.gradle.workers.WorkAction
|
import org.gradle.workers.WorkAction
|
||||||
import org.gradle.workers.WorkParameters
|
import org.gradle.workers.WorkParameters
|
||||||
@@ -19,6 +19,7 @@ import org.jetbrains.kotlin.build.report.metrics.measure
|
|||||||
import org.jetbrains.kotlin.gradle.tasks.GradleCompileTaskProvider
|
import org.jetbrains.kotlin.gradle.tasks.GradleCompileTaskProvider
|
||||||
import org.jetbrains.kotlin.gradle.tasks.TaskOutputsBackup
|
import org.jetbrains.kotlin.gradle.tasks.TaskOutputsBackup
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses Gradle worker api to run kotlin compilation.
|
* Uses Gradle worker api to run kotlin compilation.
|
||||||
@@ -36,43 +37,54 @@ internal class GradleCompilerRunnerWithWorkers(
|
|||||||
): WorkQueue {
|
): WorkQueue {
|
||||||
|
|
||||||
val workQueue = workerExecutor.noIsolation()
|
val workQueue = workerExecutor.noIsolation()
|
||||||
workQueue.submit(GradleKotlinCompilerWorkAction::class.java) {
|
workQueue.submit(GradleKotlinCompilerWorkAction::class.java) { params ->
|
||||||
it.compilerWorkArguments.set(workArgs)
|
params.compilerWorkArguments.set(workArgs)
|
||||||
if (taskOutputsBackup != null) {
|
if (taskOutputsBackup != null) {
|
||||||
it.taskOutputs.from(taskOutputsBackup.outputs)
|
params.taskOutputs.from(taskOutputsBackup.outputs)
|
||||||
it.taskOutputsSnapshot.set(taskOutputsBackup.previousOutputs)
|
params.buildDir.set(taskOutputsBackup.buildDirectory)
|
||||||
it.metricsReporter.set(buildMetrics)
|
params.snapshotsDir.set(taskOutputsBackup.snapshotsDir)
|
||||||
} else {
|
params.metricsReporter.set(buildMetrics)
|
||||||
// MapProperty has empty value by default: https://github.com/gradle/gradle/issues/7485
|
|
||||||
it.taskOutputsSnapshot.set(null as Map<File, Array<Byte>>?)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return workQueue
|
return workQueue
|
||||||
}
|
}
|
||||||
|
|
||||||
internal abstract class GradleKotlinCompilerWorkAction
|
internal abstract class GradleKotlinCompilerWorkAction @Inject constructor(
|
||||||
: WorkAction<GradleKotlinCompilerWorkParameters> {
|
private val fileSystemOperations: FileSystemOperations
|
||||||
|
) : WorkAction<GradleKotlinCompilerWorkParameters> {
|
||||||
|
|
||||||
|
private val logger = Logging.getLogger("kotlin-compile-worker")
|
||||||
|
|
||||||
override fun execute() {
|
override fun execute() {
|
||||||
|
val taskOutputsBackup = if (parameters.snapshotsDir.isPresent) {
|
||||||
|
TaskOutputsBackup(
|
||||||
|
fileSystemOperations,
|
||||||
|
parameters.buildDir,
|
||||||
|
parameters.snapshotsDir,
|
||||||
|
parameters.taskOutputs
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
GradleKotlinCompilerWork(
|
GradleKotlinCompilerWork(
|
||||||
parameters.compilerWorkArguments.get()
|
parameters.compilerWorkArguments.get()
|
||||||
).run()
|
).run()
|
||||||
} catch (e: GradleException) {
|
} catch (e: GradleException) {
|
||||||
if (parameters.taskOutputsSnapshot.isPresent) {
|
// Currently, metrics are not reported as in the worker we are getting new instance of [BuildMetricsReporter]
|
||||||
val taskOutputsBackup = TaskOutputsBackup(
|
// [BuildDataRecorder] knows nothing about this new instance. Possibly could be fixed in the future by migrating
|
||||||
parameters.taskOutputs,
|
// [BuildMetricsReporter] to be shared Gradle service.
|
||||||
parameters.taskOutputsSnapshot.get()
|
if (taskOutputsBackup != null) {
|
||||||
)
|
|
||||||
// Currently, metrics are not reported as in the worker we are getting new instance of [BuildMetricsReporter]
|
|
||||||
// [BuildDataRecorder] knows nothing about this new instance. Possibly could be fixed in the future by migrating
|
|
||||||
// [BuildMetricsReporter] to be shared Gradle service.
|
|
||||||
parameters.metricsReporter.get().measure(BuildTime.RESTORE_OUTPUT_FROM_BACKUP) {
|
parameters.metricsReporter.get().measure(BuildTime.RESTORE_OUTPUT_FROM_BACKUP) {
|
||||||
|
logger.info("Restoring task outputs to pre-compilation state")
|
||||||
taskOutputsBackup.restoreOutputs()
|
taskOutputsBackup.restoreOutputs()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw e
|
throw e
|
||||||
|
} finally {
|
||||||
|
taskOutputsBackup?.deleteSnapshot()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,7 +92,8 @@ internal class GradleCompilerRunnerWithWorkers(
|
|||||||
internal interface GradleKotlinCompilerWorkParameters : WorkParameters {
|
internal interface GradleKotlinCompilerWorkParameters : WorkParameters {
|
||||||
val compilerWorkArguments: Property<GradleKotlinCompilerWorkArguments>
|
val compilerWorkArguments: Property<GradleKotlinCompilerWorkArguments>
|
||||||
val taskOutputs: ConfigurableFileCollection
|
val taskOutputs: ConfigurableFileCollection
|
||||||
val taskOutputsSnapshot: MapProperty<File, Array<Byte>>
|
val snapshotsDir: DirectoryProperty
|
||||||
|
val buildDir: DirectoryProperty
|
||||||
val metricsReporter: Property<BuildMetricsReporter>
|
val metricsReporter: Property<BuildMetricsReporter>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+11
-1
@@ -213,6 +213,9 @@ abstract class AbstractKotlinCompile<T : CommonCompilerArguments> : AbstractKotl
|
|||||||
|
|
||||||
private val layout = project.layout
|
private val layout = project.layout
|
||||||
|
|
||||||
|
@get:Inject
|
||||||
|
internal abstract val fileSystemOperations: FileSystemOperations
|
||||||
|
|
||||||
@get:Internal
|
@get:Internal
|
||||||
protected val objects: ObjectFactory = project.objects
|
protected val objects: ObjectFactory = project.objects
|
||||||
|
|
||||||
@@ -339,7 +342,14 @@ abstract class AbstractKotlinCompile<T : CommonCompilerArguments> : AbstractKotl
|
|||||||
val outputsBackup: TaskOutputsBackup? =
|
val outputsBackup: TaskOutputsBackup? =
|
||||||
if (isIncrementalCompilationEnabled() && inputChanges.isIncremental)
|
if (isIncrementalCompilationEnabled() && inputChanges.isIncremental)
|
||||||
buildMetrics.measure(BuildTime.BACKUP_OUTPUT) {
|
buildMetrics.measure(BuildTime.BACKUP_OUTPUT) {
|
||||||
TaskOutputsBackup(allOutputFiles())
|
TaskOutputsBackup(
|
||||||
|
fileSystemOperations,
|
||||||
|
layout.buildDirectory,
|
||||||
|
layout.buildDirectory.dir("snapshot/kotlin/$name"),
|
||||||
|
allOutputFiles()
|
||||||
|
).also {
|
||||||
|
it.createSnapshot()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else null
|
else null
|
||||||
|
|
||||||
|
|||||||
+55
-31
@@ -5,43 +5,67 @@
|
|||||||
|
|
||||||
package org.jetbrains.kotlin.gradle.tasks
|
package org.jetbrains.kotlin.gradle.tasks
|
||||||
|
|
||||||
|
import org.gradle.api.file.Directory
|
||||||
|
import org.gradle.api.file.DirectoryProperty
|
||||||
import org.gradle.api.file.FileCollection
|
import org.gradle.api.file.FileCollection
|
||||||
import org.jetbrains.kotlin.utils.keysToMap
|
import org.gradle.api.file.FileSystemOperations
|
||||||
|
import org.gradle.api.provider.Provider
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.HashSet
|
|
||||||
|
|
||||||
internal class TaskOutputsBackup(
|
internal class TaskOutputsBackup(
|
||||||
val outputs: FileCollection,
|
val fileSystemOperations: FileSystemOperations,
|
||||||
val previousOutputs: Map<File, Array<Byte>> = outputs
|
val buildDirectory: DirectoryProperty,
|
||||||
.flatMap {
|
val snapshotsDir: Provider<Directory>,
|
||||||
if (it.isDirectory) {
|
val outputs: FileCollection
|
||||||
it.walk().filter(File::isFile)
|
|
||||||
} else {
|
|
||||||
sequenceOf(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.filter { it.exists() }
|
|
||||||
.toSet()
|
|
||||||
.keysToMap { it.readBytes().toTypedArray() }
|
|
||||||
) {
|
) {
|
||||||
|
fun createSnapshot() {
|
||||||
fun restoreOutputs() {
|
// Kotlin JS compilation task declares one file from 'destinationDirectory' output as task `@OutputFile'
|
||||||
outputs.forEach {
|
// property. To avoid snapshot sync collisions, each snapshot output directory has also 'index' as prefix.
|
||||||
if (it.isDirectory) {
|
outputs.files.toSortedSet().forEachIndexed { index, outputPath ->
|
||||||
it.deleteRecursively()
|
val pathInSnapshot = "$index${File.separator}${outputPath.pathRelativeToBuildDirectory}"
|
||||||
} else if (it.isFile) {
|
if (outputPath.isDirectory) {
|
||||||
it.delete()
|
fileSystemOperations.sync { spec ->
|
||||||
|
spec.from(outputPath)
|
||||||
|
spec.into(snapshotsDir.map { it.dir(pathInSnapshot) })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileSystemOperations.copy { spec ->
|
||||||
|
spec.from(outputPath)
|
||||||
|
spec.into(snapshotsDir.map { it.file(pathInSnapshot).asFile.parentFile })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val dirs = HashSet<File>()
|
|
||||||
|
|
||||||
for ((file, bytes) in previousOutputs) {
|
|
||||||
val dir = file.parentFile
|
|
||||||
if (dirs.add(dir)) {
|
|
||||||
dir.mkdirs()
|
|
||||||
}
|
|
||||||
file.writeBytes(bytes.toByteArray())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun restoreOutputs() {
|
||||||
|
fileSystemOperations.delete {
|
||||||
|
it.delete(outputs)
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs.files.toSortedSet().forEachIndexed { index, outputPath ->
|
||||||
|
val pathInSnapshot = "$index${File.separator}${outputPath.pathRelativeToBuildDirectory}"
|
||||||
|
val fileInSnapshot = snapshotsDir.get().file(pathInSnapshot).asFile
|
||||||
|
if (fileInSnapshot.isDirectory) {
|
||||||
|
fileSystemOperations.sync { spec ->
|
||||||
|
spec.from(snapshotsDir.map { it.dir(pathInSnapshot) })
|
||||||
|
spec.into(outputPath)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileSystemOperations.copy { spec ->
|
||||||
|
spec.from(snapshotsDir.map { it.file(pathInSnapshot).asFile.parentFile })
|
||||||
|
spec.into(outputPath.parentFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteSnapshot() {
|
||||||
|
fileSystemOperations.delete { it.delete(snapshotsDir) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val File.pathRelativeToBuildDirectory: String
|
||||||
|
get() {
|
||||||
|
val buildDir = buildDirectory.get().asFile
|
||||||
|
return relativeTo(buildDir).path
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user