Skip to content

Commit c76cc24

Browse files
qwwdfsadNikita Nazarovnikita-nazarov
authored
Add method that allows dumping all coroutine information needed by the IDEA debugger in one call (#2923)
Co-authored-by: Nikita Nazarov <[email protected]> Co-authored-by: Nikita Nazarov <[email protected]>
1 parent 3f459d5 commit c76cc24

File tree

3 files changed

+151
-1
lines changed

3 files changed

+151
-1
lines changed

kotlinx-coroutines-core/jvm/src/debug/internal/DebugProbesImpl.kt

+54-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package kotlinx.coroutines.debug.internal
66

77
import kotlinx.atomicfu.*
88
import kotlinx.coroutines.*
9-
import kotlinx.coroutines.debug.*
109
import kotlinx.coroutines.internal.*
1110
import kotlinx.coroutines.internal.ScopeCoroutine
1211
import java.io.*
@@ -163,6 +162,60 @@ internal object DebugProbesImpl {
163162
}
164163
}
165164

165+
/*
166+
* This method optimises the number of packages sent by the IDEA debugger
167+
* to a client VM to speed up fetching of coroutine information.
168+
*
169+
* The return value is an array of objects, which consists of four elements:
170+
* 1) A string in a JSON format that stores information that is needed to display
171+
* every coroutine in the coroutine panel in the IDEA debugger.
172+
* 2) An array of last observed threads.
173+
* 3) An array of last observed frames.
174+
* 4) An array of DebugCoroutineInfo.
175+
*
176+
* ### Implementation note
177+
* For methods like `dumpCoroutinesInfo` JDWP provides `com.sun.jdi.ObjectReference`
178+
* that does a roundtrip to client VM for *each* field or property read.
179+
* To avoid that, we serialize most of the critical for UI data into a primitives
180+
* to save an exponential number of roundtrips.
181+
*
182+
* Internal (JVM-public) method used by IDEA debugger as of 1.6.0-RC.
183+
*/
184+
@OptIn(ExperimentalStdlibApi::class)
185+
public fun dumpCoroutinesInfoAsJsonAndReferences(): Array<Any> {
186+
fun Any.toStringWithQuotes() = "\"$this\""
187+
val coroutinesInfo = dumpCoroutinesInfo()
188+
val size = coroutinesInfo.size
189+
val lastObservedThreads = ArrayList<Thread?>(size)
190+
val lastObservedFrames = ArrayList<CoroutineStackFrame?>(size)
191+
val coroutinesInfoAsJson = ArrayList<String>(size)
192+
for (info in coroutinesInfo) {
193+
val context = info.context
194+
val name = context[CoroutineName.Key]?.name?.toStringWithQuotes()
195+
val dispatcher = context[CoroutineDispatcher.Key]?.toStringWithQuotes()
196+
coroutinesInfoAsJson.add(
197+
"""
198+
{
199+
"name": $name,
200+
"id": ${context[CoroutineId.Key]?.id},
201+
"dispatcher": $dispatcher,
202+
"sequenceNumber": ${info.sequenceNumber},
203+
"state": "${info.state}"
204+
}
205+
""".trimIndent()
206+
)
207+
lastObservedFrames.add(info.lastObservedFrame)
208+
lastObservedThreads.add(info.lastObservedThread)
209+
}
210+
211+
return arrayOf(
212+
"[${coroutinesInfoAsJson.joinToString()}]",
213+
lastObservedThreads.toTypedArray(),
214+
lastObservedFrames.toTypedArray(),
215+
coroutinesInfo.toTypedArray()
216+
)
217+
}
218+
166219
/*
167220
* Internal (JVM-public) method used by IDEA debugger as of 1.4-M3.
168221
*/

