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