IJPL-156281 make coroutine dumper resilient to circular job dependencies

GitOrigin-RevId: 95d64a8fca56c22a2fd9c3d0a7efd29c39ff8126
This commit is contained in:
Vadim Salavatov
2024-06-07 19:16:09 +02:00
committed by intellij-monorepo-bot
parent 58e9edebab
commit fe1a45223e
2 changed files with 78 additions and 9 deletions

View File

@@ -159,22 +159,29 @@ private fun jobTrees(scope: CoroutineScope? = null): Sequence<JobTree> {
return sequence {
for (job in rootJobs) {
yieldAll(buildJobTrees(job, jobToStack))
yieldAll(buildJobTrees(job, jobToStack, hashSetOf()))
}
}
}
private fun buildJobTrees(
job: Job,
jobToStack: Map<Job, DebugCoroutineInfo>
jobToStack: Map<Job, DebugCoroutineInfo>,
visited: MutableSet<Job>
): List<JobTree> {
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<Sta
return stripCoroutineTrace(trace)
}
return DebugProbesImpl.enhanceStackTraceWithThreadDump(info, trace)
}
private fun <T, R> MutableSet<T>.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"
}
}

View File

@@ -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()
}
}