From 28c8c9231838d8dfd5dc3e30d86e6792f29cee86 Mon Sep 17 00:00:00 2001 From: Daniil Ovchinnikov Date: Mon, 5 Feb 2024 19:41:28 +0100 Subject: [PATCH] IJPL-475 ability to skip waiting for completion of project coroutines GitOrigin-RevId: 8f976fe99b2e8b1d333889ba92a5517da7dfd1e2 --- .../openapi/application/impl/modality.kt | 10 ++-- .../src/com/intellij/ide/shutdown.kt | 57 +++++++++++++++++++ .../project/impl/ProjectManagerImpl.kt | 8 ++- .../intellij/diagnostic/coroutineDumper.kt | 6 +- .../util/resources/misc/registry.properties | 4 ++ 5 files changed, 78 insertions(+), 7 deletions(-) diff --git a/platform/core-impl/src/com/intellij/openapi/application/impl/modality.kt b/platform/core-impl/src/com/intellij/openapi/application/impl/modality.kt index da97dd83a091..915deb21d82f 100644 --- a/platform/core-impl/src/com/intellij/openapi/application/impl/modality.kt +++ b/platform/core-impl/src/com/intellij/openapi/application/impl/modality.kt @@ -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 inModalContext(modalJob: JobProvider, action: (ModalityState) -> T): T { - val newModalityState = LaterInvocator.getCurrentModalityState().appendEntity(modalJob) - LaterInvocator.enterModal(modalJob, newModalityState) +fun 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) } } diff --git a/platform/platform-impl/src/com/intellij/ide/shutdown.kt b/platform/platform-impl/src/com/intellij/ide/shutdown.kt index 87de483b12e4..c6803bf0d410 100644 --- a/platform/platform-impl/src/com/intellij/ide/shutdown.kt +++ b/platform/platform-impl/src/com/intellij/ide/shutdown.kt @@ -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 + } + } +} diff --git a/platform/platform-impl/src/com/intellij/openapi/project/impl/ProjectManagerImpl.kt b/platform/platform-impl/src/com/intellij/openapi/project/impl/ProjectManagerImpl.kt index dbb5d7c54184..df8b7f9dc8fc 100644 --- a/platform/platform-impl/src/com/intellij/openapi/project/impl/ProjectManagerImpl.kt +++ b/platform/platform-impl/src/com/intellij/openapi/project/impl/ProjectManagerImpl.kt @@ -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) + } } } diff --git a/platform/util/base/src/com/intellij/diagnostic/coroutineDumper.kt b/platform/util/base/src/com/intellij/diagnostic/coroutineDumper.kt index c15dfb6994d7..9a9b835158d4 100644 --- a/platform/util/base/src/com/intellij/diagnostic/coroutineDumper.kt +++ b/platform/util/base/src/com/intellij/diagnostic/coroutineDumper.kt @@ -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 { return runCatching { DebugProbes.enableCreationStackTraces = false @@ -43,7 +47,7 @@ fun enableCoroutineDump(): Result { */ @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() diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index 0620da1a3862..b22e070d100e 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -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