Merge branch 'develop' into debugger-sm-bypass
# Conflicts:
# kotlinx-coroutines-core/common/src/Timeout.kt
diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt
index 96a35c7..183495a 100644
--- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt
+++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt
@@ -1,11 +1,11 @@
-public final class kotlinx/coroutines/debug/CoroutineState {
- public final fun component1 ()Lkotlin/coroutines/Continuation;
- public final fun copy (Lkotlin/coroutines/Continuation;Lkotlin/coroutines/jvm/internal/CoroutineStackFrame;J)Lkotlinx/coroutines/debug/CoroutineState;
- public static synthetic fun copy$default (Lkotlinx/coroutines/debug/CoroutineState;Lkotlin/coroutines/Continuation;Lkotlin/coroutines/jvm/internal/CoroutineStackFrame;JILjava/lang/Object;)Lkotlinx/coroutines/debug/CoroutineState;
+public final class kotlinx/coroutines/debug/CoroutineInfo {
+ public final fun component1 ()Lkotlin/coroutines/CoroutineContext;
+ public final fun copy (Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/jvm/internal/CoroutineStackFrame;J)Lkotlinx/coroutines/debug/CoroutineInfo;
+ public static synthetic fun copy$default (Lkotlinx/coroutines/debug/CoroutineInfo;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/jvm/internal/CoroutineStackFrame;JILjava/lang/Object;)Lkotlinx/coroutines/debug/CoroutineInfo;
public fun equals (Ljava/lang/Object;)Z
- public final fun getContinuation ()Lkotlin/coroutines/Continuation;
+ public final fun getContext ()Lkotlin/coroutines/CoroutineContext;
public final fun getCreationStackTrace ()Ljava/util/List;
- public final fun getJobOrNull ()Lkotlinx/coroutines/Job;
+ public final fun getJob ()Lkotlinx/coroutines/Job;
public final fun getState ()Lkotlinx/coroutines/debug/State;
public fun hashCode ()I
public final fun lastObservedStackTrace ()Ljava/util/List;
@@ -16,7 +16,7 @@
public static final field INSTANCE Lkotlinx/coroutines/debug/DebugProbes;
public final fun dumpCoroutines (Ljava/io/PrintStream;)V
public static synthetic fun dumpCoroutines$default (Lkotlinx/coroutines/debug/DebugProbes;Ljava/io/PrintStream;ILjava/lang/Object;)V
- public final fun dumpCoroutinesState ()Ljava/util/List;
+ public final fun dumpCoroutinesInfo ()Ljava/util/List;
public final fun getSanitizeStackTraces ()Z
public final fun install ()V
public final fun jobToString (Lkotlinx/coroutines/Job;)Ljava/lang/String;
diff --git a/kotlinx-coroutines-core/common/src/Timeout.kt b/kotlinx-coroutines-core/common/src/Timeout.kt
index 6ffb97c..efda5a7 100644
--- a/kotlinx-coroutines-core/common/src/Timeout.kt
+++ b/kotlinx-coroutines-core/common/src/Timeout.kt
@@ -83,8 +83,8 @@
@JvmField val uCont: Continuation<U> // unintercepted continuation
) : AbstractCoroutine<T>(uCont.context, active = true), Runnable, Continuation<T>, CoroutineStackFrame {
override val defaultResumeMode: Int get() = MODE_DIRECT
- override val callerFrame: CoroutineStackFrame? get() = (uCont as? CoroutineStackFrame)?.callerFrame
- override fun getStackTraceElement(): StackTraceElement? = (uCont as? CoroutineStackFrame)?.getStackTraceElement()
+ override val callerFrame: CoroutineStackFrame? get() = (uCont as? CoroutineStackFrame)
+ override fun getStackTraceElement(): StackTraceElement? = null
override val cancelsParent: Boolean
get() = false // it throws exception to parent instead of cancelling it
diff --git a/kotlinx-coroutines-debug/README.md b/kotlinx-coroutines-debug/README.md
index 0dc413c..25fc745 100644
--- a/kotlinx-coroutines-debug/README.md
+++ b/kotlinx-coroutines-debug/README.md
@@ -10,7 +10,7 @@
After that, you can use [DebugProbes.dumpCoroutines] to print all active (suspended or running) coroutines, including their state, creation and
suspension stacktraces.
-Additionally, it is possible to process the list of such coroutines via [DebugProbes.dumpCoroutinesState] or dump isolated parts
+Additionally, it is possible to process the list of such coroutines via [DebugProbes.dumpCoroutinesInfo] or dump isolated parts
of coroutines hierarchy referenced by a [Job] or [CoroutineScope] instances using [DebugProbes.printJob] and [DebugProbes.printScope] respectively.
### Using in your project
@@ -159,7 +159,7 @@
[DebugProbes]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/index.html
[DebugProbes.install]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/install.html
[DebugProbes.dumpCoroutines]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/dump-coroutines.html
-[DebugProbes.dumpCoroutinesState]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/dump-coroutines-state.html
+[DebugProbes.dumpCoroutinesInfo]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/dump-coroutines-info.html
[DebugProbes.printJob]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/print-job.html
[DebugProbes.printScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/print-scope.html
<!--- INDEX kotlinx.coroutines.debug.junit4 -->
diff --git a/kotlinx-coroutines-debug/src/CoroutineState.kt b/kotlinx-coroutines-debug/src/CoroutineInfo.kt
similarity index 90%
rename from kotlinx-coroutines-debug/src/CoroutineState.kt
rename to kotlinx-coroutines-debug/src/CoroutineInfo.kt
index 9d4489d..2d4b680 100644
--- a/kotlinx-coroutines-debug/src/CoroutineState.kt
+++ b/kotlinx-coroutines-debug/src/CoroutineInfo.kt
@@ -12,11 +12,11 @@
import kotlin.coroutines.jvm.internal.*
/**
- * Class describing coroutine state.
+ * Class describing coroutine info such as its context, state and stacktrace.
*/
@ExperimentalCoroutinesApi
-public data class CoroutineState internal constructor(
- public val continuation: Continuation<*>,
+public data class CoroutineInfo internal constructor(
+ val context: CoroutineContext,
private val creationStackBottom: CoroutineStackFrame,
@JvmField internal val sequenceNumber: Long
) {
@@ -25,7 +25,7 @@
* [Job] associated with a current coroutine or null.
* May be later used in [DebugProbes.printJob].
*/
- public val jobOrNull: Job? get() = continuation.context[Job]
+ public val job: Job? get() = context[Job]
/**
* Creation stacktrace of the coroutine.
@@ -42,11 +42,12 @@
@JvmField
internal var lastObservedThread: Thread? = null
- private var lastObservedFrame: CoroutineStackFrame? = null
+ @JvmField
+ internal var lastObservedFrame: CoroutineStackFrame? = null
// Copy constructor
- internal constructor(coroutine: Continuation<*>, state: CoroutineState) : this(
- coroutine,
+ internal constructor(coroutine: Continuation<*>, state: CoroutineInfo) : this(
+ coroutine.context,
state.creationStackBottom,
state.sequenceNumber
) {
@@ -54,6 +55,21 @@
this.lastObservedFrame = state.lastObservedFrame
}
+ /**
+ * Last observed stacktrace of the coroutine captured on its suspension or resumption point.
+ * It means that for [running][State.RUNNING] coroutines resulting stacktrace is inaccurate and
+ * reflects stacktrace of the resumption point, not the actual current stacktrace.
+ */
+ public fun lastObservedStackTrace(): List<StackTraceElement> {
+ var frame: CoroutineStackFrame? = lastObservedFrame ?: return emptyList()
+ val result = ArrayList<StackTraceElement>()
+ while (frame != null) {
+ frame.getStackTraceElement()?.let { result.add(sanitize(it)) }
+ frame = frame.callerFrame
+ }
+ return result
+ }
+
private fun creationStackTrace(): List<StackTraceElement> {
// Skip "Coroutine creation stacktrace" frame
return sequence<StackTraceElement> { yieldFrames(creationStackBottom.callerFrame) }.toList()
@@ -79,21 +95,6 @@
lastObservedThread = null
}
}
-
- /**
- * Last observed stacktrace of the coroutine captured on its suspension or resumption point.
- * It means that for [running][State.RUNNING] coroutines resulting stacktrace is inaccurate and
- * reflects stacktrace of the resumption point, not the actual current stacktrace.
- */
- public fun lastObservedStackTrace(): List<StackTraceElement> {
- var frame: CoroutineStackFrame? = lastObservedFrame ?: return emptyList()
- val result = ArrayList<StackTraceElement>()
- while (frame != null) {
- frame.getStackTraceElement()?.let { result.add(sanitize(it)) }
- frame = frame.callerFrame
- }
- return result
- }
}
/**
diff --git a/kotlinx-coroutines-debug/src/DebugProbes.kt b/kotlinx-coroutines-debug/src/DebugProbes.kt
index af30c5e..a81fd7a 100644
--- a/kotlinx-coroutines-debug/src/DebugProbes.kt
+++ b/kotlinx-coroutines-debug/src/DebugProbes.kt
@@ -95,10 +95,10 @@
printJob(scope.coroutineContext[Job] ?: error("Job is not present in the scope"), out)
/**
- * Returns all existing coroutine states.
+ * Returns all existing coroutines info.
* The resulting collection represents a consistent snapshot of all existing coroutines at the moment of invocation.
*/
- public fun dumpCoroutinesState(): List<CoroutineState> = DebugProbesImpl.dumpCoroutinesState()
+ public fun dumpCoroutinesInfo(): List<CoroutineInfo> = DebugProbesImpl.dumpCoroutinesInfo()
/**
* Dumps all active coroutines into the given output stream, providing a consistent snapshot of all existing coroutines at the moment of invocation.
diff --git a/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt b/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt
index c97515e..29b1e36 100644
--- a/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt
+++ b/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt
@@ -6,7 +6,6 @@
import kotlinx.coroutines.*
import kotlinx.coroutines.debug.*
-import kotlinx.coroutines.internal.artificialFrame
import net.bytebuddy.*
import net.bytebuddy.agent.*
import net.bytebuddy.dynamic.loading.*
@@ -14,8 +13,10 @@
import java.text.*
import java.util.*
import kotlin.collections.ArrayList
+import kotlin.collections.HashMap
import kotlin.coroutines.*
import kotlin.coroutines.jvm.internal.*
+import kotlinx.coroutines.internal.artificialFrame as createArtificialFrame // IDEA bug workaround
/**
* Mirror of [DebugProbes] with actual implementation.
@@ -25,13 +26,23 @@
internal object DebugProbesImpl {
private const val ARTIFICIAL_FRAME_MESSAGE = "Coroutine creation stacktrace"
private val dateFormat = SimpleDateFormat("yyyy/MM/dd HH:mm:ss")
- private val capturedCoroutines = WeakHashMap<ArtificialStackFrame<*>, CoroutineState>()
+ private val capturedCoroutines = HashSet<CoroutineOwner<*>>()
@Volatile
private var installations = 0
private val isInstalled: Boolean get() = installations > 0
// To sort coroutines by creation order, used as unique id
private var sequenceNumber: Long = 0
+ /*
+ * This is an optimization in the face of KT-29997:
+ * Consider suspending call stack a()->b()->c() and c() completes its execution and every call is
+ * "almost" in tail position.
+ *
+ * Then at least three RUNNING -> RUNNING transitions will occur consecutively and complexity of each is O(depth).
+ * To avoid that quadratic complexity, we are caching lookup result for such chains in this map and update it incrementally.
+ */
+ private val callerInfoCache = HashMap<CoroutineStackFrame, CoroutineInfo>()
+
@Synchronized
public fun install() {
if (++installations > 1) return
@@ -53,6 +64,7 @@
if (--installations != 0) return
capturedCoroutines.clear()
+ callerInfoCache.clear()
val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt")
val cl2 = Class.forName("kotlinx.coroutines.debug.internal.NoOpProbesKt")
@@ -67,17 +79,17 @@
public fun hierarchyToString(job: Job): String {
check(isInstalled) { "Debug probes are not installed" }
val jobToStack = capturedCoroutines
- .filterKeys { it.delegate.context[Job] != null }
- .mapKeys { it.key.delegate.context[Job]!! }
+ .filter { it.delegate.context[Job] != null }
+ .associateBy({ it.delegate.context[Job]!! }, {it.info})
return buildString {
job.build(jobToStack, this, "")
}
}
- private fun Job.build(map: Map<Job, CoroutineState>, builder: StringBuilder, indent: String) {
- val state = map[this]
+ private fun Job.build(map: Map<Job, CoroutineInfo>, builder: StringBuilder, indent: String) {
+ val info = map[this]
val newIndent: String
- if (state == null) { // Append coroutine without stacktrace
+ if (info == null) { // Append coroutine without stacktrace
// Do not print scoped coroutines and do not increase indentation level
@Suppress("INVISIBLE_REFERENCE")
if (this !is kotlinx.coroutines.internal.ScopeCoroutine<*>) {
@@ -88,9 +100,9 @@
}
} else {
// Append coroutine with its last stacktrace element
- val element = state.lastObservedStackTrace().firstOrNull()
- val contState = state.state
- builder.append("$indent$debugString, continuation is $contState at line $element\n")
+ val element = info.lastObservedStackTrace().firstOrNull()
+ val state = info.state
+ builder.append("$indent$debugString, continuation is $state at line $element\n")
newIndent = indent + "\t"
}
// Append children with new indent
@@ -103,39 +115,40 @@
private val Job.debugString: String get() = if (this is JobSupport) toDebugString() else toString()
@Synchronized
- public fun dumpCoroutinesState(): List<CoroutineState> {
+ public fun dumpCoroutinesInfo(): List<CoroutineInfo> {
check(isInstalled) { "Debug probes are not installed" }
- return capturedCoroutines.entries.asSequence()
- .map { CoroutineState(it.key.delegate, it.value) }
+ return capturedCoroutines.asSequence()
+ .map { CoroutineInfo(it.delegate, it.info) }
.sortedBy { it.sequenceNumber }
.toList()
}
public fun dumpCoroutines(out: PrintStream) {
- check(isInstalled) { "Debug probes are not installed" }
// Avoid inference with other out/err invocations by creating a string first
dumpCoroutines().let { out.println(it) }
}
@Synchronized
private fun dumpCoroutines(): String = buildString {
+ check(isInstalled) { "Debug probes are not installed" }
// Synchronization window can be reduce even more, but no need to do it here
append("Coroutines dump ${dateFormat.format(System.currentTimeMillis())}")
capturedCoroutines
.asSequence()
- .sortedBy { it.value.sequenceNumber }
- .forEach { (key, value) ->
- val observedStackTrace = value.lastObservedStackTrace()
- val enhancedStackTrace = enhanceStackTraceWithThreadDump(value, observedStackTrace)
- val state = if (value.state == State.RUNNING && enhancedStackTrace === observedStackTrace)
- "${value.state} (Last suspension stacktrace, not an actual stacktrace)"
+ .sortedBy { it.info.sequenceNumber }
+ .forEach { owner ->
+ val info = owner.info
+ val observedStackTrace = info.lastObservedStackTrace()
+ val enhancedStackTrace = enhanceStackTraceWithThreadDump(info, observedStackTrace)
+ val state = if (info.state == State.RUNNING && enhancedStackTrace === observedStackTrace)
+ "${info.state} (Last suspension stacktrace, not an actual stacktrace)"
else
- value.state.toString()
+ info.state.toString()
- append("\n\nCoroutine $key, state: $state")
+ append("\n\nCoroutine ${owner.delegate}, state: $state")
if (observedStackTrace.isEmpty()) {
- append("\n\tat ${artificialFrame(ARTIFICIAL_FRAME_MESSAGE)}")
- printStackTrace(value.creationStackTrace)
+ append("\n\tat ${createArtificialFrame(ARTIFICIAL_FRAME_MESSAGE)}")
+ printStackTrace(info.creationStackTrace)
} else {
printStackTrace(enhancedStackTrace)
}
@@ -143,17 +156,17 @@
}
/**
- * Tries to enhance [coroutineTrace] (obtained by call to [CoroutineState.lastObservedStackTrace]) with
- * thread dump of [CoroutineState.lastObservedThread].
+ * Tries to enhance [coroutineTrace] (obtained by call to [CoroutineInfo.lastObservedStackTrace]) with
+ * thread dump of [CoroutineInfo.lastObservedThread].
*
* Returns [coroutineTrace] if enhancement was unsuccessful or the enhancement result.
*/
private fun enhanceStackTraceWithThreadDump(
- state: CoroutineState,
+ info: CoroutineInfo,
coroutineTrace: List<StackTraceElement>
): List<StackTraceElement> {
- val thread = state.lastObservedThread
- if (state.state != State.RUNNING || thread == null) return coroutineTrace
+ val thread = info.lastObservedThread
+ if (info.state != State.RUNNING || thread == null) return coroutineTrace
// Avoid security manager issues
val actualTrace = runCatching { thread.stackTrace }.getOrNull()
?: return coroutineTrace
@@ -168,12 +181,11 @@
* 2) Find the next frame after BaseContinuationImpl.resumeWith (continuation machinery).
* Invariant: this method is called under the lock, so such method **should** be present
* in continuation stacktrace.
- *
* 3) Find target method in continuation stacktrace (metadata-based)
* 4) Prepend dumped stacktrace (trimmed by target frame) to continuation stacktrace
*
* Heuristic may fail on recursion and overloads, but it will be automatically improved
- * with KT-29997
+ * with KT-29997.
*/
val indexOfResumeWith = actualTrace.indexOfFirst {
it.className == "kotlin.coroutines.jvm.internal.BaseContinuationImpl" &&
@@ -181,33 +193,62 @@
it.fileName == "ContinuationImpl.kt"
}
- // We haven't found "BaseContinuationImpl.resumeWith" resume call in stacktrace
- // This is some inconsistency in machinery, do not fail, fallback
- val continuationFrame = actualTrace.getOrNull(indexOfResumeWith - 1)
- ?: return coroutineTrace
+ val (continuationStartFrame, frameSkipped) = findContinuationStartIndex(
+ indexOfResumeWith,
+ actualTrace,
+ coroutineTrace)
- val continuationStartFrame = coroutineTrace.indexOfFirst {
- it.fileName == continuationFrame.fileName &&
- it.className == continuationFrame.className &&
- it.methodName == continuationFrame.methodName
- } + 1
+ if (continuationStartFrame == -1) return coroutineTrace
- if (continuationStartFrame == 0) return coroutineTrace
-
- val expectedSize = indexOfResumeWith + coroutineTrace.size - continuationStartFrame
+ val delta = if (frameSkipped) 1 else 0
+ val expectedSize = indexOfResumeWith + coroutineTrace.size - continuationStartFrame - 1 - delta
val result = ArrayList<StackTraceElement>(expectedSize)
-
- for (index in 0 until indexOfResumeWith) {
+ for (index in 0 until indexOfResumeWith - delta) {
result += actualTrace[index]
}
- for (index in continuationStartFrame until coroutineTrace.size) {
+ for (index in continuationStartFrame + 1 until coroutineTrace.size) {
result += coroutineTrace[index]
}
return result
}
+ /**
+ * Tries to find the lowest meaningful frame above `resumeWith` in the real stacktrace and
+ * its match in a coroutines stacktrace (steps 2-3 in heuristic).
+ *
+ * This method does more than just matching `realTrace.indexOf(resumeWith) - 1`:
+ * If method above `resumeWith` has no line number (thus it is `stateMachine.invokeSuspend`),
+ * it's skipped and attempt to match next one is made because state machine could have been missing in the original coroutine stacktrace.
+ *
+ * Returns index of such frame (or -1) and flag indicating whether frame with state machine was skipped
+ */
+ private fun findContinuationStartIndex(
+ indexOfResumeWith: Int,
+ actualTrace: Array<StackTraceElement>,
+ coroutineTrace: List<StackTraceElement>
+ ): Pair<Int, Boolean> {
+ val result = findIndexOfFrame(indexOfResumeWith - 1, actualTrace, coroutineTrace)
+ if (result == -1) return findIndexOfFrame(indexOfResumeWith - 2, actualTrace, coroutineTrace) to true
+ return result to false
+ }
+
+ private fun findIndexOfFrame(
+ frameIndex: Int,
+ actualTrace: Array<StackTraceElement>,
+ coroutineTrace: List<StackTraceElement>
+ ): Int {
+ val continuationFrame = actualTrace.getOrNull(frameIndex)
+ ?: return -1
+
+ return coroutineTrace.indexOfFirst {
+ it.fileName == continuationFrame.fileName &&
+ it.className == continuationFrame.className &&
+ it.methodName == continuationFrame.methodName
+ }
+ }
+
private fun StringBuilder.printStackTrace(frames: List<StackTraceElement>) {
frames.forEach { frame ->
append("\n\tat $frame")
@@ -219,23 +260,53 @@
internal fun probeCoroutineSuspended(frame: Continuation<*>) = updateState(frame, State.SUSPENDED)
private fun updateState(frame: Continuation<*>, state: State) {
- if (!isInstalled) return
+ // KT-29997 is here only since 1.3.30
+ if (state == State.RUNNING && KotlinVersion.CURRENT.isAtLeast(1, 3, 30)) {
+ val stackFrame = frame as? CoroutineStackFrame ?: return
+ updateRunningState(stackFrame, state)
+ return
+ }
+
// Find ArtificialStackFrame of the coroutine
- val owner = frame.owner()
+ val owner = frame.owner() ?: return
updateState(owner, frame, state)
}
- @Synchronized
- private fun updateState(owner: ArtificialStackFrame<*>?, frame: Continuation<*>, state: State) {
- val coroutineState = capturedCoroutines[owner] ?: return
- coroutineState.updateState(state, frame)
+ @Synchronized // See comment to callerInfoCache
+ private fun updateRunningState(frame: CoroutineStackFrame, state: State) {
+ if (!isInstalled) return
+ // Lookup coroutine info in cache or by traversing stack frame
+ val info: CoroutineInfo
+ val cached = callerInfoCache.remove(frame)
+ if (cached != null) {
+ info = cached
+ } else {
+ info = frame.owner()?.info ?: return
+ // Guard against improper implementations of CoroutineStackFrame and bugs in the compiler
+ callerInfoCache.remove(info.lastObservedFrame?.realCaller())
+ }
+
+ info.updateState(state, frame as Continuation<*>)
+ // Do not cache it for proxy-classes such as ScopeCoroutines
+ val caller = frame.realCaller() ?: return
+ callerInfoCache[caller] = info
}
- private fun Continuation<*>.owner(): ArtificialStackFrame<*>? =
- (this as? CoroutineStackFrame)?.owner()
+ private tailrec fun CoroutineStackFrame.realCaller(): CoroutineStackFrame? {
+ val caller = callerFrame ?: return null
+ return if (caller.getStackTraceElement() != null) caller else caller.realCaller()
+ }
- private tailrec fun CoroutineStackFrame.owner(): ArtificialStackFrame<*>? =
- if (this is ArtificialStackFrame<*>) this else callerFrame?.owner()
+ @Synchronized
+ private fun updateState(owner: CoroutineOwner<*>, frame: Continuation<*>, state: State) {
+ if (!isInstalled) return
+ owner.info.updateState(state, frame)
+ }
+
+ private fun Continuation<*>.owner(): CoroutineOwner<*>? = (this as? CoroutineStackFrame)?.owner()
+
+ private tailrec fun CoroutineStackFrame.owner(): CoroutineOwner<*>? =
+ if (this is CoroutineOwner<*>) this else callerFrame?.owner()
internal fun <T> probeCoroutineCreated(completion: Continuation<T>): Continuation<T> {
if (!isInstalled) return completion
@@ -249,7 +320,7 @@
* Here we replace completion with a sequence of CoroutineStackFrame objects
* which represents creation stacktrace, thus making stacktrace recovery mechanism
* even more verbose (it will attach coroutine creation stacktrace to all exceptions),
- * and then using this artificial frame as an identifier of coroutineSuspended/resumed calls.
+ * and then using CoroutineOwner completion as unique identifier of coroutineSuspended/resumed calls.
*/
val stacktrace = sanitizeStackTrace(Exception())
val frame = stacktrace.foldRight<StackTraceElement, CoroutineStackFrame?>(null) { frame, acc ->
@@ -257,24 +328,38 @@
override val callerFrame: CoroutineStackFrame? = acc
override fun getStackTraceElement(): StackTraceElement = frame
}
- }
- return ArtificialStackFrame(completion, frame!!).also {
- storeFrame(it, completion)
- }
+ }!!
+
+ return createOwner(completion, frame)
}
@Synchronized
- private fun <T> storeFrame(frame: ArtificialStackFrame<T>, completion: Continuation<T>) {
- capturedCoroutines[frame] = CoroutineState(completion, frame, ++sequenceNumber)
+ private fun <T> createOwner(completion: Continuation<T>, frame: CoroutineStackFrame): Continuation<T> {
+ if (!isInstalled) return completion
+ val info = CoroutineInfo(completion.context, frame, ++sequenceNumber)
+ val owner = CoroutineOwner(completion, info, frame)
+ capturedCoroutines += owner
+ return owner
}
@Synchronized
- private fun probeCoroutineCompleted(coroutine: ArtificialStackFrame<*>) {
- capturedCoroutines.remove(coroutine)
+ private fun probeCoroutineCompleted(owner: CoroutineOwner<*>) {
+ capturedCoroutines.remove(owner)
+ /*
+ * This removal is a guard against improperly implemented CoroutineStackFrame
+ * and bugs in the compiler.
+ */
+ val caller = owner.info.lastObservedFrame?.realCaller()
+ callerInfoCache.remove(caller)
}
- private class ArtificialStackFrame<T>(
+ /**
+ * This class is injected as completion of all continuations in [probeCoroutineCompleted].
+ * It is owning the coroutine info and responsible for managing all its external info related to debug agent.
+ */
+ private class CoroutineOwner<T>(
@JvmField val delegate: Continuation<T>,
+ @JvmField val info: CoroutineInfo,
frame: CoroutineStackFrame
) : Continuation<T> by delegate, CoroutineStackFrame by frame {
override fun resumeWith(result: Result<T>) {
@@ -292,7 +377,7 @@
if (!DebugProbes.sanitizeStackTraces) {
return List(size - probeIndex) {
- if (it == 0) artificialFrame(ARTIFICIAL_FRAME_MESSAGE) else stackTrace[it + probeIndex]
+ if (it == 0) createArtificialFrame(ARTIFICIAL_FRAME_MESSAGE) else stackTrace[it + probeIndex]
}
}
@@ -302,7 +387,7 @@
* output will be [e, i1, i3, e, i4, e, i5, i7]
*/
val result = ArrayList<StackTraceElement>(size - probeIndex + 1)
- result += artificialFrame(ARTIFICIAL_FRAME_MESSAGE)
+ result += createArtificialFrame(ARTIFICIAL_FRAME_MESSAGE)
var includeInternalFrame = true
for (i in (probeIndex + 1) until size - 1) {
val element = stackTrace[i]
diff --git a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt
index fd74fa9..a00a17d 100644
--- a/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt
+++ b/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt
@@ -73,8 +73,8 @@
private fun cancelIfNecessary() {
if (cancelOnTimeout) {
- DebugProbes.dumpCoroutinesState().forEach {
- it.jobOrNull?.cancel()
+ DebugProbes.dumpCoroutinesInfo().forEach {
+ it.job?.cancel()
}
}
}
diff --git a/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt b/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt
index dd4dc79..35e9d1e 100644
--- a/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt
+++ b/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt
@@ -5,32 +5,13 @@
package kotlinx.coroutines.debug
import kotlinx.coroutines.*
-import org.junit.*
import org.junit.Test
import kotlin.coroutines.*
import kotlin.test.*
-@Suppress("SUSPENSION_POINT_INSIDE_MONITOR") // bug in 1.3.0 FE
-class CoroutinesDumpTest : TestBase() {
-
+class CoroutinesDumpTest : DebugTestBase() {
private val monitor = Any()
- @Before
- fun setUp() {
- before()
- DebugProbes.sanitizeStackTraces = false
- DebugProbes.install()
- }
-
- @After
- fun tearDown() {
- try {
- DebugProbes.uninstall()
- } finally {
- onCompletion()
- }
- }
-
@Test
fun testSuspendedCoroutine() = synchronized(monitor) {
val deferred = GlobalScope.async {
@@ -49,8 +30,8 @@
"\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" +
"\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n")
- val found = DebugProbes.dumpCoroutinesState().single { it.jobOrNull === deferred }
- assertSame(deferred, found.jobOrNull)
+ val found = DebugProbes.dumpCoroutinesInfo().single { it.job === deferred }
+ assertSame(deferred, found.job)
runBlocking { deferred.cancelAndJoin() }
}
@@ -85,8 +66,10 @@
}
awaitCoroutineStarted()
+ Thread.sleep(10)
verifyDump(
- "Coroutine \"coroutine#1\":DeferredCoroutine{Active}@1e4a7dd4, state: RUNNING (Last suspension stacktrace, not an actual stacktrace)\n" +
+ "Coroutine \"coroutine#1\":DeferredCoroutine{Active}@1e4a7dd4, state: RUNNING\n" +
+ "\tat java.lang.Thread.sleep(Native Method)\n" +
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.nestedActiveMethod(CoroutinesDumpTest.kt:111)\n" +
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest.activeMethod(CoroutinesDumpTest.kt:106)\n" +
"\tat kotlinx.coroutines.debug.CoroutinesDumpTest\$testRunningCoroutineWithSuspensionPoint\$1\$deferred\$1.invokeSuspend(CoroutinesDumpTest.kt:71)\n" +
@@ -111,7 +94,7 @@
}
awaitCoroutineStarted()
- val coroutine = DebugProbes.dumpCoroutinesState().first()
+ val coroutine = DebugProbes.dumpCoroutinesInfo().first()
val result = coroutine.creationStackTrace.fold(StringBuilder()) { acc, element ->
acc.append(element.toString())
acc.append('\n')
diff --git a/kotlinx-coroutines-debug/test/DebugTestBase.kt b/kotlinx-coroutines-debug/test/DebugTestBase.kt
new file mode 100644
index 0000000..7ce2149
--- /dev/null
+++ b/kotlinx-coroutines-debug/test/DebugTestBase.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+package kotlinx.coroutines.debug
+
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.debug.junit4.*
+import org.junit.*
+
+open class DebugTestBase : TestBase() {
+
+ @JvmField
+ @Rule
+ val timeout = CoroutinesTimeout.seconds(10)
+
+ @Before
+ open fun setUp() {
+ before()
+ DebugProbes.sanitizeStackTraces = false
+ DebugProbes.install()
+ }
+
+ @After
+ fun tearDown() {
+ try {
+ DebugProbes.uninstall()
+ } finally {
+ onCompletion()
+ }
+ }
+}
\ No newline at end of file
diff --git a/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt b/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt
index b5e53bf..7208294 100644
--- a/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt
+++ b/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt
@@ -4,36 +4,19 @@
package kotlinx.coroutines.debug
import kotlinx.coroutines.*
-import org.junit.*
import org.junit.Test
import java.util.concurrent.*
import kotlin.test.*
-class RunningThreadStackMergeTest : TestBase() {
+class RunningThreadStackMergeTest : DebugTestBase() {
private val testMainBlocker = CountDownLatch(1) // Test body blocks on it
private val coroutineBlocker = CyclicBarrier(2) // Launched coroutine blocks on it
- @Before
- fun setUp() {
- before()
- DebugProbes.install()
- }
-
- @After
- fun tearDown() {
- try {
- DebugProbes.uninstall()
- } finally {
- onCompletion()
- }
- }
-
@Test
fun testStackMergeWithContext() = runTest {
launchCoroutine()
awaitCoroutineStarted()
-
verifyDump(
"Coroutine \"coroutine#1\":BlockingCoroutine{Active}@62230679", // <- this one is ignored
"Coroutine \"coroutine#2\":StandaloneCoroutine{Active}@50284dc4, state: RUNNING\n" +
@@ -49,7 +32,7 @@
"\tat kotlinx.coroutines.debug.RunningThreadStackMergeTest\$launchCoroutine\$1.invokeSuspend(RunningThreadStackMergeTest.kt:68)\n" +
"\t(Coroutine creation stacktrace)\n" +
"\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)",
- ignoredCoroutine = "BlockingCoroutine"
+ ignoredCoroutine = ":BlockingCoroutine"
)
coroutineBlocker.await()
}
@@ -102,26 +85,11 @@
"\tat kotlinx.coroutines.debug.RunningThreadStackMergeTest\$launchEscapingCoroutine\$1.invokeSuspend(RunningThreadStackMergeTest.kt:116)\n" +
"\t(Coroutine creation stacktrace)\n" +
"\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)",
- ignoredCoroutine = "BlockingCoroutine"
+ ignoredCoroutine = ":BlockingCoroutine"
)
coroutineBlocker.await()
}
- @Test
- fun testRunBlocking() = runBlocking {
- verifyDump("Coroutine \"coroutine#1\":BlockingCoroutine{Active}@4bcd176c, state: RUNNING\n" +
- "\tat java.lang.Thread.getStackTrace(Thread.java:1552)\n" +
- "\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.enhanceStackTraceWithThreadDump(DebugProbesImpl.kt:147)\n" +
- "\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.dumpCoroutines(DebugProbesImpl.kt:122)\n" +
- "\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.dumpCoroutines(DebugProbesImpl.kt:109)\n" +
- "\tat kotlinx.coroutines.debug.DebugProbes.dumpCoroutines(DebugProbes.kt:122)\n" +
- "\tat kotlinx.coroutines.debug.StracktraceUtilsKt.verifyDump(StracktraceUtils.kt)\n" +
- "\tat kotlinx.coroutines.debug.StracktraceUtilsKt.verifyDump\$default(StracktraceUtils.kt)\n" +
- "\tat kotlinx.coroutines.debug.RunningThreadStackMergeTest\$testRunBlocking\$1.invokeSuspend(RunningThreadStackMergeTest.kt:112)\n" +
- "\t(Coroutine creation stacktrace)\n" +
- "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n")
- }
-
private fun CoroutineScope.launchEscapingCoroutine() {
launch(Dispatchers.Default) {
suspendingFunctionWithContext()
@@ -138,6 +106,57 @@
assertTrue(true)
}
+ @Test
+ fun testMergeThroughInvokeSuspend() = runTest {
+ launchEscapingCoroutineWithoutContext()
+ awaitCoroutineStarted()
+ verifyDump(
+ "Coroutine \"coroutine#1\":BlockingCoroutine{Active}@62230679", // <- this one is ignored
+ "Coroutine \"coroutine#2\":StandaloneCoroutine{Active}@3aea3c67, state: RUNNING\n" +
+ "\tat sun.misc.Unsafe.park(Native Method)\n" +
+ "\tat java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)\n" +
+ "\tat java.util.concurrent.locks.AbstractQueuedSynchronizer\$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)\n" +
+ "\tat java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:234)\n" +
+ "\tat java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)\n" +
+ "\tat kotlinx.coroutines.debug.RunningThreadStackMergeTest.nonSuspendingFun(RunningThreadStackMergeTest.kt:83)\n" +
+ "\tat kotlinx.coroutines.debug.RunningThreadStackMergeTest.suspendingFunctionWithoutContext(RunningThreadStackMergeTest.kt:160)\n" +
+ "\tat kotlinx.coroutines.debug.RunningThreadStackMergeTest\$launchEscapingCoroutineWithoutContext\$1.invokeSuspend(RunningThreadStackMergeTest.kt:153)\n" +
+ "\t(Coroutine creation stacktrace)\n" +
+ "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)",
+ ignoredCoroutine = ":BlockingCoroutine"
+ )
+ coroutineBlocker.await()
+ }
+
+ private fun CoroutineScope.launchEscapingCoroutineWithoutContext() {
+ launch(Dispatchers.Default) {
+ suspendingFunctionWithoutContext()
+ assertTrue(true)
+ }
+ }
+
+ private suspend fun suspendingFunctionWithoutContext() {
+ actualSuspensionPoint()
+ nonSuspendingFun()
+ assertTrue(true)
+ }
+
+ @Test
+ fun testRunBlocking() = runBlocking {
+ verifyDump("Coroutine \"coroutine#1\":BlockingCoroutine{Active}@4bcd176c, state: RUNNING\n" +
+ "\tat java.lang.Thread.getStackTrace(Thread.java:1552)\n" +
+ "\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.enhanceStackTraceWithThreadDump(DebugProbesImpl.kt:147)\n" +
+ "\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.dumpCoroutines(DebugProbesImpl.kt:122)\n" +
+ "\tat kotlinx.coroutines.debug.internal.DebugProbesImpl.dumpCoroutines(DebugProbesImpl.kt:109)\n" +
+ "\tat kotlinx.coroutines.debug.DebugProbes.dumpCoroutines(DebugProbes.kt:122)\n" +
+ "\tat kotlinx.coroutines.debug.StracktraceUtilsKt.verifyDump(StracktraceUtils.kt)\n" +
+ "\tat kotlinx.coroutines.debug.StracktraceUtilsKt.verifyDump\$default(StracktraceUtils.kt)\n" +
+ "\tat kotlinx.coroutines.debug.RunningThreadStackMergeTest\$testRunBlocking\$1.invokeSuspend(RunningThreadStackMergeTest.kt:112)\n" +
+ "\t(Coroutine creation stacktrace)\n" +
+ "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n")
+ }
+
+
private suspend fun actualSuspensionPoint() {
nestedSuspensionPoint()
assertTrue(true)
diff --git a/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt b/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt
index 4d35d02..3ee80ad 100644
--- a/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt
+++ b/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt
@@ -13,21 +13,11 @@
import java.util.concurrent.*
import kotlin.test.*
-class SanitizedProbesTest : TestBase() {
+class SanitizedProbesTest : DebugTestBase() {
@Before
- fun setUp() {
- before()
+ override fun setUp() {
+ super.setUp()
DebugProbes.sanitizeStackTraces = true
- DebugProbes.install()
- }
-
- @After
- fun tearDown() {
- try {
- DebugProbes.uninstall()
- } finally {
- onCompletion()
- }
}
@Test
diff --git a/kotlinx-coroutines-debug/test/ScopedBuildersTest.kt b/kotlinx-coroutines-debug/test/ScopedBuildersTest.kt
index 25b4633..c762725 100644
--- a/kotlinx-coroutines-debug/test/ScopedBuildersTest.kt
+++ b/kotlinx-coroutines-debug/test/ScopedBuildersTest.kt
@@ -8,22 +8,7 @@
import org.junit.*
import kotlin.coroutines.*
-class ScopedBuildersTest : TestBase() {
- @Before
- fun setUp() {
- before()
- DebugProbes.sanitizeStackTraces = false
- DebugProbes.install()
- }
-
- @After
- fun tearDown() {
- try {
- DebugProbes.uninstall()
- } finally {
- onCompletion()
- }
- }
+class ScopedBuildersTest : DebugTestBase() {
@Test
fun testNestedScopes() = runBlocking {
diff --git a/kotlinx-coroutines-debug/test/StartModeProbesTest.kt b/kotlinx-coroutines-debug/test/StartModeProbesTest.kt
index bd33c5c..a0297da 100644
--- a/kotlinx-coroutines-debug/test/StartModeProbesTest.kt
+++ b/kotlinx-coroutines-debug/test/StartModeProbesTest.kt
@@ -9,23 +9,7 @@
import org.junit.Test
import kotlin.test.*
-class StartModeProbesTest : TestBase() {
-
- @Before
- fun setUp() {
- before()
- DebugProbes.sanitizeStackTraces = false
- DebugProbes.install()
- }
-
- @After
- fun tearDown() {
- try {
- DebugProbes.uninstall()
- } finally {
- onCompletion()
- }
- }
+class StartModeProbesTest : DebugTestBase() {
@Test
fun testUndispatched() = runTest {
diff --git a/kotlinx-coroutines-debug/test/StracktraceUtils.kt b/kotlinx-coroutines-debug/test/StracktraceUtils.kt
index 62dcd78..cab4ed8 100644
--- a/kotlinx-coroutines-debug/test/StracktraceUtils.kt
+++ b/kotlinx-coroutines-debug/test/StracktraceUtils.kt
@@ -97,7 +97,7 @@
trace.any { tr -> tr.contains(frame) }
}
- assertEquals(createdCoroutinesCount, DebugProbes.dumpCoroutinesState().size)
+ assertEquals(createdCoroutinesCount, DebugProbes.dumpCoroutinesInfo().size)
assertTrue(matches)
}