diff --git a/platform/util/base/src/com/intellij/diagnostic/coroutineDumper.kt b/platform/util/base/src/com/intellij/diagnostic/coroutineDumper.kt index dee81555dc24..fec643e9cf06 100644 --- a/platform/util/base/src/com/intellij/diagnostic/coroutineDumper.kt +++ b/platform/util/base/src/com/intellij/diagnostic/coroutineDumper.kt @@ -159,22 +159,29 @@ private fun jobTrees(scope: CoroutineScope? = null): Sequence { return sequence { for (job in rootJobs) { - yieldAll(buildJobTrees(job, jobToStack)) + yieldAll(buildJobTrees(job, jobToStack, hashSetOf())) } } } private fun buildJobTrees( job: Job, - jobToStack: Map + jobToStack: Map, + visited: MutableSet ): List { - val info = jobToStack[job] - if (info === null && job is ScopeCoroutine<*>) { - // don't yield ScopeCoroutine without info, such as `coroutineScope` or `withContext` - return job.children.flatMap { buildJobTrees(it, jobToStack) }.toList() - } - else { - return listOf(JobTree(job, info, job.children.flatMap { buildJobTrees(it, jobToStack) }.toList())) + return visited.withElement(job) { notSeenThisJob -> + if (notSeenThisJob) { + val info = jobToStack[job] + if (info === null && job is ScopeCoroutine<*>) { + // don't yield ScopeCoroutine without info, such as `coroutineScope` or `withContext` + job.children.flatMap { buildJobTrees(it, jobToStack, visited) }.toList() + } + else { + listOf(JobTree(job, info, job.children.flatMap { buildJobTrees(it, jobToStack, visited) }.toList())) + } + } else { + listOf(JobTree(RecursiveJob(job), null, emptyList())) + } } } @@ -308,4 +315,20 @@ private fun traceToDump(info: DebugCoroutineInfo, stripTrace: Boolean): List MutableSet.withElement(elem: T, body: (added: Boolean) -> R): R { + val added = add(elem) + try { + return body(added) + } + finally { + if (added) remove(elem) + } +} + +private class RecursiveJob(private val originalJob: Job) : Job by originalJob { + override fun toString(): String { + return "CIRCULAR REFERENCE: $originalJob" + } } \ No newline at end of file diff --git a/platform/util/testSrc/com/intellij/diagnostic/CoroutineDumpTest.kt b/platform/util/testSrc/com/intellij/diagnostic/CoroutineDumpTest.kt new file mode 100644 index 000000000000..21997fcfeb10 --- /dev/null +++ b/platform/util/testSrc/com/intellij/diagnostic/CoroutineDumpTest.kt @@ -0,0 +1,46 @@ +// 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.diagnostic + +import com.intellij.platform.util.coroutines.childScope +import kotlinx.coroutines.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import java.util.concurrent.TimeUnit + +class CoroutineDumpTest { + companion object { + @JvmStatic + @BeforeAll + fun enableDumps() { + enableCoroutineDump() + } + } + + @OptIn(InternalCoroutinesApi::class) + @Suppress("SSBasedInspection", "DEPRECATION_ERROR") + @Test + @Timeout(value = 5, unit = TimeUnit.SECONDS) + fun testRecursiveJobsDump() { + val projectScope = CoroutineScope(CoroutineName("Project")) + val pluginScope = CoroutineScope(CoroutineName("Plugin")) + val activity = projectScope.childScope("Project Activity") + pluginScope.coroutineContext[Job]!!.attachChild(activity.coroutineContext[Job]!! as ChildJob) + // e.g. bug here + activity.coroutineContext[Job]!!.attachChild(pluginScope.coroutineContext[Job]!! as ChildJob) + assertEquals(""" +- JobImpl{Active} + - "Project Activity":supervisor:ChildScope{Active} + - JobImpl{Active} + - CIRCULAR REFERENCE: "Project Activity":supervisor:ChildScope{Active} +""", dumpCoroutines(projectScope, true, true)) + assertEquals(""" +- JobImpl{Active} + - "Project Activity":supervisor:ChildScope{Active} + - CIRCULAR REFERENCE: JobImpl{Active} +""", dumpCoroutines(pluginScope, true, true)) + projectScope.cancel() + pluginScope.cancel() + } +} \ No newline at end of file