diff --git a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt index 73d731091d..f0fc50e658 100644 --- a/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt +++ b/kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt @@ -6,7 +6,6 @@ package kotlinx.coroutines.debug.internal import kotlinx.atomicfu.* import kotlinx.coroutines.* -import kotlinx.coroutines.debug.* import kotlinx.coroutines.internal.* import kotlinx.coroutines.internal.ScopeCoroutine import java.io.* @@ -163,6 +162,60 @@ internal object DebugProbesImpl { } } + /* + * This method optimises the number of packages sent by the IDEA debugger + * to a client VM to speed up fetching of coroutine information. + * + * The return value is an array of objects, which consists of four elements: + * 1) A string in a JSON format that stores information that is needed to display + * every coroutine in the coroutine panel in the IDEA debugger. + * 2) An array of last observed threads. + * 3) An array of last observed frames. + * 4) An array of DebugCoroutineInfo. + * + * ### Implementation note + * For methods like `dumpCoroutinesInfo` JDWP provides `com.sun.jdi.ObjectReference` + * that does a roundtrip to client VM for *each* field or property read. + * To avoid that, we serialize most of the critical for UI data into a primitives + * to save an exponential number of roundtrips. + * + * Internal (JVM-public) method used by IDEA debugger as of 1.6.0-RC. + */ + @OptIn(ExperimentalStdlibApi::class) + public fun dumpCoroutinesInfoAsJsonAndReferences(): Array { + fun Any.toStringWithQuotes() = "\"$this\"" + val coroutinesInfo = dumpCoroutinesInfo() + val size = coroutinesInfo.size + val lastObservedThreads = ArrayList(size) + val lastObservedFrames = ArrayList(size) + val coroutinesInfoAsJson = ArrayList(size) + for (info in coroutinesInfo) { + val context = info.context + val name = context[CoroutineName.Key]?.name?.toStringWithQuotes() + val dispatcher = context[CoroutineDispatcher.Key]?.toStringWithQuotes() + coroutinesInfoAsJson.add( + """ + { + "name": $name, + "id": ${context[CoroutineId.Key]?.id}, + "dispatcher": $dispatcher, + "sequenceNumber": ${info.sequenceNumber}, + "state": "${info.state}" + } + """.trimIndent() + ) + lastObservedFrames.add(info.lastObservedFrame) + lastObservedThreads.add(info.lastObservedThread) + } + + return arrayOf( + "[${coroutinesInfoAsJson.joinToString()}]", + lastObservedThreads.toTypedArray(), + lastObservedFrames.toTypedArray(), + coroutinesInfo.toTypedArray() + ) + } + /* * Internal (JVM-public) method used by IDEA debugger as of 1.4-M3. */ diff --git a/kotlinx-coroutines-debug/build.gradle b/kotlinx-coroutines-debug/build.gradle index 43d94d1841..4830670d24 100644 --- a/kotlinx-coroutines-debug/build.gradle +++ b/kotlinx-coroutines-debug/build.gradle @@ -27,6 +27,7 @@ dependencies { shadowDeps "net.bytebuddy:byte-buddy-agent:$byte_buddy_version" compileOnly "io.projectreactor.tools:blockhound:$blockhound_version" testImplementation "io.projectreactor.tools:blockhound:$blockhound_version" + testImplementation "com.google.code.gson:gson:2.8.6" api "net.java.dev.jna:jna:$jna_version" api "net.java.dev.jna:jna-platform:$jna_version" } diff --git a/kotlinx-coroutines-debug/test/DumpCoroutineInfoAsJsonAndReferencesTest.kt b/kotlinx-coroutines-debug/test/DumpCoroutineInfoAsJsonAndReferencesTest.kt new file mode 100644 index 0000000000..4808470eb6 --- /dev/null +++ b/kotlinx-coroutines-debug/test/DumpCoroutineInfoAsJsonAndReferencesTest.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +package kotlinx.coroutines.debug + +import com.google.gson.* +import kotlinx.coroutines.* +import kotlinx.coroutines.debug.internal.* +import org.junit.Test +import kotlin.coroutines.* +import kotlin.test.* + +@ExperimentalStdlibApi +class DumpCoroutineInfoAsJsonAndReferencesTest : DebugTestBase() { + private data class CoroutineInfoFromJson( + val name: String?, + val id: Long?, + val dispatcher: String?, + val sequenceNumber: Long?, + val state: String? + ) + + @Test + fun testDumpOfUnnamedCoroutine() = + runTestWithNamedDeferred(name = null) + + @Test + fun testDumpOfNamedCoroutine() = + runTestWithNamedDeferred("Name") + + @Test + fun testDumpWithNoCoroutines() { + val dumpResult = DebugProbesImpl.dumpCoroutinesInfoAsJsonAndReferences() + assertEquals(dumpResult.size, 4) + assertIsEmptyArray(dumpResult[1]) + assertIsEmptyArray(dumpResult[2]) + assertIsEmptyArray(dumpResult[3]) + } + + private fun assertIsEmptyArray(obj: Any) = + assertTrue(obj is Array<*> && obj.isEmpty()) + + private fun runTestWithNamedDeferred(name: String?) = runTest { + val context = if (name == null) EmptyCoroutineContext else CoroutineName(name) + val deferred = async(context) { + suspendingMethod() + assertTrue(true) + } + yield() + verifyDump() + deferred.cancelAndJoin() + } + + private suspend fun suspendingMethod() { + delay(Long.MAX_VALUE) + } + + private fun verifyDump() { + val dumpResult = DebugProbesImpl.dumpCoroutinesInfoAsJsonAndReferences() + + assertEquals(dumpResult.size, 4) + + val coroutinesInfoAsJsonString = dumpResult[0] + val lastObservedThreads = dumpResult[1] + val lastObservedFrames = dumpResult[2] + val coroutinesInfo = dumpResult[3] + + assertTrue(coroutinesInfoAsJsonString is String) + assertTrue(lastObservedThreads is Array<*>) + assertTrue(lastObservedFrames is Array<*>) + assertTrue(coroutinesInfo is Array<*>) + + val coroutinesInfoFromJson = Gson().fromJson(coroutinesInfoAsJsonString, Array::class.java) + + val size = coroutinesInfo.size + assertTrue(size != 0) + assertEquals(size, coroutinesInfoFromJson.size) + assertEquals(size, lastObservedFrames.size) + assertEquals(size, lastObservedThreads.size) + + for (i in 0 until size) { + val info = coroutinesInfo[i] + val infoFromJson = coroutinesInfoFromJson[i] + assertTrue(info is DebugCoroutineInfo) + assertEquals(info.lastObservedThread, lastObservedThreads[i]) + assertEquals(info.lastObservedFrame, lastObservedFrames[i]) + assertEquals(info.sequenceNumber, infoFromJson.sequenceNumber) + assertEquals(info.state, infoFromJson.state) + val context = info.context + assertEquals(context[CoroutineName.Key]?.name, infoFromJson.name) + assertEquals(context[CoroutineId.Key]?.id, infoFromJson.id) + assertEquals(context[CoroutineDispatcher.Key]?.toString(), infoFromJson.dispatcher) + } + } +}