All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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