Skip to content

Pr/2920 #2923

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 8, 2021
Merged

Pr/2920 #2923

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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<Any> {
fun Any.toStringWithQuotes() = "\"$this\""
val coroutinesInfo = dumpCoroutinesInfo()
val size = coroutinesInfo.size
val lastObservedThreads = ArrayList<Thread?>(size)
val lastObservedFrames = ArrayList<CoroutineStackFrame?>(size)
val coroutinesInfoAsJson = ArrayList<String>(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.
*/
Expand Down
1 change: 1 addition & 0 deletions kotlinx-coroutines-debug/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CoroutineInfoFromJson>::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)
}
}
}