• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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         // Used by the IDEA debugger via reflection and must be kept binary-compatible, see KTIJ-24102
34         get() = _context.get()
35 
36     // Used by the IDEA debugger via reflection and must be kept binary-compatible, see KTIJ-24102
37     public val creationStackTrace: List<StackTraceElement> get() = creationStackTrace()
38 
39     /**
40      * Last observed state of the coroutine.
41      * Can be CREATED, RUNNING, SUSPENDED.
42      */
43     internal val state: String get() = _state
44 
45     // Used by the IDEA debugger via reflection and must be kept binary-compatible, see KTIJ-24102
46     @Volatile
47     @JvmField
48     public var _state: String = CREATED
49 
50     /*
51      * How many consecutive unmatched 'updateState(RESUMED)' this object has received.
52      * It can be `> 1` in two cases:
53      *
54      * - The coroutine is finishing and its state is being unrolled in BaseContinuationImpl, see comment to DebugProbesImpl#callerInfoCache
55      *   Such resumes are not expected to be matched and are ignored.
56      * - We encountered suspend-resume race explained above, and we do wait for a match.
57      */
58     private var unmatchedResume = 0
59 
60     /**
61      * Here we orchestrate overlapping state updates that are coming asynchronously.
62      * In a nutshell, `probeCoroutineSuspended` can arrive **later** than its matching `probeCoroutineResumed`,
63      * e.g. for the following code:
64      * ```
65      * suspend fun foo() = yield()
66      * ```
67      *
68      * we have this sequence:
69      * ```
70      * fun foo(...) {
71      *     uCont.intercepted().dispatchUsingDispatcher() // 1
72      *     // Notify the debugger the coroutine is suspended
73      *     probeCoroutineSuspended() // 2
74      *     return COROUTINE_SUSPENDED // Unroll the stack
75      * }
76      * ```
77      * Nothing prevents coroutine to be dispatched and invoke `probeCoroutineResumed` right between '1' and '2'.
78      * See also: https://github.com/Kotlin/kotlinx.coroutines/issues/3193
79      *
80      * [shouldBeMatched] -- `false` if it is an expected consecutive `probeCoroutineResumed` from BaseContinuationImpl,
81      * `true` otherwise.
82      */
83     @Synchronized
updateStatenull84     internal fun updateState(state: String, frame: Continuation<*>, shouldBeMatched: Boolean) {
85         /**
86          * We observe consecutive resume that had to be matched, but it wasn't,
87          * increment
88          */
89         if (_state == RUNNING && state == RUNNING && shouldBeMatched) {
90             ++unmatchedResume
91         } else if (unmatchedResume > 0 && state == SUSPENDED) {
92             /*
93              * We received late 'suspend' probe for unmatched resume, skip it.
94              * Here we deliberately allow the very unlikely race;
95              * Consider the following scenario ('[r:a]' means "probeCoroutineResumed at a()"):
96              * ```
97              * [r:a] a() -> b() [s:b] [r:b] -> (back to a) a() -> c() [s:c]
98              * ```
99              * We can, in theory, observe the following probes interleaving:
100              * ```
101              * r:a
102              * r:b // Unmatched resume
103              * s:c // Matched suspend, discard
104              * s:b
105              * ```
106              * Thus mis-attributing 'lastObservedFrame' to a previously-observed.
107              * It is possible in theory (though I've failed to reproduce it), yet
108              * is more preferred than indefinitely mismatched state (-> mismatched real/enhanced stacktrace)
109              */
110             --unmatchedResume
111             return
112         }
113 
114         // Propagate only non-duplicating transitions to running, see KT-29997
115         if (_state == state && state == SUSPENDED && lastObservedFrame != null) return
116 
117         _state = state
118         lastObservedFrame = frame as? CoroutineStackFrame
119         lastObservedThread = if (state == RUNNING) {
120             Thread.currentThread()
121         } else {
122             null
123         }
124     }
125 
126     // Used by the IDEA debugger via reflection and must be kept binary-compatible, see KTIJ-24102
127     @JvmField
128     @Volatile
129     public var lastObservedThread: Thread? = null
130 
131     /**
132      * We cannot keep a strong reference to the last observed frame of the coroutine, because this will
133      * prevent garbage-collection of a coroutine that was lost.
134      *
135      * Used by the IDEA debugger via reflection and must be kept binary-compatible, see KTIJ-24102
136      */
137     @Volatile
138     @JvmField
139     public var _lastObservedFrame: WeakReference<CoroutineStackFrame>? = null
140     internal var lastObservedFrame: CoroutineStackFrame?
141         get() = _lastObservedFrame?.get()
142         set(value) {
<lambda>null143             _lastObservedFrame = value?.let { WeakReference(it) }
144         }
145 
146     /**
147      * Last observed stacktrace of the coroutine captured on its suspension or resumption point.
148      * It means that for [running][State.RUNNING] coroutines resulting stacktrace is inaccurate and
149      * reflects stacktrace of the resumption point, not the actual current stacktrace.
150      */
lastObservedStackTracenull151     internal fun lastObservedStackTrace(): List<StackTraceElement> {
152         var frame: CoroutineStackFrame? = lastObservedFrame ?: return emptyList()
153         val result = ArrayList<StackTraceElement>()
154         while (frame != null) {
155             frame.getStackTraceElement()?.let { result.add(it) }
156             frame = frame.callerFrame
157         }
158         return result
159     }
160 
creationStackTracenull161     private fun creationStackTrace(): List<StackTraceElement> {
162         val bottom = creationStackBottom ?: return emptyList()
163         // Skip "Coroutine creation stacktrace" frame
164         return sequence { yieldFrames(bottom.callerFrame) }.toList()
165     }
166 
yieldFramesnull167     private tailrec suspend fun SequenceScope<StackTraceElement>.yieldFrames(frame: CoroutineStackFrame?) {
168         if (frame == null) return
169         frame.getStackTraceElement()?.let { yield(it) }
170         val caller = frame.callerFrame
171         if (caller != null) {
172             yieldFrames(caller)
173         }
174     }
175 
toStringnull176     override fun toString(): String = "DebugCoroutineInfo(state=$state,context=$context)"
177 }
178