blob: d358d49d1e1f085342dc0c71baabb89072c40a40 [file] [log] [blame]
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +03001/*
Vsevolod Tolstopyatov41a2e302021-02-04 07:16:48 -08002 * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +03003 */
4
5package kotlinx.coroutines.debug.internal
6
Vsevolod Tolstopyatov4116fbf2020-03-13 14:13:09 +03007import kotlinx.atomicfu.*
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +03008import kotlinx.coroutines.*
dkhalanskyjbc3d711b2021-05-13 14:44:27 +03009import kotlinx.coroutines.internal.*
Roman Elizarovc05de882020-07-16 23:34:40 +030010import kotlinx.coroutines.internal.ScopeCoroutine
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +030011import java.io.*
Roman Elizarovc05de882020-07-16 23:34:40 +030012import java.lang.StackTraceElement
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +030013import java.text.*
Vsevolod Tolstopyatov4116fbf2020-03-13 14:13:09 +030014import java.util.concurrent.locks.*
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +030015import kotlin.collections.ArrayList
Vsevolod Tolstopyatov4116fbf2020-03-13 14:13:09 +030016import kotlin.concurrent.*
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +030017import kotlin.coroutines.*
Roman Elizarovc05de882020-07-16 23:34:40 +030018import kotlin.coroutines.jvm.internal.CoroutineStackFrame
19import kotlin.synchronized
dkhalanskyjbc3d711b2021-05-13 14:44:27 +030020import kotlinx.coroutines.internal.artificialFrame as createArtificialFrame // IDEA bug workaround
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +030021
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +030022internal object DebugProbesImpl {
dkhalanskyjbc3d711b2021-05-13 14:44:27 +030023 private const val ARTIFICIAL_FRAME_MESSAGE = "Coroutine creation stacktrace"
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +030024 private val dateFormat = SimpleDateFormat("yyyy/MM/dd HH:mm:ss")
Roman Elizarovc05de882020-07-16 23:34:40 +030025
26 private var weakRefCleanerThread: Thread? = null
27
28 // Values are boolean, so this map does not need to use a weak reference queue
29 private val capturedCoroutinesMap = ConcurrentWeakMap<CoroutineOwner<*>, Boolean>()
30 private val capturedCoroutines: Set<CoroutineOwner<*>> get() = capturedCoroutinesMap.keys
31
Vsevolod Tolstopyatov5a80d2a2018-12-10 20:19:35 +030032 @Volatile
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +030033 private var installations = 0
Roman Elizarovc05de882020-07-16 23:34:40 +030034
35 /**
36 * This internal method is used by IDEA debugger under the JVM name of
37 * "isInstalled$kotlinx_coroutines_debug".
38 */
Vsevolod Tolstopyatovaf9a2012019-12-11 19:04:47 +030039 internal val isInstalled: Boolean get() = installations > 0
Roman Elizarovc05de882020-07-16 23:34:40 +030040
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +030041 // To sort coroutines by creation order, used as unique id
Vsevolod Tolstopyatov4116fbf2020-03-13 14:13:09 +030042 private val sequenceNumber = atomic(0L)
43 /*
44 * RW-lock that guards all debug probes state changes.
45 * All individual coroutine state transitions are guarded by read-lock
46 * and do not interfere with each other.
47 * All state reads are guarded by the write lock to guarantee a strongly-consistent
48 * snapshot of the system.
49 */
50 private val coroutineStateLock = ReentrantReadWriteLock()
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +030051
Vsevolod Tolstopyatov70a74872020-04-24 14:59:28 +030052 public var sanitizeStackTraces: Boolean = true
53 public var enableCreationStackTraces: Boolean = true
54
55 /*
56 * Substitute for service loader, DI between core and debug modules.
57 * If the agent was installed via command line -javaagent parameter, do not use byte-byddy to avoud
58 */
59 private val dynamicAttach = getDynamicAttach()
60
61 @Suppress("UNCHECKED_CAST")
62 private fun getDynamicAttach(): Function1<Boolean, Unit>? = runCatching {
63 val clz = Class.forName("kotlinx.coroutines.debug.internal.ByteBuddyDynamicAttach")
64 val ctor = clz.constructors[0]
65 ctor.newInstance() as Function1<Boolean, Unit>
66 }.getOrNull()
67
Vsevolod Tolstopyatove35baa42019-03-05 18:26:09 +030068 /*
69 * This is an optimization in the face of KT-29997:
70 * Consider suspending call stack a()->b()->c() and c() completes its execution and every call is
71 * "almost" in tail position.
72 *
73 * Then at least three RUNNING -> RUNNING transitions will occur consecutively and complexity of each is O(depth).
74 * To avoid that quadratic complexity, we are caching lookup result for such chains in this map and update it incrementally.
Roman Elizarovc05de882020-07-16 23:34:40 +030075 *
76 * [DebugCoroutineInfoImpl] keeps a lot of auxiliary information about a coroutine, so we use a weak reference queue
77 * to promptly release the corresponding memory when the reference to the coroutine itself was already collected.
Vsevolod Tolstopyatove35baa42019-03-05 18:26:09 +030078 */
Roman Elizarovc05de882020-07-16 23:34:40 +030079 private val callerInfoCache = ConcurrentWeakMap<CoroutineStackFrame, DebugCoroutineInfoImpl>(weakRefQueue = true)
Vsevolod Tolstopyatove35baa42019-03-05 18:26:09 +030080
Vsevolod Tolstopyatov4116fbf2020-03-13 14:13:09 +030081 public fun install(): Unit = coroutineStateLock.write {
Roman Elizarov12695b72018-12-11 19:20:39 +030082 if (++installations > 1) return
Roman Elizarovc05de882020-07-16 23:34:40 +030083 startWeakRefCleanerThread()
Steve Elliottca095be2022-07-25 14:26:10 +000084 if (AgentInstallationType.isInstalledStatically) return
Vsevolod Tolstopyatov70a74872020-04-24 14:59:28 +030085 dynamicAttach?.invoke(true) // attach
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +030086 }
87
Vsevolod Tolstopyatov4116fbf2020-03-13 14:13:09 +030088 public fun uninstall(): Unit = coroutineStateLock.write {
Roman Elizarov12695b72018-12-11 19:20:39 +030089 check(isInstalled) { "Agent was not installed" }
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +030090 if (--installations != 0) return
Roman Elizarovc05de882020-07-16 23:34:40 +030091 stopWeakRefCleanerThread()
92 capturedCoroutinesMap.clear()
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +030093 callerInfoCache.clear()
Steve Elliottca095be2022-07-25 14:26:10 +000094 if (AgentInstallationType.isInstalledStatically) return
Vsevolod Tolstopyatov70a74872020-04-24 14:59:28 +030095 dynamicAttach?.invoke(false) // detach
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +030096 }
97
Roman Elizarovc05de882020-07-16 23:34:40 +030098 private fun startWeakRefCleanerThread() {
99 weakRefCleanerThread = thread(isDaemon = true, name = "Coroutines Debugger Cleaner") {
100 callerInfoCache.runWeakRefQueueCleaningLoopUntilInterrupted()
101 }
102 }
103
104 private fun stopWeakRefCleanerThread() {
Steve Elliottca095be2022-07-25 14:26:10 +0000105 val thread = weakRefCleanerThread ?: return
Roman Elizarovc05de882020-07-16 23:34:40 +0300106 weakRefCleanerThread = null
Steve Elliottca095be2022-07-25 14:26:10 +0000107 thread.interrupt()
108 thread.join()
Roman Elizarovc05de882020-07-16 23:34:40 +0300109 }
110
Vsevolod Tolstopyatov4116fbf2020-03-13 14:13:09 +0300111 public fun hierarchyToString(job: Job): String = coroutineStateLock.write {
Roman Elizarov12695b72018-12-11 19:20:39 +0300112 check(isInstalled) { "Debug probes are not installed" }
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300113 val jobToStack = capturedCoroutines
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300114 .filter { it.delegate.context[Job] != null }
Vsevolod Tolstopyatov9eaa9c62020-10-19 10:17:16 -0700115 .associateBy({ it.delegate.context.job }, { it.info })
Roman Elizarov12695b72018-12-11 19:20:39 +0300116 return buildString {
117 job.build(jobToStack, this, "")
118 }
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300119 }
120
Roman Elizarovc05de882020-07-16 23:34:40 +0300121 private fun Job.build(map: Map<Job, DebugCoroutineInfoImpl>, builder: StringBuilder, indent: String) {
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300122 val info = map[this]
Vsevolod Tolstopyatov8d506b32019-02-20 18:31:02 +0300123 val newIndent: String
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300124 if (info == null) { // Append coroutine without stacktrace
Vsevolod Tolstopyatov8d506b32019-02-20 18:31:02 +0300125 // Do not print scoped coroutines and do not increase indentation level
Vsevolod Tolstopyatov8e2428c2018-12-10 19:12:08 +0300126 @Suppress("INVISIBLE_REFERENCE")
Roman Elizarovc05de882020-07-16 23:34:40 +0300127 if (this !is ScopeCoroutine<*>) {
Vsevolod Tolstopyatov8d506b32019-02-20 18:31:02 +0300128 builder.append("$indent$debugString\n")
129 newIndent = indent + "\t"
130 } else {
131 newIndent = indent
Vsevolod Tolstopyatov8e2428c2018-12-10 19:12:08 +0300132 }
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300133 } else {
Vsevolod Tolstopyatov8d506b32019-02-20 18:31:02 +0300134 // Append coroutine with its last stacktrace element
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300135 val element = info.lastObservedStackTrace().firstOrNull()
136 val state = info.state
137 builder.append("$indent$debugString, continuation is $state at line $element\n")
Vsevolod Tolstopyatov8d506b32019-02-20 18:31:02 +0300138 newIndent = indent + "\t"
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300139 }
Vsevolod Tolstopyatov8d506b32019-02-20 18:31:02 +0300140 // Append children with new indent
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300141 for (child in children) {
Vsevolod Tolstopyatov8d506b32019-02-20 18:31:02 +0300142 child.build(map, builder, newIndent)
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300143 }
144 }
145
Vsevolod Tolstopyatov8d506b32019-02-20 18:31:02 +0300146 @Suppress("DEPRECATION_ERROR") // JobSupport
147 private val Job.debugString: String get() = if (this is JobSupport) toDebugString() else toString()
148
Roman Elizarovc05de882020-07-16 23:34:40 +0300149 /**
150 * Private method that dumps coroutines so that different public-facing method can use
151 * to produce different result types.
152 */
Steve Elliottca095be2022-07-25 14:26:10 +0000153 private inline fun <R : Any> dumpCoroutinesInfoImpl(crossinline create: (CoroutineOwner<*>, CoroutineContext) -> R): List<R> =
Roman Elizarovc05de882020-07-16 23:34:40 +0300154 coroutineStateLock.write {
155 check(isInstalled) { "Debug probes are not installed" }
156 capturedCoroutines
Steve Elliottca095be2022-07-25 14:26:10 +0000157 .asSequence()
Roman Elizarovc05de882020-07-16 23:34:40 +0300158 // Stable ordering of coroutines by their sequence number
159 .sortedBy { it.info.sequenceNumber }
160 // Leave in the dump only the coroutines that were not collected while we were dumping them
Vsevolod Tolstopyatov993c1922020-10-19 03:01:50 -0700161 .mapNotNull { owner ->
162 // Fuse map and filter into one operation to save an inline
163 if (owner.isFinished()) null
164 else owner.info.context?.let { context -> create(owner, context) }
Steve Elliottca095be2022-07-25 14:26:10 +0000165 }.toList()
Roman Elizarovc05de882020-07-16 23:34:40 +0300166 }
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300167
Vsevolod Tolstopyatov70a74872020-04-24 14:59:28 +0300168 /*
Steve Elliottca095be2022-07-25 14:26:10 +0000169 * This method optimises the number of packages sent by the IDEA debugger
170 * to a client VM to speed up fetching of coroutine information.
171 *
172 * The return value is an array of objects, which consists of four elements:
173 * 1) A string in a JSON format that stores information that is needed to display
174 * every coroutine in the coroutine panel in the IDEA debugger.
175 * 2) An array of last observed threads.
176 * 3) An array of last observed frames.
177 * 4) An array of DebugCoroutineInfo.
178 *
179 * ### Implementation note
180 * For methods like `dumpCoroutinesInfo` JDWP provides `com.sun.jdi.ObjectReference`
181 * that does a roundtrip to client VM for *each* field or property read.
182 * To avoid that, we serialize most of the critical for UI data into a primitives
183 * to save an exponential number of roundtrips.
184 *
185 * Internal (JVM-public) method used by IDEA debugger as of 1.6.0-RC.
186 */
187 @OptIn(ExperimentalStdlibApi::class)
188 public fun dumpCoroutinesInfoAsJsonAndReferences(): Array<Any> {
189 val coroutinesInfo = dumpCoroutinesInfo()
190 val size = coroutinesInfo.size
191 val lastObservedThreads = ArrayList<Thread?>(size)
192 val lastObservedFrames = ArrayList<CoroutineStackFrame?>(size)
193 val coroutinesInfoAsJson = ArrayList<String>(size)
194 for (info in coroutinesInfo) {
195 val context = info.context
196 val name = context[CoroutineName.Key]?.name?.toStringWithQuotes()
197 val dispatcher = context[CoroutineDispatcher.Key]?.toStringWithQuotes()
198 coroutinesInfoAsJson.add(
199 """
200 {
201 "name": $name,
202 "id": ${context[CoroutineId.Key]?.id},
203 "dispatcher": $dispatcher,
204 "sequenceNumber": ${info.sequenceNumber},
205 "state": "${info.state}"
206 }
207 """.trimIndent()
208 )
209 lastObservedFrames.add(info.lastObservedFrame)
210 lastObservedThreads.add(info.lastObservedThread)
211 }
212
213 return arrayOf(
214 "[${coroutinesInfoAsJson.joinToString()}]",
215 lastObservedThreads.toTypedArray(),
216 lastObservedFrames.toTypedArray(),
217 coroutinesInfo.toTypedArray()
218 )
219 }
220
221 /*
222 * Internal (JVM-public) method used by IDEA debugger as of 1.6.0-RC.
223 */
224 public fun enhanceStackTraceWithThreadDumpAsJson(info: DebugCoroutineInfo): String {
225 val stackTraceElements = enhanceStackTraceWithThreadDump(info, info.lastObservedStackTrace)
226 val stackTraceElementsInfoAsJson = mutableListOf<String>()
227 for (element in stackTraceElements) {
228 stackTraceElementsInfoAsJson.add(
229 """
230 {
231 "declaringClass": "${element.className}",
232 "methodName": "${element.methodName}",
233 "fileName": ${element.fileName?.toStringWithQuotes()},
234 "lineNumber": ${element.lineNumber}
235 }
236 """.trimIndent()
237 )
238 }
239
240 return "[${stackTraceElementsInfoAsJson.joinToString()}]"
241 }
242
243 private fun Any.toStringWithQuotes() = "\"$this\""
244
245 /*
Roman Elizarovc05de882020-07-16 23:34:40 +0300246 * Internal (JVM-public) method used by IDEA debugger as of 1.4-M3.
Vsevolod Tolstopyatov70a74872020-04-24 14:59:28 +0300247 */
Roman Elizarovc05de882020-07-16 23:34:40 +0300248 public fun dumpCoroutinesInfo(): List<DebugCoroutineInfo> =
249 dumpCoroutinesInfoImpl { owner, context -> DebugCoroutineInfo(owner.info, context) }
250
251 /*
252 * Internal (JVM-public) method to be used by IDEA debugger in the future (not used as of 1.4-M3).
253 * It is equivalent to [dumpCoroutinesInfo], but returns serializable (and thus less typed) objects.
254 */
255 public fun dumpDebuggerInfo(): List<DebuggerInfo> =
256 dumpCoroutinesInfoImpl { owner, context -> DebuggerInfo(owner.info, context) }
Vsevolod Tolstopyatov70a74872020-04-24 14:59:28 +0300257
Vsevolod Tolstopyatov4116fbf2020-03-13 14:13:09 +0300258 public fun dumpCoroutines(out: PrintStream): Unit = synchronized(out) {
Vsevolod Tolstopyatov3826ae52019-09-16 20:21:33 +0300259 /*
260 * This method synchronizes both on `out` and `this` for a reason:
Vsevolod Tolstopyatov4116fbf2020-03-13 14:13:09 +0300261 * 1) Taking a write lock is required to have a consistent snapshot of coroutines.
Vsevolod Tolstopyatov3826ae52019-09-16 20:21:33 +0300262 * 2) Synchronization on `out` is not required, but prohibits interleaving with any other
263 * (asynchronous) attempt to write to this `out` (System.out by default).
264 * Yet this prevents the progress of coroutines until they are fully dumped to the out which we find acceptable compromise.
265 */
266 dumpCoroutinesSynchronized(out)
Vsevolod Tolstopyatov5a80d2a2018-12-10 20:19:35 +0300267 }
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300268
Vsevolod Tolstopyatov993c1922020-10-19 03:01:50 -0700269 /*
270 * Filters out coroutines that do not call probeCoroutineCompleted,
271 * are completed, but not yet garbage collected.
272 *
273 * Typically, we intercept completion of the coroutine so it invokes "probeCoroutineCompleted",
274 * but it's not the case for lazy coroutines that get cancelled before start.
275 */
276 private fun CoroutineOwner<*>.isFinished(): Boolean {
277 // Guarded by lock
278 val job = info.context?.get(Job) ?: return false
279 if (!job.isCompleted) return false
280 capturedCoroutinesMap.remove(this) // Clean it up by the way
281 return true
282 }
283
Vsevolod Tolstopyatov4116fbf2020-03-13 14:13:09 +0300284 private fun dumpCoroutinesSynchronized(out: PrintStream): Unit = coroutineStateLock.write {
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300285 check(isInstalled) { "Debug probes are not installed" }
Vsevolod Tolstopyatov3826ae52019-09-16 20:21:33 +0300286 out.print("Coroutines dump ${dateFormat.format(System.currentTimeMillis())}")
Roman Elizarov12695b72018-12-11 19:20:39 +0300287 capturedCoroutines
Vsevolod Tolstopyatov993c1922020-10-19 03:01:50 -0700288 .asSequence()
289 .filter { !it.isFinished() }
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300290 .sortedBy { it.info.sequenceNumber }
291 .forEach { owner ->
292 val info = owner.info
293 val observedStackTrace = info.lastObservedStackTrace()
Roman Elizarovc05de882020-07-16 23:34:40 +0300294 val enhancedStackTrace = enhanceStackTraceWithThreadDumpImpl(info.state, info.lastObservedThread, observedStackTrace)
Vsevolod Tolstopyatov70a74872020-04-24 14:59:28 +0300295 val state = if (info.state == RUNNING && enhancedStackTrace === observedStackTrace)
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300296 "${info.state} (Last suspension stacktrace, not an actual stacktrace)"
Roman Elizarov12695b72018-12-11 19:20:39 +0300297 else
Roman Elizarovc05de882020-07-16 23:34:40 +0300298 info.state
Vsevolod Tolstopyatov3826ae52019-09-16 20:21:33 +0300299 out.print("\n\nCoroutine ${owner.delegate}, state: $state")
Roman Elizarov12695b72018-12-11 19:20:39 +0300300 if (observedStackTrace.isEmpty()) {
dkhalanskyjbc3d711b2021-05-13 14:44:27 +0300301 out.print("\n\tat ${createArtificialFrame(ARTIFICIAL_FRAME_MESSAGE)}")
Vsevolod Tolstopyatov3826ae52019-09-16 20:21:33 +0300302 printStackTrace(out, info.creationStackTrace)
Roman Elizarov12695b72018-12-11 19:20:39 +0300303 } else {
Vsevolod Tolstopyatov3826ae52019-09-16 20:21:33 +0300304 printStackTrace(out, enhancedStackTrace)
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300305 }
Roman Elizarov12695b72018-12-11 19:20:39 +0300306 }
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300307 }
308
Vsevolod Tolstopyatov3826ae52019-09-16 20:21:33 +0300309 private fun printStackTrace(out: PrintStream, frames: List<StackTraceElement>) {
310 frames.forEach { frame ->
311 out.print("\n\tat $frame")
312 }
313 }
314
Roman Elizarovc05de882020-07-16 23:34:40 +0300315 /*
316 * Internal (JVM-public) method used by IDEA debugger as of 1.4-M3.
317 * It is similar to [enhanceStackTraceWithThreadDumpImpl], but uses debugger-facing [DebugCoroutineInfo] type.
318 */
319 @Suppress("unused")
320 public fun enhanceStackTraceWithThreadDump(
321 info: DebugCoroutineInfo,
322 coroutineTrace: List<StackTraceElement>
323 ): List<StackTraceElement> =
324 enhanceStackTraceWithThreadDumpImpl(info.state, info.lastObservedThread, coroutineTrace)
325
Vsevolod Tolstopyatovf18878f2019-02-20 11:43:23 +0300326 /**
Roman Elizarovc05de882020-07-16 23:34:40 +0300327 * Tries to enhance [coroutineTrace] (obtained by call to [DebugCoroutineInfoImpl.lastObservedStackTrace]) with
328 * thread dump of [DebugCoroutineInfoImpl.lastObservedThread].
Vsevolod Tolstopyatovf18878f2019-02-20 11:43:23 +0300329 *
330 * Returns [coroutineTrace] if enhancement was unsuccessful or the enhancement result.
331 */
Roman Elizarovc05de882020-07-16 23:34:40 +0300332 private fun enhanceStackTraceWithThreadDumpImpl(
333 state: String,
334 thread: Thread?,
Vsevolod Tolstopyatovf18878f2019-02-20 11:43:23 +0300335 coroutineTrace: List<StackTraceElement>
336 ): List<StackTraceElement> {
Roman Elizarovc05de882020-07-16 23:34:40 +0300337 if (state != RUNNING || thread == null) return coroutineTrace
Vsevolod Tolstopyatovf18878f2019-02-20 11:43:23 +0300338 // Avoid security manager issues
339 val actualTrace = runCatching { thread.stackTrace }.getOrNull()
340 ?: return coroutineTrace
341
342 /*
343 * Here goes heuristic that tries to merge two stacktraces: real one
344 * (that has at least one but usually not so many suspend function frames)
345 * and coroutine one that has only suspend function frames.
346 *
347 * Heuristic:
348 * 1) Dump lastObservedThread
349 * 2) Find the next frame after BaseContinuationImpl.resumeWith (continuation machinery).
350 * Invariant: this method is called under the lock, so such method **should** be present
351 * in continuation stacktrace.
Vsevolod Tolstopyatovf18878f2019-02-20 11:43:23 +0300352 * 3) Find target method in continuation stacktrace (metadata-based)
353 * 4) Prepend dumped stacktrace (trimmed by target frame) to continuation stacktrace
354 *
355 * Heuristic may fail on recursion and overloads, but it will be automatically improved
Vsevolod Tolstopyatove35baa42019-03-05 18:26:09 +0300356 * with KT-29997.
Vsevolod Tolstopyatovf18878f2019-02-20 11:43:23 +0300357 */
358 val indexOfResumeWith = actualTrace.indexOfFirst {
359 it.className == "kotlin.coroutines.jvm.internal.BaseContinuationImpl" &&
360 it.methodName == "resumeWith" &&
361 it.fileName == "ContinuationImpl.kt"
362 }
363
Vsevolod Tolstopyatove62f8f72021-04-15 22:57:35 +0300364 val (continuationStartFrame, delta) = findContinuationStartIndex(
Vsevolod Tolstopyatovffaedf12019-03-01 20:30:56 +0300365 indexOfResumeWith,
366 actualTrace,
Vsevolod Tolstopyatov70a74872020-04-24 14:59:28 +0300367 coroutineTrace
368 )
Vsevolod Tolstopyatovf18878f2019-02-20 11:43:23 +0300369
Vsevolod Tolstopyatovffaedf12019-03-01 20:30:56 +0300370 if (continuationStartFrame == -1) return coroutineTrace
Vsevolod Tolstopyatovf18878f2019-02-20 11:43:23 +0300371
Vsevolod Tolstopyatovffaedf12019-03-01 20:30:56 +0300372 val expectedSize = indexOfResumeWith + coroutineTrace.size - continuationStartFrame - 1 - delta
Vsevolod Tolstopyatovf18878f2019-02-20 11:43:23 +0300373 val result = ArrayList<StackTraceElement>(expectedSize)
Vsevolod Tolstopyatovffaedf12019-03-01 20:30:56 +0300374 for (index in 0 until indexOfResumeWith - delta) {
Vsevolod Tolstopyatovf18878f2019-02-20 11:43:23 +0300375 result += actualTrace[index]
376 }
377
Vsevolod Tolstopyatovffaedf12019-03-01 20:30:56 +0300378 for (index in continuationStartFrame + 1 until coroutineTrace.size) {
Vsevolod Tolstopyatovf18878f2019-02-20 11:43:23 +0300379 result += coroutineTrace[index]
380 }
381
382 return result
383 }
384
Vsevolod Tolstopyatovffaedf12019-03-01 20:30:56 +0300385 /**
386 * Tries to find the lowest meaningful frame above `resumeWith` in the real stacktrace and
387 * its match in a coroutines stacktrace (steps 2-3 in heuristic).
388 *
389 * This method does more than just matching `realTrace.indexOf(resumeWith) - 1`:
390 * If method above `resumeWith` has no line number (thus it is `stateMachine.invokeSuspend`),
391 * it's skipped and attempt to match next one is made because state machine could have been missing in the original coroutine stacktrace.
392 *
Vsevolod Tolstopyatove62f8f72021-04-15 22:57:35 +0300393 * Returns index of such frame (or -1) and number of skipped frames (up to 2, for state machine and for access$).
Vsevolod Tolstopyatovffaedf12019-03-01 20:30:56 +0300394 */
395 private fun findContinuationStartIndex(
396 indexOfResumeWith: Int,
397 actualTrace: Array<StackTraceElement>,
398 coroutineTrace: List<StackTraceElement>
Vsevolod Tolstopyatove62f8f72021-04-15 22:57:35 +0300399 ): Pair<Int, Int> {
400 /*
401 * Since Kotlin 1.5.0 we have these access$ methods that we have to skip.
402 * So we have to test next frame for invokeSuspend, for $access and for actual suspending call.
403 */
404 repeat(3) {
405 val result = findIndexOfFrame(indexOfResumeWith - 1 - it, actualTrace, coroutineTrace)
406 if (result != -1) return result to it
407 }
408 return -1 to 0
Vsevolod Tolstopyatovffaedf12019-03-01 20:30:56 +0300409 }
410
411 private fun findIndexOfFrame(
412 frameIndex: Int,
413 actualTrace: Array<StackTraceElement>,
414 coroutineTrace: List<StackTraceElement>
415 ): Int {
416 val continuationFrame = actualTrace.getOrNull(frameIndex)
417 ?: return -1
418
419 return coroutineTrace.indexOfFirst {
420 it.fileName == continuationFrame.fileName &&
421 it.className == continuationFrame.className &&
422 it.methodName == continuationFrame.methodName
423 }
424 }
425
Vsevolod Tolstopyatov70a74872020-04-24 14:59:28 +0300426 internal fun probeCoroutineResumed(frame: Continuation<*>) = updateState(frame, RUNNING)
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300427
Vsevolod Tolstopyatov70a74872020-04-24 14:59:28 +0300428 internal fun probeCoroutineSuspended(frame: Continuation<*>) = updateState(frame, SUSPENDED)
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300429
Vsevolod Tolstopyatov70a74872020-04-24 14:59:28 +0300430 private fun updateState(frame: Continuation<*>, state: String) {
Vsevolod Tolstopyatovad542c42020-06-17 04:43:28 -0700431 if (!isInstalled) return
Vsevolod Tolstopyatove35baa42019-03-05 18:26:09 +0300432 // KT-29997 is here only since 1.3.30
Vsevolod Tolstopyatov70a74872020-04-24 14:59:28 +0300433 if (state == RUNNING && KotlinVersion.CURRENT.isAtLeast(1, 3, 30)) {
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300434 val stackFrame = frame as? CoroutineStackFrame ?: return
435 updateRunningState(stackFrame, state)
Vsevolod Tolstopyatove35baa42019-03-05 18:26:09 +0300436 return
437 }
438
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300439 // Find ArtificialStackFrame of the coroutine
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300440 val owner = frame.owner() ?: return
Vsevolod Tolstopyatov5a80d2a2018-12-10 20:19:35 +0300441 updateState(owner, frame, state)
442 }
443
Vsevolod Tolstopyatov4116fbf2020-03-13 14:13:09 +0300444 // See comment to callerInfoCache
Vsevolod Tolstopyatov70a74872020-04-24 14:59:28 +0300445 private fun updateRunningState(frame: CoroutineStackFrame, state: String): Unit = coroutineStateLock.read {
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300446 if (!isInstalled) return
447 // Lookup coroutine info in cache or by traversing stack frame
Roman Elizarovc05de882020-07-16 23:34:40 +0300448 val info: DebugCoroutineInfoImpl
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300449 val cached = callerInfoCache.remove(frame)
450 if (cached != null) {
451 info = cached
452 } else {
453 info = frame.owner()?.info ?: return
454 // Guard against improper implementations of CoroutineStackFrame and bugs in the compiler
Vsevolod Tolstopyatov4116fbf2020-03-13 14:13:09 +0300455 val realCaller = info.lastObservedFrame?.realCaller()
456 if (realCaller != null) callerInfoCache.remove(realCaller)
Vsevolod Tolstopyatove35baa42019-03-05 18:26:09 +0300457 }
458
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300459 info.updateState(state, frame as Continuation<*>)
460 // Do not cache it for proxy-classes such as ScopeCoroutines
461 val caller = frame.realCaller() ?: return
462 callerInfoCache[caller] = info
Vsevolod Tolstopyatove35baa42019-03-05 18:26:09 +0300463 }
464
465 private tailrec fun CoroutineStackFrame.realCaller(): CoroutineStackFrame? {
466 val caller = callerFrame ?: return null
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300467 return if (caller.getStackTraceElement() != null) caller else caller.realCaller()
Vsevolod Tolstopyatove35baa42019-03-05 18:26:09 +0300468 }
469
Vsevolod Tolstopyatov70a74872020-04-24 14:59:28 +0300470 private fun updateState(owner: CoroutineOwner<*>, frame: Continuation<*>, state: String) = coroutineStateLock.read {
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300471 if (!isInstalled) return
472 owner.info.updateState(state, frame)
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300473 }
474
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300475 private fun Continuation<*>.owner(): CoroutineOwner<*>? = (this as? CoroutineStackFrame)?.owner()
Vsevolod Tolstopyatov8e2428c2018-12-10 19:12:08 +0300476
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300477 private tailrec fun CoroutineStackFrame.owner(): CoroutineOwner<*>? =
478 if (this is CoroutineOwner<*>) this else callerFrame?.owner()
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300479
Vsevolod Tolstopyatov4116fbf2020-03-13 14:13:09 +0300480 // Not guarded by the lock at all, does not really affect consistency
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300481 internal fun <T> probeCoroutineCreated(completion: Continuation<T>): Continuation<T> {
Roman Elizarov12695b72018-12-11 19:20:39 +0300482 if (!isInstalled) return completion
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300483 /*
Vsevolod Tolstopyatov8e2428c2018-12-10 19:12:08 +0300484 * If completion already has an owner, it means that we are in scoped coroutine (coroutineScope, withContext etc.),
485 * then piggyback on its already existing owner and do not replace completion
486 */
487 val owner = completion.owner()
Roman Elizarov12695b72018-12-11 19:20:39 +0300488 if (owner != null) return completion
Vsevolod Tolstopyatov8e2428c2018-12-10 19:12:08 +0300489 /*
Roman Elizarovc05de882020-07-16 23:34:40 +0300490 * Here we replace completion with a sequence of StackTraceFrame objects
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300491 * which represents creation stacktrace, thus making stacktrace recovery mechanism
492 * even more verbose (it will attach coroutine creation stacktrace to all exceptions),
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300493 * and then using CoroutineOwner completion as unique identifier of coroutineSuspended/resumed calls.
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300494 */
Vsevolod Tolstopyatov70a74872020-04-24 14:59:28 +0300495 val frame = if (enableCreationStackTraces) {
Roman Elizarovc05de882020-07-16 23:34:40 +0300496 sanitizeStackTrace(Exception()).toStackTraceFrame()
Vsevolod Tolstopyatov4116fbf2020-03-13 14:13:09 +0300497 } else {
498 null
499 }
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300500 return createOwner(completion, frame)
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300501 }
502
dkhalanskyjbc3d711b2021-05-13 14:44:27 +0300503 private fun List<StackTraceElement>.toStackTraceFrame(): StackTraceFrame? =
504 foldRight<StackTraceElement, StackTraceFrame?>(null) { frame, acc ->
505 StackTraceFrame(acc, frame)
506 }
Roman Elizarovc05de882020-07-16 23:34:40 +0300507
508 private fun <T> createOwner(completion: Continuation<T>, frame: StackTraceFrame?): Continuation<T> {
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300509 if (!isInstalled) return completion
Roman Elizarovc05de882020-07-16 23:34:40 +0300510 val info = DebugCoroutineInfoImpl(completion.context, frame, sequenceNumber.incrementAndGet())
dkhalanskyjbc3d711b2021-05-13 14:44:27 +0300511 val owner = CoroutineOwner(completion, info, frame)
Roman Elizarovc05de882020-07-16 23:34:40 +0300512 capturedCoroutinesMap[owner] = true
513 if (!isInstalled) capturedCoroutinesMap.clear()
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300514 return owner
Vsevolod Tolstopyatov5a80d2a2018-12-10 20:19:35 +0300515 }
516
Vsevolod Tolstopyatov4116fbf2020-03-13 14:13:09 +0300517 // Not guarded by the lock at all, does not really affect consistency
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300518 private fun probeCoroutineCompleted(owner: CoroutineOwner<*>) {
Roman Elizarovc05de882020-07-16 23:34:40 +0300519 capturedCoroutinesMap.remove(owner)
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300520 /*
521 * This removal is a guard against improperly implemented CoroutineStackFrame
522 * and bugs in the compiler.
523 */
Vsevolod Tolstopyatov4116fbf2020-03-13 14:13:09 +0300524 val caller = owner.info.lastObservedFrame?.realCaller() ?: return
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300525 callerInfoCache.remove(caller)
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300526 }
527
Vsevolod Tolstopyatov2496dc22019-03-05 18:43:13 +0300528 /**
529 * This class is injected as completion of all continuations in [probeCoroutineCompleted].
530 * It is owning the coroutine info and responsible for managing all its external info related to debug agent.
531 */
532 private class CoroutineOwner<T>(
Vsevolod Tolstopyatov8e2428c2018-12-10 19:12:08 +0300533 @JvmField val delegate: Continuation<T>,
dkhalanskyjbc3d711b2021-05-13 14:44:27 +0300534 @JvmField val info: DebugCoroutineInfoImpl,
535 private val frame: CoroutineStackFrame?
Vsevolod Tolstopyatov4116fbf2020-03-13 14:13:09 +0300536 ) : Continuation<T> by delegate, CoroutineStackFrame {
537
538 override val callerFrame: CoroutineStackFrame?
539 get() = frame?.callerFrame
540
541 override fun getStackTraceElement(): StackTraceElement? = frame?.getStackTraceElement()
542
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300543 override fun resumeWith(result: Result<T>) {
544 probeCoroutineCompleted(this)
545 delegate.resumeWith(result)
546 }
547
548 override fun toString(): String = delegate.toString()
549 }
550
Vsevolod Tolstopyatov5a80d2a2018-12-10 20:19:35 +0300551 private fun <T : Throwable> sanitizeStackTrace(throwable: T): List<StackTraceElement> {
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300552 val stackTrace = throwable.stackTrace
553 val size = stackTrace.size
dkhalanskyjbc3d711b2021-05-13 14:44:27 +0300554 val probeIndex = stackTrace.indexOfLast { it.className == "kotlin.coroutines.jvm.internal.DebugProbesKt" }
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300555
Vsevolod Tolstopyatov70a74872020-04-24 14:59:28 +0300556 if (!sanitizeStackTraces) {
dkhalanskyjbc3d711b2021-05-13 14:44:27 +0300557 return List(size - probeIndex) {
558 if (it == 0) createArtificialFrame(ARTIFICIAL_FRAME_MESSAGE) else stackTrace[it + probeIndex]
559 }
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300560 }
561
562 /*
563 * Trim intervals of internal methods from the stacktrace (bounds are excluded from trimming)
dkhalanskyjb5e7c61f2021-01-18 11:29:38 +0300564 * E.g. for sequence [e, i1, i2, i3, e, i4, e, i5, i6, i7]
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300565 * output will be [e, i1, i3, e, i4, e, i5, i7]
dkhalanskyjb5e7c61f2021-01-18 11:29:38 +0300566 *
567 * If an interval of internal methods ends in a synthetic method, the outermost non-synthetic method in that
568 * interval will also be included.
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300569 */
dkhalanskyjbc3d711b2021-05-13 14:44:27 +0300570 val result = ArrayList<StackTraceElement>(size - probeIndex + 1)
571 result += createArtificialFrame(ARTIFICIAL_FRAME_MESSAGE)
572 var i = probeIndex + 1
dkhalanskyjb5e7c61f2021-01-18 11:29:38 +0300573 while (i < size) {
574 if (stackTrace[i].isInternalMethod) {
575 result += stackTrace[i] // we include the boundary of the span in any case
576 // first index past the end of the span of internal methods that starts from `i`
577 var j = i + 1
578 while (j < size && stackTrace[j].isInternalMethod) {
579 ++j
580 }
581 // index of the last non-synthetic internal methods in this span, or `i` if there are no such methods
582 var k = j - 1
583 while (k > i && stackTrace[k].fileName == null) {
584 k -= 1
585 }
586 if (k > i && k < j - 1) {
587 /* there are synthetic internal methods at the end of this span, but there is a non-synthetic method
588 after `i`, so we include it. */
589 result += stackTrace[k]
590 }
591 result += stackTrace[j - 1] // we include the other boundary of this span in any case, too
592 i = j
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300593 } else {
dkhalanskyjb5e7c61f2021-01-18 11:29:38 +0300594 result += stackTrace[i]
595 ++i
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300596 }
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300597 }
Vsevolod Tolstopyatov5a80d2a2018-12-10 20:19:35 +0300598 return result
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300599 }
600
601 private val StackTraceElement.isInternalMethod: Boolean get() = className.startsWith("kotlinx.coroutines")
Vsevolod Tolstopyatovc7239ac2018-12-10 11:41:00 +0300602}