
jvmMain.debug.internal.DebugCoroutineInfoImpl.kt Maven / Gradle / Ivy
package kotlinx.coroutines.debug.internal
import java.lang.ref.*
import kotlin.coroutines.*
import kotlin.coroutines.jvm.internal.*
internal const val CREATED = "CREATED"
internal const val RUNNING = "RUNNING"
internal const val SUSPENDED = "SUSPENDED"
/**
* Internal implementation class where debugger tracks details it knows about each coroutine.
* Its mutable fields can be updated concurrently, thus marked with `@Volatile`
*/
@PublishedApi
internal class DebugCoroutineInfoImpl internal constructor(
context: CoroutineContext?,
/**
* A reference to a stack-trace that is converted to a [StackTraceFrame] which implements [CoroutineStackFrame].
* The actual reference to the coroutine is not stored here, so we keep a strong reference.
*/
internal val creationStackBottom: StackTraceFrame?,
// Used by the IDEA debugger via reflection and must be kept binary-compatible, see KTIJ-24102
@JvmField public val sequenceNumber: Long
) {
/**
* We cannot keep a strong reference to the context, because with the [Job] in the context it will indirectly
* keep a reference to the last frame of an abandoned coroutine which the debugger should not be preventing
* garbage-collection of. The reference to context will not disappear as long as the coroutine itself is not lost.
*/
private val _context = WeakReference(context)
public val context: CoroutineContext? // can be null when the coroutine was already garbage-collected
get() = _context.get()
public val creationStackTrace: List get() = creationStackTrace()
/**
* Last observed state of the coroutine.
* Can be CREATED, RUNNING, SUSPENDED.
*/
internal val state: String get() = _state
// Used by the IDEA debugger via reflection and must be kept binary-compatible, see KTIJ-24102
@Volatile
@JvmField
public var _state: String = CREATED
/*
* How many consecutive unmatched 'updateState(RESUMED)' this object has received.
* It can be `> 1` in two cases:
*
* - The coroutine is finishing and its state is being unrolled in BaseContinuationImpl, see comment to DebugProbesImpl#callerInfoCache
* Such resumes are not expected to be matched and are ignored.
* - We encountered suspend-resume race explained above, and we do wait for a match.
*/
private var unmatchedResume = 0
/**
* Here we orchestrate overlapping state updates that are coming asynchronously.
* In a nutshell, `probeCoroutineSuspended` can arrive **later** than its matching `probeCoroutineResumed`,
* e.g. for the following code:
* ```
* suspend fun foo() = yield()
* ```
*
* we have this sequence:
* ```
* fun foo(...) {
* uCont.intercepted().dispatchUsingDispatcher() // 1
* // Notify the debugger the coroutine is suspended
* probeCoroutineSuspended() // 2
* return COROUTINE_SUSPENDED // Unroll the stack
* }
* ```
* Nothing prevents coroutine to be dispatched and invoke `probeCoroutineResumed` right between '1' and '2'.
* See also: https://github.com/Kotlin/kotlinx.coroutines/issues/3193
*
* [shouldBeMatched] -- `false` if it is an expected consecutive `probeCoroutineResumed` from BaseContinuationImpl,
* `true` otherwise.
*/
@Synchronized
internal fun updateState(state: String, frame: Continuation<*>, shouldBeMatched: Boolean) {
/**
* We observe consecutive resume that had to be matched, but it wasn't,
* increment
*/
if (_state == RUNNING && state == RUNNING && shouldBeMatched) {
++unmatchedResume
} else if (unmatchedResume > 0 && state == SUSPENDED) {
/*
* We received late 'suspend' probe for unmatched resume, skip it.
* Here we deliberately allow the very unlikely race;
* Consider the following scenario ('[r:a]' means "probeCoroutineResumed at a()"):
* ```
* [r:a] a() -> b() [s:b] [r:b] -> (back to a) a() -> c() [s:c]
* ```
* We can, in theory, observe the following probes interleaving:
* ```
* r:a
* r:b // Unmatched resume
* s:c // Matched suspend, discard
* s:b
* ```
* Thus mis-attributing 'lastObservedFrame' to a previously-observed.
* It is possible in theory (though I've failed to reproduce it), yet
* is more preferred than indefinitely mismatched state (-> mismatched real/enhanced stacktrace)
*/
--unmatchedResume
return
}
// Propagate only non-duplicating transitions to running, see KT-29997
if (_state == state && state == SUSPENDED && lastObservedFrame != null) return
_state = state
lastObservedFrame = frame as? CoroutineStackFrame
lastObservedThread = if (state == RUNNING) {
Thread.currentThread()
} else {
null
}
}
// Used by the IDEA debugger via reflection and must be kept binary-compatible, see KTIJ-24102
@JvmField
@Volatile
public var lastObservedThread: Thread? = null
/**
* We cannot keep a strong reference to the last observed frame of the coroutine, because this will
* prevent garbage-collection of a coroutine that was lost.
*
* Used by the IDEA debugger via reflection and must be kept binary-compatible, see KTIJ-24102
*/
@Volatile
@JvmField
public var _lastObservedFrame: WeakReference? = null
internal var lastObservedFrame: CoroutineStackFrame?
get() = _lastObservedFrame?.get()
set(value) {
_lastObservedFrame = value?.let { WeakReference(it) }
}
/**
* 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.
*/
internal fun lastObservedStackTrace(): List {
var frame: CoroutineStackFrame? = lastObservedFrame ?: return emptyList()
val result = ArrayList()
while (frame != null) {
frame.getStackTraceElement()?.let { result.add(it) }
frame = frame.callerFrame
}
return result
}
private fun creationStackTrace(): List {
val bottom = creationStackBottom ?: return emptyList()
// Skip "Coroutine creation stacktrace" frame
return sequence { yieldFrames(bottom.callerFrame) }.toList()
}
private tailrec suspend fun SequenceScope.yieldFrames(frame: CoroutineStackFrame?) {
if (frame == null) return
frame.getStackTraceElement()?.let { yield(it) }
val caller = frame.callerFrame
if (caller != null) {
yieldFrames(caller)
}
}
override fun toString(): String = "DebugCoroutineInfo(state=$state,context=$context)"
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy