Skip to content

Commit d410596

Browse files
committed
[Gradle] Shutdown coroutines dispatcher threads after compiler invocation
IJ SDK has dependency on coroutines. It launches coroutine and thus spawns dispatcher threads when compiler instantiates KotlinCoreProjectEnvironment that extends `CoreProjectEnvironment` from IJ SDK. In particular CodeInsightContextManagerImpl.subscribeToChanges does the first launch. As a workaround in this changes we will shutdown coroutines on the compiler classloader after it finishes its job. Later we would need to properly manage the Thread resources with compiler ^KT-84152
1 parent 348430b commit d410596

3 files changed

Lines changed: 125 additions & 16 deletions

File tree

libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/ExecutionStrategyIT.kt

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
package org.jetbrains.kotlin.gradle
22

33
import org.gradle.api.logging.LogLevel
4+
import org.gradle.kotlin.dsl.kotlin
45
import org.gradle.testkit.runner.BuildResult
56
import org.gradle.util.GradleVersion
67
import org.jetbrains.kotlin.gradle.internals.asFinishLogMessage
78
import org.jetbrains.kotlin.gradle.plugin.diagnostics.KotlinToolingDiagnostics
89
import org.jetbrains.kotlin.gradle.plugin.diagnostics.ToolingDiagnosticFactory
10+
import org.jetbrains.kotlin.gradle.plugin.extraProperties
911
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
1012
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilerExecutionStrategy
1113
import org.jetbrains.kotlin.gradle.tasks.withType
1214
import org.jetbrains.kotlin.gradle.testbase.*
1315
import org.jetbrains.kotlin.gradle.util.checkedReplace
16+
import org.junit.jupiter.api.Disabled
1417
import org.junit.jupiter.api.DisplayName
1518
import kotlin.io.path.appendText
19+
import kotlin.io.path.createFile
20+
import kotlin.io.path.createParentDirectories
21+
import kotlin.io.path.writeText
22+
import kotlin.test.fail
1623

