1 package kotlinx.coroutines.debug.internal
2 
3 import java.lang.ref.*
4 import kotlin.coroutines.*
5 import kotlin.coroutines.jvm.internal.*
6 
7 internal const val CREATED = "CREATED"
8 internal const val RUNNING = "RUNNING"
9 internal const val SUSPENDED = "SUSPENDED"
10 
11 /**
12  * Internal implementation class where debugger tracks details it knows about each coroutine.
13  * Its mutable fields can be updated concurrently, thus marked with `@Volatile`
14  */
15 @PublishedApi
16 internal class DebugCoroutineInfoImpl internal constructor(
17     context: CoroutineContext?,
18     /**
19      * A reference to a stack-trace that is converted to a [StackTraceFrame] which implements [CoroutineStackFrame].
20      * The actual reference to the coroutine is not stored here, so we keep a strong reference.
21      */
22     internal val creationStackBottom: StackTraceFrame?,
23     // Used by the IDEA debugger via reflection and must be kept binary-compatible, see KTIJ-24102
24     @JvmField public val sequenceNumber: Long
25 ) {
26     /**
27      * We cannot keep a strong reference to the context, because with the [Job] in the context it will indirectly
28      * keep a reference to the last frame of an abandoned coroutine which the debugger should not be preventing
29      * garbage-collection of. The reference to context will not disappear as long as the coroutine itself is not lost.
30      */
31     private val _context = WeakReference(context)
32     public val context: CoroutineContext? // can be null when the coroutine was already garbage-collected
33         get() = _context.get()
34 
35     public val creationStackTrace: List<StackTraceElement> get() = creationStackTrace()
36 
37     /**
38      * Last observed state of the coroutine.
39      * Can be CREATED, RUNNING, SUSPENDED.
40      */
41     internal val state: String get() = _state
42 
43     // Used by the IDEA debugger via reflection and must be kept binary-compatible, see KTIJ-24102
44     @Volatile
45     @JvmField
46     public var _state: String = CREATED
47 
48     /*
49      * How many consecutive unmatched 'updateState(RESUMED)' this object has received.
50      * It can be `> 1` in two cases:
51      *
52      * - The coroutine is finishing and its state is being unrolled in BaseContinuationImpl, see comment to DebugProbesImpl#callerInfoCache
53      *   Such resumes are not expected to be matched and are ignored.
54      * - We encountered suspend-resume race explained above, and we do wait for a match.
55      */
56     private var unmatchedResume = 0
57 
58     /**
59      * Here we orchestrate overlapping state updates that are coming asynchronously.
60      * In a nutshell, `probeCoroutineSuspended` can arrive **later** than its matching `probeCoroutineResumed`,
61      * e.g. for the following code:
62      * ```
63      * suspend fun foo() = yield()
64      * ```
65      *
66      * we have this sequence:
67      * ```
68      * fun foo(...) {
69      *     uCont.intercepted().dispatchUsingDispatcher() // 1
70      *     // Notify the debugger the coroutine is suspended
71      *     probeCoroutineSuspended() // 2
72      *     return COROUTINE_SUSPENDED // Unroll the stack
73      * }
74      * ```
75      * Nothing prevents coroutine to be dispatched and invoke `probeCoroutineResumed` right between '1' and '2'.
76      * See also: https://github.com/Kotlin/kotlinx.coroutines/issues/3193
77      *
78      * [shouldBeMatched] -- `false` if it is an expected consecutive `probeCoroutineResumed` from BaseContinuationImpl,
79      * `true` otherwise.
80      */
81     @Synchronized
updateStatenull82     internal fun updateState(state: String, frame: Continuation<*>, shouldBeMatched: Boolean) {
83         /**
84          * We observe consecutive resume that had to be matched, but it wasn't,
85          * increment
86          */
87         if (_state == RUNNING && state == RUNNING && shouldBeMatched) {
88             ++unmatchedResume
89         } else if (unmatchedResume > 0 && state == SUSPENDED) {
90             /*
91              * We received late 'suspend' probe for unmatched resume, skip it.
92              * Here we deliberately allow the very unlikely race;
93              * Consider the following scenario ('[r:a]' means "probeCoroutineResumed at a()"):
94              * ```
95              * [r:a] a() -> b() [s:b] [r:b] -> (back to a) a() -> c() [s:c]
96              * ```
97              * We can, in theory, observe the following probes interleaving:
98              * ```
99              * r:a
100              * r:b // Unmatched resume
101              * s:c // Matched suspend, discard
102              * s:b
103              * ```
104              * Thus mis-attributing 'lastObservedFrame' to a previously-observed.
105              * It is possible in theory (though I've failed to reproduce it), yet
106              * is more preferred than indefinitely mismatched state (-> mismatched real/enhanced stacktrace)
107              */
108             --unmatchedResume
109             return
110         }
111 
112         // Propagate only non-duplicating transitions to running, see KT-29997
113         if (_state == state && state == SUSPENDED && lastObservedFrame != null) return
114 
115         _state = state
116         lastObservedFrame = frame as? CoroutineStackFrame
117         lastObservedThread = if (state == RUNNING) {
118             Thread.currentThread()
119         } else {
120             null
121         }
122     }
123 
124     // Used by the IDEA debugger via reflection and must be kept binary-compatible, see KTIJ-24102
125     @JvmField
126     @Volatile
127     public var lastObservedThread: Thread? = null
128 
129     /**
130      * We cannot keep a strong reference to the last observed frame of the coroutine, because this will
131      * prevent garbage-collection of a coroutine that was lost.
132      *
133      * Used by the IDEA debugger via reflection and must be kept binary-compatible, see KTIJ-24102
134      */
135     @Volatile
136     @JvmField
137     public var _lastObservedFrame: WeakReference<CoroutineStackFrame>? = null
138     internal var lastObservedFrame: CoroutineStackFrame?
139         get() = _lastObservedFrame?.get()
140         set(value) {
<lambda>null141             _lastObservedFrame = value?.let { WeakReference(it) }
142         }
143 
144     /**
145      * Last observed stacktrace of the coroutine captured on its suspension or resumption point.
146      * It means that for [running][State.RUNNING] coroutines resulting stacktrace is inaccurate and
147      * reflects stacktrace of the resumption point, not the actual current stacktrace.
148      */
lastObservedStackTracenull149     internal fun lastObservedStackTrace(): List<StackTraceElement> {
150         var frame: CoroutineStackFrame? = lastObservedFrame ?: return emptyList()
151         val result = ArrayList<StackTraceElement>()
152         while (frame != null) {
153             frame.getStackTraceElement()?.let { result.add(it) }
154             frame = frame.callerFrame
155         }
156         return result
157     }
158 
creationStackTracenull159     private fun creationStackTrace(): List<StackTraceElement> {
160         val bottom = creationStackBottom ?: return emptyList()
161         // Skip "Coroutine creation stacktrace" frame
162         return sequence { yieldFrames(bottom.callerFrame) }.toList()
163     }
164 
yieldFramesnull165     private tailrec suspend fun SequenceScope<StackTraceElement>.yieldFrames(frame: CoroutineStackFrame?) {
166         if (frame == null) return
167         frame.getStackTraceElement()?.let { yield(it) }
168         val caller = frame.callerFrame
169         if (caller != null) {
170             yieldFrames(caller)
171         }
172     }
173 
toStringnull174     override fun toString(): String = "DebugCoroutineInfo(state=$state,context=$context)"
175 }
176