Skip to content

Commit

Permalink
Warn if using Kotlin Compile Daemon Fallback (#195)
Browse files Browse the repository at this point in the history
  • Loading branch information
runningcode authored Feb 14, 2022
1 parent a9e1e70 commit 87f378c
Show file tree
Hide file tree
Showing 17 changed files with 260 additions and 31 deletions.
6 changes: 3 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import com.osacky.doctor.DoctorExtension
import org.gradle.api.tasks.testing.logging.TestLogEvent

plugins {
kotlin("jvm") version "1.5.30"
id("com.github.ben-manes.versions") version "0.39.0"
kotlin("jvm") version "1.6.10"
id("com.github.ben-manes.versions") version "0.41.0"
id("com.osacky.doctor")
}

Expand All @@ -25,7 +25,7 @@ tasks.withType(Test::class.java).configureEach {
}

tasks.wrapper {
gradleVersion = "7.2"
gradleVersion = "7.3.3"
}

buildScan {
Expand Down
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

## Unreleased

## 0.8.0
* [Skip multiple daemons check on non-Unix machines, not supported yet](https://github.com/runningcode/gradle-doctor/issues/84)
* [Detect Kotlin Compiler Daemon failing to connect.](https://github.com/runningcode/gradle-doctor/issues/194)

## 0.7.3
* Fix [compatiblity with Java 8.](https://github.com/runningcode/gradle-doctor/issues/171)
Expand Down
10 changes: 10 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@
* http://github.com/gradle/gradle/issues/2488
*/
disallowCleanTaskDependencies = true
/**
* Warn if using the Kotlin Compiler Daemon Fallback. The fallback is incredibly slow and should be avoided.
* https://youtrack.jetbrains.com/issue/KT-48843
*/
warnIfKotlinCompileDaemonFallback = true


/** Configuration properties relating to JAVA_HOME */
Expand Down Expand Up @@ -140,6 +145,11 @@
* http://github.com/gradle/gradle/issues/2488
*/
disallowCleanTaskDependencies.set(true)
/**
* Warn if using the Kotlin Compiler Daemon Fallback. The fallback is incredibly slow and should be avoided.
* https://youtrack.jetbrains.com/issue/KT-48843
*/
warnIfKotlinCompileDaemonFallback.set(true)

/** Configuration properties relating to JAVA_HOME */
javaHome {
Expand Down
4 changes: 4 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,7 @@ Watch this Virtual Android Makers 2019 entitled [The Secrets of the Build Scan P
* Warn when build spends more than 10% of the time garbage collecting.
* Warn when connection to maven repositories is slowing down the build.
* Warn when build cache connection speed is slowing down the build.

## Supported Gradle versions
The minimum supported Gradle version is 6.1.1.
If using Gradle 5.x, please use Gradle Doctor version 0.7.3.
8 changes: 4 additions & 4 deletions doctor-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import org.gradle.api.tasks.testing.logging.TestLogEvent

plugins {
`kotlin-dsl`
kotlin("jvm") version "1.5.30"
kotlin("jvm") version "1.6.10"
id("com.gradle.plugin-publish") version "0.16.0"
id("org.jmailen.kotlinter") version "3.3.0"
`maven-publish`
Expand All @@ -26,14 +26,14 @@ gradlePlugin {
}

dependencies {
compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.30")
compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10")
implementation("com.osacky.tagger:tagger-lib:0.2")
implementation("io.reactivex.rxjava3:rxjava:3.0.2")
implementation("io.reactivex.rxjava3:rxjava:3.1.3")
"parallelGCTestImplementation"(testFixtures(project))
"integrationTestImplementation"(testFixtures(project))
testFixturesApi(gradleTestKit())
testFixturesApi("junit:junit:4.13.2")
testFixturesApi("com.google.truth:truth:1.0.1")
testFixturesApi("com.google.truth:truth:1.1.3")
testFixturesApi("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.osacky.doctor

import com.google.common.truth.Truth.assertThat
import org.junit.Test

class KotlinDaemonFallbackIntegrationTest : AbstractIntegrationTest() {

@Test
fun testDisallowKotlinCompileDaemonFallback() {
writeKotlinBuildGradle(true)
writeSettingsFile()
testProjectRoot.newFolder("src/main/java/foo")
testProjectRoot.newFolder("src/test/java/foo")
testProjectRoot.writeFileToName(
"src/main/java/Foo.kt",
"""
package foo
class Foo {
fun bar() {
println("Hello, world!")
}
}
""".trimIndent()
)
testProjectRoot.writeFileToName(
"src/test/java/Foo.kt",
"""
package foo
class Foo {
fun bar() {
println("Hello, world!")
}
}
""".trimIndent()
)

val result = assembleRunnerWithIncorrectDaemonArguments().build()

assertThat(result.output).contains("Could not connect to kotlin daemon. Using fallback strategy.")
assertThat(result.output).contains("The Kotlin Compiler Daemon failed to connect and likely won't recover on its own.")
}

@Test
fun allowKotlinCompileFallback() {
writeKotlinBuildGradle(false)
writeSettingsFile()
testProjectRoot.newFolder("src/main/java/foo")
testProjectRoot.writeFileToName(
"src/main/java/foo/Foo.kt",
"""
package foo
class Foo {
fun bar() {
println("Hello, world!")
}
}
""".trimIndent()
)

val result = assembleRunnerWithIncorrectDaemonArguments().build()

assertThat(result.output).contains("Could not connect to kotlin daemon. Using fallback strategy.")
assertThat(result.output).contains("SUCCESS")
}

private fun writeSettingsFile() {
testProjectRoot.writeFileToName(
"settings.gradle",
"""
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
""".trimIndent()
)
}

private fun assembleRunnerWithIncorrectDaemonArguments() = createRunner()
.withArguments("check", "-Dkotlin.daemon.jvm.options=invalid_jvm_argument_to_fail_process_startup")

private fun writeKotlinBuildGradle(allowDaemonFallback: Boolean) {
testProjectRoot.writeBuildGradle(
"""
plugins {
id "com.osacky.doctor"
id "org.jetbrains.kotlin.jvm" version "1.6.10"
}
repositories {
mavenCentral()
}
doctor {
warnIfKotlinCompileDaemonFallback = $allowDaemonFallback
javaHome {
ensureJavaHomeMatches = false
}
}
""".trimIndent()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ open class DoctorExtension(objects: ObjectFactory) {
*/
val disallowCleanTaskDependencies = objects.property<Boolean>().convention(true)

/**
* Warn if using the Kotlin Compiler Daemon Fallback. The fallback is incredibly slow and should be avoided.
* https://youtrack.jetbrains.com/issue/KT-48843
*/
val warnIfKotlinCompileDaemonFallback = objects.property<Boolean>().convention(true)

/**
* Configures `JAVA_HOME`-specific behavior.
*/
Expand Down
14 changes: 8 additions & 6 deletions doctor-plugin/src/main/java/com/osacky/doctor/DoctorPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,15 @@ class DoctorPlugin : Plugin<Project> {
val javaHomeCheck = JavaHomeCheck(extension, pillBoxPrinter)
val garbagePrinter = GarbagePrinter(clock, DirtyBeanCollector(), extension)
val buildOperations = getOperationEvents(target, extension)
val javaAnnotationTime = JavaAnnotationTime(buildOperations, extension, target.buildscript.configurations)
val javaAnnotationTime = JavaAnnotationTime(buildOperations, extension)
val downloadSpeedMeasurer = DownloadSpeedMeasurer(buildOperations, extension, intervalMeasurer)
val buildCacheConnectionMeasurer = BuildCacheConnectionMeasurer(buildOperations, extension, intervalMeasurer)
val buildCacheKey = RemoteCacheEstimation((buildOperations as BuildOperations), target, clock)
val slowerFromCacheCollector = buildOperations.slowerFromCacheCollector()
val jetifierWarning = JetifierWarning(extension, target)
val javaElevenGC = JavaGCFlagChecker(pillBoxPrinter, extension)
val list = listOf(daemonChecker, javaHomeCheck, garbagePrinter, javaAnnotationTime, downloadSpeedMeasurer, buildCacheConnectionMeasurer, buildCacheKey, slowerFromCacheCollector, jetifierWarning, javaElevenGC)
val kotlinCompileDaemonFallbackDetector = KotlinCompileDaemonFallbackDetector(target, extension)
val list = listOf(daemonChecker, javaHomeCheck, garbagePrinter, javaAnnotationTime, downloadSpeedMeasurer, buildCacheConnectionMeasurer, buildCacheKey, slowerFromCacheCollector, jetifierWarning, javaElevenGC, kotlinCompileDaemonFallbackDetector)

garbagePrinter.onStart()
javaAnnotationTime.onStart()
Expand All @@ -67,6 +68,7 @@ class DoctorPlugin : Plugin<Project> {
daemonChecker.onStart()
javaHomeCheck.onStart()
javaElevenGC.onStart()
kotlinCompileDaemonFallbackDetector.onStart()
}

val buildScanApi = ScanApi(target)
Expand Down Expand Up @@ -155,7 +157,7 @@ class DoctorPlugin : Plugin<Project> {
pillBoxPrinter.writePrescription(thingsToPrint)
}

if (target.gradle.shouldUseCoCaClasses()) {
if (shouldUseCoCaClasses()) {
val closeService =
target.gradle.sharedServices.registerIfAbsent("close-service", BuildFinishService::class.java) { }.get()
closeService.closeMeWhenFinished {
Expand Down Expand Up @@ -198,8 +200,8 @@ class DoctorPlugin : Plugin<Project> {
}

private fun ensureMinimumSupportedGradleVersion() {
if (GradleVersion.current() < GradleVersion.version("5.2")) {
throw GradleException("Must be using Gradle Version 5.2 in order to use DoctorPlugin. Current Gradle Version is ${GradleVersion.current()}")
if (GradleVersion.current() < GradleVersion.version("6.1.1")) {
throw GradleException("Must be using Gradle Version 6.1.1 in order to use DoctorPlugin. Current Gradle Version is ${GradleVersion.current()}")
}
}

Expand All @@ -211,7 +213,7 @@ class DoctorPlugin : Plugin<Project> {
}

private fun getOperationEvents(target: Project, extension: DoctorExtension): OperationEvents {
return if (target.gradle.shouldUseCoCaClasses()) {
return if (shouldUseCoCaClasses()) {
val listenerService = target.gradle.sharedServices.registerIfAbsent("listener-service", BuildOperationListenerService::class.java) {
this.parameters.getNegativeAvoidanceThreshold().set(extension.negativeAvoidanceThreshold)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
package com.osacky.doctor

import com.osacky.doctor.internal.plusAssign
import com.osacky.tagger.ScanApi
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.disposables.Disposable
import org.gradle.api.artifacts.ConfigurationContainer
import org.gradle.api.internal.tasks.compile.CompileJavaBuildOperationType
import org.gradle.internal.logging.events.operations.LogEventBuildOperationProgressDetails

class JavaAnnotationTime(
private val operationEvents: OperationEvents,
private val doctorExtension: DoctorExtension,
private val buildscriptConfiguration: ConfigurationContainer
) : BuildStartFinishListener, HasBuildScanTag {
private var totalDaggerTime = 0

Expand All @@ -33,10 +31,6 @@ class JavaAnnotationTime(
}
}

infix operator fun CompositeDisposable.plusAssign(disposable: Disposable) {
add(disposable)
}

override fun onFinish(): List<String> {
disposable.dispose()
if (totalDaggerTime > doctorExtension.daggerThreshold.get()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.osacky.doctor

import com.osacky.doctor.internal.sysProperty
import com.osacky.tagger.ScanApi
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.gradle.api.Project
import org.gradle.internal.logging.LoggingManagerInternal
import org.gradle.internal.logging.events.LogEvent
import org.gradle.internal.logging.events.OutputEvent
import org.gradle.internal.logging.events.OutputEventListener
import org.gradle.kotlin.dsl.support.serviceOf
import java.util.concurrent.atomic.AtomicInteger

class KotlinCompileDaemonFallbackDetector(
private val project: Project,
private val extension: DoctorExtension
) : BuildStartFinishListener, HasBuildScanTag {
private val fallbackCounter = AtomicInteger(0)
private val loggingService = project.gradle.serviceOf<LoggingManagerInternal>()
private val failureEventListener = FailureEventListener(fallbackCounter)
private val disposable = CompositeDisposable()

override fun onStart() {
if (!extension.warnIfKotlinCompileDaemonFallback.get() || isDaemonDisabled(project)) {
return
}
loggingService.addOutputEventListener(failureEventListener)
}

override fun onFinish(): List<String> {
loggingService.removeOutputEventListener(failureEventListener)
disposable.dispose()
if (hasUsedFallback()) {
return listOf(
"""
The Kotlin Compiler Daemon failed to connect and likely won't recover on its own.
The fallback strategy is incredibly slow and should be avoided.
https://youtrack.jetbrains.com/issue/KT-48843
To recover, try killing the Kotlin Compiler Daemon:
1. ./gradlew --stop
2. Find Kotlin daemon process id (pid): `jps | grep Kotlin`
3. kill <pid>
If that didn't help, check that there are no invalid JVM arguments in "kotlin.daemon.jvm.options" property except for Xmx.
""".trimIndent()
)
}
return emptyList()
}

override fun addCustomValues(buildScanApi: ScanApi) {
if (hasUsedFallback()) {
buildScanApi.tag("doctor-kotlin-compile-daemon-fallback")
}
}

private fun hasUsedFallback() = fallbackCounter.get() > 1

/**
* Copy of internal logic in GradleKotlinCompilerRunner
*/
private fun isDaemonDisabled(project: Project): Boolean {
val strategy = sysProperty("kotlin.compiler.execution.strategy", project.providers).orElse("daemon")
return strategy != "daemon" // "in-process", "out-of-process"
}
}

internal class FailureEventListener(
private val fallbacksCounter: AtomicInteger,
) : OutputEventListener {

override fun onOutput(event: OutputEvent) {
if (isFallbackMessage(event)) {
// Can't fail a build from OutputEventListener. So, only mark it
fallbacksCounter.incrementAndGet()
}
}

private fun isFallbackMessage(event: OutputEvent): Boolean {
return event is LogEvent &&
event.message.contains("Could not connect to kotlin daemon. Using fallback strategy.")
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
package com.osacky.doctor.internal

import org.gradle.api.invocation.Gradle
import org.gradle.api.provider.ProviderFactory
import org.gradle.util.GradleVersion
import java.util.Optional

fun Gradle.shouldUseCoCaClasses(): Boolean = GradleVersion.version(gradleVersion) >= GradleVersion.version("6.5")
fun shouldUseCoCaClasses(): Boolean = isGradle65OrNewer()

fun isGradle65OrNewer(): Boolean {
return GradleVersion.current() >= GradleVersion.version("6.5")
}

fun sysProperty(name: String, providers: ProviderFactory): Optional<String> {
if (isGradle65OrNewer()) {
val property = providers.systemProperty(name).forUseAtConfigurationTime()
return Optional.ofNullable(property.orNull)
}
return Optional.ofNullable(System.getProperty(name))
}
Loading

0 comments on commit 87f378c

Please sign in to comment.