1724
@DisplayName("Kotlin JS compile execution strategy")
1825
class ExecutionStrategyJsIT : ExecutionStrategyIT() {
@@ -344,3 +351,76 @@ abstract class ExecutionStrategyIT : KGPDaemonsBaseTest() {
344351
protected abstract fun BuildResult.checkOutput(project: TestProject)
345352
protected abstract fun BuildResult.checkOutputAfterChange(project: TestProject)
346353
}
354+
355+
class NoActiveThreadsAfterCompilerInvocationIT : KGPDaemonsBaseTest() {
356+
@DisplayName("KT-84152: [BTA] In-process compilation should not leave active threads")
357+
@Disabled("Isn't fixed for BTA yet")
358+
@GradleTest
359+
fun testBta(gradleVersion: GradleVersion) = test(gradleVersion, buildOptions = defaultBuildOptions.copy(runViaBuildToolsApi = true))
360+
361+
@DisplayName("KT-84152: In-process compilation should not leave active threads")
362+
@GradleTest
363+
fun testNonBta(gradleVersion: GradleVersion) = test(gradleVersion, buildOptions = defaultBuildOptions.copy(runViaBuildToolsApi = false))
364+
365+
private fun test(
366+
gradleVersion: GradleVersion,
367+
buildOptions: BuildOptions
368+
) {
369+
project(
370+
"empty",
371+
gradleVersion,
372+
buildOptions = buildOptions.copy(
373+
compilerExecutionStrategy = KotlinCompilerExecutionStrategy.IN_PROCESS
374+
)
375+
) {
376+
plugins {
377+
kotlin("jvm")
378+
}
379+
380+
kotlinSourcesDir().resolve("Foo.kt")
381+
.createParentDirectories()
382+
.createFile()
383+
.writeText("class Foo")
384+
385+
buildScriptInjection {
386+
fun makeThreadsSnapshot(): Set<String> = Thread
387+
.getAllStackTraces()
388+
.keys.groupBy { it.javaClass.name + ":" + it.name }
389+
.map { (name, threads) -> "$name (total ${threads.size})" }.toSet()
390+
391+
project.tasks.named("compileKotlin").configure {
392+
var threadsBeforeKotlinCompile: Set<String>? = null
393+
it.doFirst {
394+
threadsBeforeKotlinCompile = makeThreadsSnapshot()
395+
}
396+
397+
it.doLast {
398+
val threadsAfter = makeThreadsSnapshot()
399+
val threadsBefore = checkNotNull(threadsBeforeKotlinCompile) { "threadsBeforeKotlinCompile must not be null" }
400+
check(threadsBefore.isNotEmpty()) { "[threadsBefore] snapshot must not be empty" }
401+
check(threadsAfter.isNotEmpty()) { "[threadsAfter] snapshot must not be empty" }
402+
403+
val newThreadsAfterExecution = threadsAfter - threadsBefore
404+
val expectedGradleWorkerThreads = listOf(
405+
"""java\.lang\.Thread:pool-\d+-thread-\d+ \(total \d+\)""".toRegex(),
406+
"""java\.lang\.Thread:WorkerExecutor Queue \(total 1\)""".toRegex(),
407+
"""java\.lang\.Thread:Unconstrained build operations Thread \d+ \(total \d+\)""".toRegex(),
408+
)
409+
410+
val newThreadsAfterExecutionFiltered = newThreadsAfterExecution.filter { threadInfo ->
411+
expectedGradleWorkerThreads.all { !threadInfo.matches(it) }
412+
}
413+
414+
if (newThreadsAfterExecutionFiltered.isNotEmpty()) {
415+
throw RuntimeException("Threads were left active after compilation: $newThreadsAfterExecutionFiltered")
416+
}
417+
}
418+
}
419+
}
420+
421+
build("compileKotlin") {
422+
assertTasksExecuted(":compileKotlin")
423+
}
424+
}
425+
}
426+
}

libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/KGPDaemonsBaseTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import org.gradle.tooling.internal.consumer.ConnectorServices
99
import org.junit.jupiter.api.extension.AfterTestExecutionCallback
1010
import org.junit.jupiter.api.extension.RegisterExtension
1111
import java.nio.file.Path
12+
import kotlin.io.path.exists
1213

1314
@DaemonsGradlePluginTests
1415
abstract class KGPDaemonsBaseTest : KGPBaseTest() {
@@ -26,6 +27,6 @@ abstract class KGPDaemonsBaseTest : KGPBaseTest() {
2627
AfterTestExecutionCallback { context ->
2728
println("[KGPDaemonsBaseTest] Test '${context.displayName}' completed. Terminating Gradle and Kotlin daemons.")
2829
ConnectorServices.reset()
29-
awaitKotlinDaemonTermination(kotlinDaemonRunFilesDir)
30+
if (kotlinDaemonRunFilesDir.exists()) awaitKotlinDaemonTermination(kotlinDaemonRunFilesDir)
3031
}
3132
}

libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/compilerRunner/GradleKotlinCompilerWork.kt

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -370,25 +370,53 @@ internal class GradleKotlinCompilerWork @Inject constructor(
370370
Array<String>::class.java
371371
)
372372

373-
val res = exec.invoke(compiler.declaredConstructors.single().newInstance(), out, emptyServices, config.compilerArgs)
374-
val exitCode = ExitCode.valueOf(res.toString())
375-
processCompilerOutput(
376-
messageCollector,
377-
OutputItemsCollectorImpl(),
378-
stream,
379-
exitCode
380-
)
381373
try {
382-
metrics.measure(CLEAR_JAR_CACHE) {
383-
val coreEnvironment = Class.forName("org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment", true, classLoader)
384-
val dispose = coreEnvironment.getMethod("disposeApplicationEnvironment")
385-
dispose.invoke(null)
374+
val res = exec.invoke(compiler.declaredConstructors.single().newInstance(), out, emptyServices, config.compilerArgs)
375+
val exitCode = ExitCode.valueOf(res.toString())
376+
processCompilerOutput(
377+
messageCollector,
378+
OutputItemsCollectorImpl(),
379+
stream,
380+
exitCode
381+
)
382+
try {
383+
metrics.measure(CLEAR_JAR_CACHE) {
384+
val coreEnvironment = Class.forName("org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment", true, classLoader)
385+
val dispose = coreEnvironment.getMethod("disposeApplicationEnvironment")
386+
dispose.invoke(null)
387+
}
388+
} catch (e: Throwable) {
389+
log.warn("Unable to clear jar cache after in-process compilation", e)
386390
}
391+
392+
log.logFinish(KotlinCompilerExecutionStrategy.IN_PROCESS)
393+
return exitCode
394+
} finally {
395+
classLoader.invokeKotlinxCoroutinesDispatcherShutdown()
396+
}
397+
}
398+
399+
private fun ClassLoader.invokeKotlinxCoroutinesDispatcherShutdown() {
400+
val dispatcherClass = try {
401+
Class.forName("kotlinx.coroutines.Dispatchers", true, this)
402+
} catch (_: ClassNotFoundException) {
403+
return
404+
}
405+
406+
try {
407+
val instanceField = dispatcherClass.getField("INSTANCE")
408+
val instance = instanceField.get(null)
409+
val shutdownMethod = dispatcherClass.getMethod("shutdown")
410+
shutdownMethod.invoke(instance)
387411
} catch (e: Throwable) {
388-
log.warn("Unable to clear jar cache after in-process compilation", e)
412+
log.warn(
413+
"""Unable to shutdown Kotlinx coroutines dispatcher after compiler invocation.
414+
| Please report this issue https://kotl.in/issue ;
415+
| To workaround this don't use 'in-process' compiler execution strategy.
416+
| https://kotlinlang.org/docs/gradle-compilation-and-caches.html#defining-kotlin-compiler-execution-strategy""".trimMargin(),
417+
e
418+
)
389419
}
390-
log.logFinish(KotlinCompilerExecutionStrategy.IN_PROCESS)
391-
return exitCode
392420
}
393421

394422
private fun requestedCompilationResults(): EnumSet<CompilationResultCategory> {

0 commit comments

Comments
 (0)