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