Clean up fall-back logic in IncrementalCompilerRunner

Make it clear that there 3 distinct cases:
   1. Incremental compilation completed with an ExitCode.
   2. Incremental compilation was not possible for some valid reason
      (e.g., for a clean build), and we will perform non-incremental
      compilation.
   3. Incremental compilation failed with an exception.
      In this case, we will:
        - Print a warning with a stack trace
        - Ask the user to file a bug
        - Collect rebuild reason enum for analytics
           + TODO: Collect the stack trace too
        - Fall back to non-incremental compilation

Test: Existing BaseIncrementalCompilationMultiProjectIT.testFailureHandling_UserError,
      Updated BaseIncrementalCompilationMultiProjectIT.testFailureHandling_ToolError

^KT-53015: In progress
This commit is contained in:
Hung Nguyen
2022-06-20 13:41:40 +01:00
committed by Andrey Uskov
parent 57bbc335f4
commit def886cd31
24 changed files with 409 additions and 341 deletions
@@ -2,6 +2,7 @@ package org.jetbrains.kotlin.gradle
import org.gradle.testkit.runner.BuildResult
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.build.report.metrics.BuildAttribute
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilerExecutionStrategy
import org.jetbrains.kotlin.gradle.testbase.*
import org.jetbrains.kotlin.gradle.util.checkedReplace
@@ -735,7 +736,7 @@ abstract class BaseIncrementalCompilationMultiProjectIT : IncrementalCompilation
// In the next build, compilation should be incremental and fail, then fall back to non-incremental compilation and succeed
build(":lib:compileKotlin") {
assertIncrementalCompilationFellBackToNonIncremental()
assertIncrementalCompilationFellBackToNonIncremental(BuildAttribute.IC_FAILED_TO_COMPILE_INCREMENTALLY)
// Also check that the output is not deleted (regression test for KT-49780)
assertFileExists(lookupFile)
}
@@ -96,8 +96,10 @@ fun BuildResult.assertNonIncrementalCompilation(reason: BuildAttribute? = null)
} else {
assertOutputContains(NON_INCREMENTAL_COMPILATION_WILL_BE_PERFORMED)
}
// Also check that incremental compilation was not attempted, failed, and fell back to non-incremental compilation
assertOutputDoesNotContain(PERFORMING_INCREMENTAL_COMPILATION)
// Also check that the other cases didn't happen
assertOutputDoesNotContain(INCREMENTAL_COMPILATION_COMPLETED)
assertOutputDoesNotContain(FALLING_BACK_TO_NON_INCREMENTAL_COMPILATION)
}
/**
@@ -108,9 +110,11 @@ fun BuildResult.assertNonIncrementalCompilation(reason: BuildAttribute? = null)
* Note: Log level of output must be set to [LogLevel.DEBUG].
*/
fun BuildResult.assertIncrementalCompilation(expectedCompiledKotlinFiles: Iterable<Path>? = null) {
assertOutputContains(PERFORMING_INCREMENTAL_COMPILATION)
// Also check that incremental compilation did not fail and fall back to non-incremental compilation
assertOutputContains(INCREMENTAL_COMPILATION_COMPLETED)
// Also check that the other cases didn't happen
assertOutputDoesNotContain(NON_INCREMENTAL_COMPILATION_WILL_BE_PERFORMED)
assertOutputDoesNotContain(FALLING_BACK_TO_NON_INCREMENTAL_COMPILATION)
expectedCompiledKotlinFiles?.let {
assertSameFiles(expected = it, actual = extractCompiledKotlinFiles(output), "Compiled Kotlin files differ:\n")
@@ -122,9 +126,19 @@ fun BuildResult.assertIncrementalCompilation(expectedCompiledKotlinFiles: Iterab
*
* Note: Log level of output must be set to [LogLevel.DEBUG].
*/
fun BuildResult.assertIncrementalCompilationFellBackToNonIncremental() {
assertOutputContains("$NON_INCREMENTAL_COMPILATION_WILL_BE_PERFORMED: ${BuildAttribute.INCREMENTAL_COMPILATION_FAILED.name}")
fun BuildResult.assertIncrementalCompilationFellBackToNonIncremental(reason: BuildAttribute? = null) {
if (reason != null) {
assertOutputContains("$FALLING_BACK_TO_NON_INCREMENTAL_COMPILATION (reason = ${reason.name})")
} else {
assertOutputContains(FALLING_BACK_TO_NON_INCREMENTAL_COMPILATION)
}
// Also check that the other cases didn't happen
assertOutputDoesNotContain(INCREMENTAL_COMPILATION_COMPLETED)
assertOutputDoesNotContain(NON_INCREMENTAL_COMPILATION_WILL_BE_PERFORMED)
}
private const val PERFORMING_INCREMENTAL_COMPILATION = "Performing incremental compilation"
// Each of the following messages should uniquely correspond to a case in `IncrementalCompilerRunner.ICResult`
private const val INCREMENTAL_COMPILATION_COMPLETED = "Incremental compilation completed"
const val NON_INCREMENTAL_COMPILATION_WILL_BE_PERFORMED = "Non-incremental compilation will be performed"
private const val FALLING_BACK_TO_NON_INCREMENTAL_COMPILATION = "Falling back to non-incremental compilation"
@@ -5,7 +5,6 @@
package org.jetbrains.kotlin.compilerRunner
import org.gradle.api.GradleException
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.logging.Logging
@@ -18,9 +17,7 @@ import org.gradle.workers.WorkerExecutor
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.gradle.tasks.GradleCompileTaskProvider
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilerExecutionStrategy
import org.jetbrains.kotlin.gradle.tasks.TaskOutputsBackup
import org.jetbrains.kotlin.gradle.tasks.*
import java.io.File
import javax.inject.Inject
@@ -43,7 +40,7 @@ internal class GradleCompilerRunnerWithWorkers(
workQueue.submit(GradleKotlinCompilerWorkAction::class.java) { params ->
params.compilerWorkArguments.set(workArgs)
if (taskOutputsBackup != null) {
params.taskOutputs.set(taskOutputsBackup.outputs)
params.taskOutputsToRestore.set(taskOutputsBackup.outputsToRestore)
params.buildDir.set(taskOutputsBackup.buildDirectory)
params.snapshotsDir.set(taskOutputsBackup.snapshotsDir)
params.metricsReporter.set(buildMetrics)
@@ -64,8 +61,7 @@ internal class GradleCompilerRunnerWithWorkers(
fileSystemOperations,
parameters.buildDir,
parameters.snapshotsDir,
parameters.taskOutputs.get(),
outputsToExclude = emptyList(),
parameters.taskOutputsToRestore.get(),
logger
)
} else {
@@ -76,11 +72,14 @@ internal class GradleCompilerRunnerWithWorkers(
GradleKotlinCompilerWork(
parameters.compilerWorkArguments.get()
).run()
} catch (e: GradleException) {
// 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.
if (taskOutputsBackup != null) {
} catch (e: FailedCompilationException) {
// Restore outputs only in cases where we expect that the user will make some changes to their project:
// - For a compilation error, the user will need to fix their source code
// - For an OOM error, the user will need to increase their memory settings
// In the other cases where there is nothing the user can fix in their project, we should not restore the outputs.
// Otherwise, the next build(s) will likely fail in exactly the same way as this build because their inputs and outputs are
// the same.
if (taskOutputsBackup != null && (e is CompilationErrorException || e is OOMErrorException)) {
parameters.metricsReporter.get().measure(BuildTime.RESTORE_OUTPUT_FROM_BACKUP) {
logger.info("Restoring task outputs to pre-compilation state")
taskOutputsBackup.restoreOutputs()
@@ -96,7 +95,7 @@ internal class GradleCompilerRunnerWithWorkers(
internal interface GradleKotlinCompilerWorkParameters : WorkParameters {
val compilerWorkArguments: Property<GradleKotlinCompilerWorkArguments>
val taskOutputs: ListProperty<File>
val taskOutputsToRestore: ListProperty<File>
val snapshotsDir: DirectoryProperty
val buildDir: DirectoryProperty
val metricsReporter: Property<BuildMetricsReporter>
@@ -5,7 +5,6 @@
package org.jetbrains.kotlin.compilerRunner
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.invocation.Gradle
import org.gradle.api.logging.Logger
@@ -33,11 +32,7 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinWithJavaTarget
import org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildStatsService
import org.jetbrains.kotlin.gradle.plugin.variantImplementationFactory
import org.jetbrains.kotlin.gradle.tasks.*
import org.jetbrains.kotlin.gradle.utils.IsolatedKotlinClasspathClassCastException
import org.jetbrains.kotlin.gradle.utils.archivePathCompatible
import org.jetbrains.kotlin.gradle.utils.findByType
import org.jetbrains.kotlin.gradle.utils.newTmpFile
import org.jetbrains.kotlin.gradle.utils.relativeOrCanonical
import org.jetbrains.kotlin.gradle.utils.*
import org.jetbrains.kotlin.incremental.IncrementalModuleEntry
import org.jetbrains.kotlin.incremental.IncrementalModuleInfo
import org.jetbrains.kotlin.statistics.metrics.BooleanMetrics
@@ -223,8 +218,9 @@ internal open class GradleCompilerRunner(
try {
val kotlinCompilerRunnable = GradleKotlinCompilerWork(workArgs)
kotlinCompilerRunnable.run()
} catch (e: GradleException) {
if (taskOutputsBackup != null) {
} catch (e: FailedCompilationException) {
// Restore outputs only for CompilationErrorException or OOMErrorException (see GradleKotlinCompilerWorkAction.execute)
if (taskOutputsBackup != null && (e is CompilationErrorException || e is OOMErrorException)) {
buildMetrics.measure(BuildTime.RESTORE_OUTPUT_FROM_BACKUP) {
taskOutputsBackup.restoreOutputs()
}
@@ -17,7 +17,7 @@ import org.jetbrains.kotlin.gradle.plugin.internal.state.TaskLoggers
import org.jetbrains.kotlin.gradle.report.*
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilerExecutionStrategy
import org.jetbrains.kotlin.gradle.tasks.cleanOutputsAndLocalState
import org.jetbrains.kotlin.gradle.tasks.throwGradleExceptionIfError
import org.jetbrains.kotlin.gradle.tasks.throwExceptionIfCompilationFailed
import org.jetbrains.kotlin.gradle.utils.stackTraceAsString
import org.jetbrains.kotlin.incremental.ChangedFiles
import org.jetbrains.kotlin.incremental.ClasspathChanges
@@ -125,7 +125,7 @@ internal class GradleKotlinCompilerWork @Inject constructor(
incrementalCompilationEnvironment.multiModuleICSettings.buildHistoryFile.delete()
}
throwGradleExceptionIfError(exitCode, executionStrategy)
throwExceptionIfCompilationFailed(exitCode, executionStrategy)
} finally {
val taskInfo = TaskExecutionInfo(
changedFiles = incrementalCompilationEnvironment?.changedFiles,
@@ -424,9 +424,7 @@ abstract class BuildReportsService : BuildService<BuildReportsService.Parameters
taskExecutionResult?.buildMetrics?.buildPerformanceMetrics?.asMap()?.filterValues { value -> value != 0L } ?: emptyMap()
val changes = when (val changedFiles = taskExecutionResult?.taskInfo?.changedFiles) {
is ChangedFiles.Known -> changedFiles.modified.map { it.absolutePath } + changedFiles.removed.map { it.absolutePath }
is ChangedFiles.Dependencies -> changedFiles.modified.map { it.absolutePath } + changedFiles.removed.map { it.absolutePath }
else -> emptyList<String>()
}
return CompileStatisticsData(
durationMs = durationMs,
@@ -141,7 +141,7 @@ abstract class KotlinJsDce @Inject constructor(
buildDir.get().asFile,
jvmArgs
)
throwGradleExceptionIfError(exitCode, KotlinCompilerExecutionStrategy.OUT_OF_PROCESS)
throwExceptionIfCompilationFailed(exitCode, KotlinCompilerExecutionStrategy.OUT_OF_PROCESS)
}
private fun isDceCandidate(file: File): Boolean {
@@ -363,7 +363,7 @@ abstract class AbstractKotlinCompile<T : CommonCompilerArguments> @Inject constr
private val systemPropertiesService = CompilerSystemPropertiesService.registerIfAbsent(project.gradle)
/** Task outputs that we don't want to include in [TaskOutputsBackup] (see [TaskOutputsBackup]'s kdoc for more info). */
/** Task outputs that we don't want to include in [TaskOutputsBackup] (see [TaskOutputsBackup.outputsToRestore] for more info). */
@get:Internal
protected open val taskOutputsBackupExcludes: List<File> = emptyList()
@@ -391,8 +391,7 @@ abstract class AbstractKotlinCompile<T : CommonCompilerArguments> @Inject constr
fileSystemOperations,
layout.buildDirectory,
layout.buildDirectory.dir("snapshot/kotlin/$name"),
allOutputFiles(),
taskOutputsBackupExcludes,
outputsToRestore = allOutputFiles() - taskOutputsBackupExcludes,
logger
).also {
it.createSnapshot()
@@ -596,7 +595,6 @@ abstract class KotlinCompile @Inject constructor(
classpathSnapshotProperties.classpathSnapshot
)
// Exclude classpathSnapshotDir from TaskOutputsBackup (see TaskOutputsBackup's kdoc for more info). */
override val taskOutputsBackupExcludes: List<File>
get() = classpathSnapshotProperties.classpathSnapshotDir.orNull?.asFile?.let { listOf(it) } ?: emptyList()
@@ -5,7 +5,9 @@
package org.jetbrains.kotlin.gradle.tasks
import org.gradle.api.file.*
import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.logging.Logger
import org.gradle.api.provider.Provider
import java.io.File
@@ -14,35 +16,31 @@ import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.util.zip.*
import java.util.zip.Deflater
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
internal class TaskOutputsBackup(
val fileSystemOperations: FileSystemOperations,
private val fileSystemOperations: FileSystemOperations,
val buildDirectory: DirectoryProperty,
val snapshotsDir: Provider<Directory>,
allOutputs: List<File>,
/**
* Task outputs that we don't want to back up for performance reasons (e.g., if (1) they are too big, and (2) they are usually updated
* only at the end of the task execution--in a failed task run, they are usually unchanged and therefore don't need to be restored).
* Task outputs to back up and restore.
*
* NOTE: In `IncrementalCompilerRunner`, if incremental compilation fails, it will try again by cleaning all the outputs and perform
* non-incremental compilation. It is important that `IncrementalCompilerRunner` do not clean [outputsToExclude] immediately but only
* right before [outputsToExclude] are updated (which is usually at the end of the task execution). This is so that if the fallback
* compilation fails, [outputsToExclude] will remain unchanged and the other outputs will be restored, and the next task run can be
* incremental.
* Note that this could be a subset of all the outputs of a task because there could be task outputs that we don't want to back up and
* restore (e.g., if (1) they are too big and (2) they are updated only at the end of the task execution so in a failed task run, they
* are usually unchanged and therefore don't need to be restored).
*/
outputsToExclude: List<File> = emptyList(),
val outputsToRestore: List<File>,
val logger: Logger
) {
/** The outputs to back up and restore. Note that this may be a subset of all the outputs of a task (see `outputsToExclude`). */
val outputs: List<File> = allOutputs - outputsToExclude.toSet()
fun createSnapshot() {
// Kotlin JS compilation task declares one file from 'destinationDirectory' output as task `@OutputFile'
// property. To avoid snapshot sync collisions, each snapshot output directory has also 'index' as prefix.
outputs.toSortedSet().forEachIndexed { index, outputPath ->
outputsToRestore.toSortedSet().forEachIndexed { index, outputPath ->
if (outputPath.isDirectory && !outputPath.isEmptyDirectory) {
compressDirectoryToZip(
File(snapshotsDir.get().asFile, index.asSnapshotArchiveName),
@@ -59,10 +57,10 @@ internal class TaskOutputsBackup(
fun restoreOutputs() {
fileSystemOperations.delete {
it.delete(outputs)
it.delete(outputsToRestore)
}
outputs.toSortedSet().forEachIndexed { index, outputPath ->
outputsToRestore.toSortedSet().forEachIndexed { index, outputPath ->
val snapshotDir = snapshotsDir.get().file(index.asSnapshotDirectoryName).asFile
if (snapshotDir.isDirectory) {
fileSystemOperations.copy { spec ->
@@ -1,6 +1,5 @@
package org.jetbrains.kotlin.gradle.tasks
import org.gradle.api.GradleException
import org.jetbrains.kotlin.build.report.metrics.BuildMetricsReporter
import org.jetbrains.kotlin.build.report.metrics.BuildTime
import org.jetbrains.kotlin.build.report.metrics.measure
@@ -10,18 +9,19 @@ import org.jetbrains.kotlin.gradle.internal.tasks.TaskWithLocalState
import org.jetbrains.kotlin.gradle.internal.tasks.allOutputFiles
import org.jetbrains.kotlin.gradle.logging.GradleKotlinLogger
import org.jetbrains.kotlin.gradle.logging.kotlinDebug
import org.jetbrains.kotlin.incremental.cleanDirectoryContents
import org.jetbrains.kotlin.incremental.deleteDirectoryContents
import org.jetbrains.kotlin.incremental.deleteRecursivelyOrThrow
import java.io.File
fun throwGradleExceptionIfError(
/** Throws [FailedCompilationException] if compilation completed with [exitCode] != [ExitCode.OK]. */
fun throwExceptionIfCompilationFailed(
exitCode: ExitCode,
executionStrategy: KotlinCompilerExecutionStrategy
) {
when (exitCode) {
ExitCode.COMPILATION_ERROR -> throw GradleException("Compilation error. See log for more details")
ExitCode.INTERNAL_ERROR -> throw GradleException("Internal compiler error. See log for more details")
ExitCode.SCRIPT_EXECUTION_ERROR -> throw GradleException("Script execution error. See log for more details")
ExitCode.COMPILATION_ERROR -> throw CompilationErrorException("Compilation error. See log for more details")
ExitCode.INTERNAL_ERROR -> throw FailedCompilationException("Internal compiler error. See log for more details")
ExitCode.SCRIPT_EXECUTION_ERROR -> throw FailedCompilationException("Script execution error. See log for more details")
ExitCode.OOM_ERROR -> {
var exceptionMessage = "Not enough memory to run compilation."
when (executionStrategy) {
@@ -31,13 +31,22 @@ fun throwGradleExceptionIfError(
exceptionMessage += " Try to increase it via 'gradle.properties':\norg.gradle.jvmargs=-Xmx<size>"
KotlinCompilerExecutionStrategy.OUT_OF_PROCESS -> Unit
}
throw GradleException(exceptionMessage)
throw OOMErrorException(exceptionMessage)
}
ExitCode.OK -> Unit
else -> throw IllegalStateException("Unexpected exit code: $exitCode")
}
}
/** Exception thrown when [ExitCode] != [ExitCode.OK]. */
internal open class FailedCompilationException(message: String) : RuntimeException(message)
/** Exception thrown when [ExitCode] == [ExitCode.COMPILATION_ERROR]. */
internal class CompilationErrorException(message: String) : FailedCompilationException(message)
/** Exception thrown when [ExitCode] == [ExitCode.OOM_ERROR]. */
internal class OOMErrorException(message: String) : FailedCompilationException(message)
internal fun TaskWithLocalState.cleanOutputsAndLocalState(reason: String? = null) {
val log = GradleKotlinLogger(logger)
cleanOutputsAndLocalState(allOutputFiles(), log, metrics.get(), reason)
@@ -59,7 +68,7 @@ internal fun cleanOutputsAndLocalState(
when {
file.isDirectory -> {
log.debug("Deleting contents of output directory: $file")
file.cleanDirectoryContents()
file.deleteDirectoryContents()
}
file.isFile -> {
log.debug("Deleting output file: $file")