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