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