IJPL-475 ability to skip waiting for completion of project coroutines

GitOrigin-RevId: 8f976fe99b2e8b1d333889ba92a5517da7dfd1e2
This commit is contained in:
Daniil Ovchinnikov
2024-02-05 19:41:28 +01:00
committed by intellij-monorepo-bot
parent a71c43c894
commit 28c8c92318
5 changed files with 78 additions and 7 deletions

View File

@@ -1,17 +1,17 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.application.impl
import com.intellij.openapi.application.ModalityState
import org.jetbrains.annotations.ApiStatus.Internal
@Internal
fun <T> inModalContext(modalJob: JobProvider, action: (ModalityState) -> T): T {
val newModalityState = LaterInvocator.getCurrentModalityState().appendEntity(modalJob)
LaterInvocator.enterModal(modalJob, newModalityState)
fun <T> inModalContext(modalEntity: Any, action: (ModalityState) -> T): T {
val newModalityState = LaterInvocator.getCurrentModalityState().appendEntity(modalEntity)
LaterInvocator.enterModal(modalEntity, newModalityState)
try {
return action(newModalityState)
}
finally {
LaterInvocator.leaveModal(modalJob)
LaterInvocator.leaveModal(modalEntity)
}
}

View File

@@ -2,9 +2,13 @@
package com.intellij.ide
import com.intellij.diagnostic.dumpCoroutines
import com.intellij.diagnostic.isCoroutineDumpEnabled
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.impl.ApplicationImpl
import com.intellij.openapi.application.impl.inModalContext
import com.intellij.openapi.diagnostic.Attachment
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.trace
import com.intellij.openapi.progress.impl.pumpEventsForHierarchy
import com.intellij.openapi.project.impl.ProjectImpl
import com.intellij.openapi.util.EmptyRunnable
@@ -12,11 +16,14 @@ import com.intellij.openapi.util.IntellijInternalApi
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.TaskCancellation
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
import com.intellij.serviceContainer.ComponentManagerImpl
import com.intellij.util.ObjectUtils
import com.intellij.util.io.blockingDispatcher
import com.intellij.util.ui.EDT
import kotlinx.coroutines.*
import javax.swing.SwingUtilities
import kotlin.time.Duration
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.Duration.Companion.seconds
private val LOG = Logger.getInstance("#com.intellij.ide.shutdown")
@@ -83,3 +90,53 @@ internal fun cancelAndJoinBlocking(
}
private val delayUntilCoroutineDump: Duration = 10.seconds
internal fun cancelAndTryJoin(project: ProjectImpl) {
val containerScope = project.coroutineScope
val debugString = "Project $project"
LOG.trace { "$debugString: trying to join scope" }
val containerJob = containerScope.coroutineContext.job
val start = System.nanoTime()
containerJob.cancel()
if (containerJob.isCompleted) {
LOG.trace { "$debugString: already completed" }
return
}
inModalContext(ObjectUtils.sentinel("$debugString shutdown")) { // enter modality to avoid running arbitrary write actions which
LOG.trace { "$debugString: flushing EDT queue" }
IdeEventQueue.getInstance().flushQueue() // flush once to give EDT coroutines a chance to complete
}
if (containerJob.isCompleted) {
val elapsed = System.nanoTime() - start
// this might mean that the flush helped coroutines to complete OR completion happened on BG during the flush
LOG.trace { "$debugString: completed after flush in ${elapsed.nanoseconds}" }
return
}
if (!isCoroutineDumpEnabled()) {
return
}
// TODO install and use currentThreadCoroutineScope instead OR make this function suspending
val applicationScope = (ApplicationManager.getApplication() as ComponentManagerImpl).getCoroutineScope()
applicationScope.launch(@OptIn(IntellijInternalApi::class, DelicateCoroutinesApi::class) blockingDispatcher) {
val dumpJob = launch {
delay(delayUntilCoroutineDump)
LOG.error(
"$debugString: scope was not completed in $delayUntilCoroutineDump",
Attachment("coroutineDump.txt", dumpCoroutines(scope = containerScope)!!),
)
}
try {
containerJob.join()
val elapsed = System.nanoTime() - start
LOG.trace { "$debugString: completed in ${elapsed.nanoseconds}" }
dumpJob.cancel()
}
catch (ce: CancellationException) {
LOG.trace { "$debugString: coroutine dump was cancelled" }
throw ce
}
}
}

View File

@@ -58,6 +58,7 @@ import com.intellij.openapi.ui.MessageDialogBuilder
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.*
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.impl.ZipHandler
import com.intellij.openapi.wm.IdeFocusManager
@@ -382,7 +383,12 @@ open class ProjectManagerImpl : ProjectManagerEx(), Disposable {
// somebody can start progress here, do not wrap in write action
fireProjectClosing(project)
if (project is ProjectImpl) {
cancelAndJoinBlocking(project)
if (Registry.`is`("ide.await.project.scope.completion")) {
cancelAndJoinBlocking(project)
}
else {
cancelAndTryJoin(project)
}
}
}

View File

@@ -29,6 +29,10 @@ fun isCoroutineDumpHeader(line: String): Boolean {
return line == COROUTINE_DUMP_HEADER || line == COROUTINE_DUMP_HEADER_STRIPPED
}
fun isCoroutineDumpEnabled(): Boolean {
return DebugProbes.isInstalled
}
fun enableCoroutineDump(): Result<Unit> {
return runCatching {
DebugProbes.enableCreationStackTraces = false
@@ -43,7 +47,7 @@ fun enableCoroutineDump(): Result<Unit> {
*/
@JvmOverloads
fun dumpCoroutines(scope: CoroutineScope? = null, stripDump: Boolean = true, deduplicateTrees: Boolean = true): String? {
if (!DebugProbes.isInstalled) {
if (!isCoroutineDumpEnabled()) {
return null
}
val charset = StandardCharsets.UTF_8.name()

View File

@@ -2424,5 +2424,9 @@ editor.show.sticky.lines.debug.description=Show editor sticky lines in debug mod
ide.background.save.settings=false
ide.background.save.settings.description=Save project settings on a background thread (experimental)
ide.await.project.scope.completion=true
ide.await.project.scope.completion.description=Whether closing project should wait for all coroutines to be completed before disposing the project. \
When `false`, the project scope is cancelled without waiting. This may result in exceptions and project leaks.
# please leave this note as last line
# TODO please use EP com.intellij.registryKey for plugin/product specific keys