kotlinx-coroutines-debug/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ dependencies {
2727
shadowDeps "net.bytebuddy:byte-buddy-agent:$byte_buddy_version"
2828
compileOnly "io.projectreactor.tools:blockhound:$blockhound_version"
2929
testImplementation "io.projectreactor.tools:blockhound:$blockhound_version"
30+
testImplementation "com.google.code.gson:gson:2.8.6"
3031
api "net.java.dev.jna:jna:$jna_version"
3132
api "net.java.dev.jna:jna-platform:$jna_version"
3233
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
5+
package kotlinx.coroutines.debug
6+
7+
import com.google.gson.*
8+
import kotlinx.coroutines.*
9+
import kotlinx.coroutines.debug.internal.*
10+
import org.junit.Test
11+
import kotlin.coroutines.*
12+
import kotlin.test.*
13+
14+
@ExperimentalStdlibApi
15+
class DumpCoroutineInfoAsJsonAndReferencesTest : DebugTestBase() {
16+
private data class CoroutineInfoFromJson(
17+
val name: String?,
18+
val id: Long?,
19+
val dispatcher: String?,
20+
val sequenceNumber: Long?,
21+
val state: String?
22+
)
23+
24+
@Test
25+
fun testDumpOfUnnamedCoroutine() =
26+
runTestWithNamedDeferred(name = null)
27+
28+
@Test
29+
fun testDumpOfNamedCoroutine() =
30+
runTestWithNamedDeferred("Name")
31+
32+
@Test
33+
fun testDumpWithNoCoroutines() {
34+
val dumpResult = DebugProbesImpl.dumpCoroutinesInfoAsJsonAndReferences()
35+
assertEquals(dumpResult.size, 4)
36+
assertIsEmptyArray(dumpResult[1])
37+
assertIsEmptyArray(dumpResult[2])
38+
assertIsEmptyArray(dumpResult[3])
39+
}
40+
41+
private fun assertIsEmptyArray(obj: Any) =
42+
assertTrue(obj is Array<*> && obj.isEmpty())
43+
44+
private fun runTestWithNamedDeferred(name: String?) = runTest {
45+
val context = if (name == null) EmptyCoroutineContext else CoroutineName(name)
46+
val deferred = async(context) {
47+
suspendingMethod()
48+
assertTrue(true)
49+
}
50+
yield()
51+
verifyDump()
52+
deferred.cancelAndJoin()
53+
}
54+
55+
private suspend fun suspendingMethod() {
56+
delay(Long.MAX_VALUE)
57+
}
58+
59+
private fun verifyDump() {
60+
val dumpResult = DebugProbesImpl.dumpCoroutinesInfoAsJsonAndReferences()
61+
62+
assertEquals(dumpResult.size, 4)
63+
64+
val coroutinesInfoAsJsonString = dumpResult[0]
65+
val lastObservedThreads = dumpResult[1]
66+
val lastObservedFrames = dumpResult[2]
67+
val coroutinesInfo = dumpResult[3]
68+
69+
assertTrue(coroutinesInfoAsJsonString is String)
70+
assertTrue(lastObservedThreads is Array<*>)
71+
assertTrue(lastObservedFrames is Array<*>)
72+
assertTrue(coroutinesInfo is Array<*>)
73+
74+
val coroutinesInfoFromJson = Gson().fromJson(coroutinesInfoAsJsonString, Array<CoroutineInfoFromJson>::class.java)
75+
76+
val size = coroutinesInfo.size
77+
assertTrue(size != 0)
78+
assertEquals(size, coroutinesInfoFromJson.size)
79+
assertEquals(size, lastObservedFrames.size)
80+
assertEquals(size, lastObservedThreads.size)
81+
82+
for (i in 0 until size) {
83+
val info = coroutinesInfo[i]
84+
val infoFromJson = coroutinesInfoFromJson[i]
85+
assertTrue(info is DebugCoroutineInfo)
86+
assertEquals(info.lastObservedThread, lastObservedThreads[i])
87+
assertEquals(info.lastObservedFrame, lastObservedFrames[i])
88+
assertEquals(info.sequenceNumber, infoFromJson.sequenceNumber)
89+
assertEquals(info.state, infoFromJson.state)
90+
val context = info.context
91+
assertEquals(context[CoroutineName.Key]?.name, infoFromJson.name)
92+
assertEquals(context[CoroutineId.Key]?.id, infoFromJson.id)
93+
assertEquals(context[CoroutineDispatcher.Key]?.toString(), infoFromJson.dispatcher)
94+
}
95+
}
96+
}

0 commit comments

Comments
 (0)