1 /*
<lambda>null2 * 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 kotlinx.atomicfu.*
8 import kotlinx.coroutines.*
9 import kotlinx.coroutines.internal.ScopeCoroutine
10 import java.io.*
11 import java.lang.StackTraceElement
12 import java.text.*
13 import java.util.concurrent.locks.*
14 import kotlin.collections.ArrayList
15 import kotlin.concurrent.*
16 import kotlin.coroutines.*
17 import kotlin.coroutines.jvm.internal.CoroutineStackFrame
18 import kotlin.synchronized
19 import _COROUTINE.ArtificialStackFrames
20
21 @PublishedApi
22 internal object DebugProbesImpl {
23 private val ARTIFICIAL_FRAME = ArtificialStackFrames().coroutineCreation()
24 private val dateFormat = SimpleDateFormat("yyyy/MM/dd HH:mm:ss")
25
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
32 private val installations = atomic(0)
33
34 /**
35 * This internal method is used by the IDEA debugger under the JVM name
36 * "isInstalled$kotlinx_coroutines_debug" and must be kept binary-compatible, see KTIJ-24102
37 */
38 val isInstalled: Boolean get() = installations.value > 0
39
40 // To sort coroutines by creation order, used as a unique id
41 private val sequenceNumber = atomic(0L)
42
43 internal var sanitizeStackTraces: Boolean = true
44 internal var enableCreationStackTraces: Boolean = true
45 public var ignoreCoroutinesWithEmptyContext: Boolean = true
46
47 /*
48 * Substitute for service loader, DI between core and debug modules.
49 * If the agent was installed via command line -javaagent parameter, do not use byte-buddy to avoid dynamic attach.
50 */
51 private val dynamicAttach = getDynamicAttach()
52
53 @Suppress("UNCHECKED_CAST")
54 private fun getDynamicAttach(): Function1<Boolean, Unit>? = runCatching {
55 val clz = Class.forName("kotlinx.coroutines.debug.internal.ByteBuddyDynamicAttach")
56 val ctor = clz.constructors[0]
57 ctor.newInstance() as Function1<Boolean, Unit>
58 }.getOrNull()
59
60 /**
61 * Because `probeCoroutinesResumed` is called for every resumed continuation (see KT-29997 and the related code),
62 * we perform a performance optimization:
63 * Imagine a suspending call stack a()->b()->c(), where c() completes its execution and every call is
64 * "almost" in tail position.
65 *
66 * Then at least three RUNNING -> RUNNING transitions will occur consecutively, the complexity of each O(depth).
67 * To avoid this quadratic complexity, we are caching lookup result for such chains in this map and update it incrementally.
68 *
69 * [DebugCoroutineInfoImpl] keeps a lot of auxiliary information about a coroutine, so we use a weak reference queue
70 * to promptly release the corresponding memory when the reference to the coroutine itself was already collected.
71 */
72 private val callerInfoCache = ConcurrentWeakMap<CoroutineStackFrame, DebugCoroutineInfoImpl>(weakRefQueue = true)
73
74 internal fun install() {
75 if (installations.incrementAndGet() > 1) return
76 startWeakRefCleanerThread()
77 if (AgentInstallationType.isInstalledStatically) return
78 dynamicAttach?.invoke(true) // attach
79 }
80
81 internal fun uninstall() {
82 check(isInstalled) { "Agent was not installed" }
83 if (installations.decrementAndGet() != 0) return
84 stopWeakRefCleanerThread()
85 capturedCoroutinesMap.clear()
86 callerInfoCache.clear()
87 if (AgentInstallationType.isInstalledStatically) return
88 dynamicAttach?.invoke(false) // detach
89 }
90
91 private fun startWeakRefCleanerThread() {
92 weakRefCleanerThread = thread(isDaemon = true, name = "Coroutines Debugger Cleaner") {
93 callerInfoCache.runWeakRefQueueCleaningLoopUntilInterrupted()
94 }
95 }
96
97 private fun stopWeakRefCleanerThread() {
98 val thread = weakRefCleanerThread ?: return
99 weakRefCleanerThread = null
100 thread.interrupt()
101 thread.join()
102 }
103
104 internal fun hierarchyToString(job: Job): String {
105 check(isInstalled) { "Debug probes are not installed" }
106 val jobToStack = capturedCoroutines
107 .filter { it.delegate.context[Job] != null }
108 .associateBy({ it.delegate.context.job }, { it.info })
109 return buildString {
110 job.build(jobToStack, this, "")
111 }
112 }
113
114 private fun Job.build(map: Map<Job, DebugCoroutineInfoImpl>, builder: StringBuilder, indent: String) {
115 val info = map[this]
116 val newIndent: String
117 if (info == null) { // Append coroutine without stacktrace
118 // Do not print scoped coroutines and do not increase indentation level
119 @Suppress("INVISIBLE_REFERENCE")
120 if (this !is ScopeCoroutine<*>) {
121 builder.append("$indent$debugString\n")
122 newIndent = indent + "\t"
123 } else {
124 newIndent = indent
125 }
126 } else {
127 // Append coroutine with its last stacktrace element
128 val element = info.lastObservedStackTrace().firstOrNull()
129 val state = info.state
130 builder.append("$indent$debugString, continuation is $state at line $element\n")
131 newIndent = indent + "\t"
132 }
133 // Append children with new indent
134 for (child in children) {
135 child.build(map, builder, newIndent)
136 }
137 }
138
139 @Suppress("DEPRECATION_ERROR") // JobSupport
140 private val Job.debugString: String get() = if (this is JobSupport) toDebugString() else toString()
141
142 /**
143 * Private method that dumps coroutines so that different public-facing method can use
144 * to produce different result types.
145 */
146 private inline fun <R : Any> dumpCoroutinesInfoImpl(crossinline create: (CoroutineOwner<*>, CoroutineContext) -> R): List<R> {
147 check(isInstalled) { "Debug probes are not installed" }
148 return capturedCoroutines
149 .asSequence()
150 // Stable ordering of coroutines by their sequence number
151 .sortedBy { it.info.sequenceNumber }
152 // Leave in the dump only the coroutines that were not collected while we were dumping them
153 .mapNotNull { owner ->
154 // Fuse map and filter into one operation to save an inline
155 if (owner.isFinished()) null
156 else owner.info.context?.let { context -> create(owner, context) }
157 }.toList()
158 }
159
160 /*
161 * This method optimises the number of packages sent by the IDEA debugger
162 * to a client VM to speed up fetching of coroutine information.
163 *
164 * The return value is an array of objects, which consists of four elements:
165 * 1) A string in a JSON format that stores information that is needed to display
166 * every coroutine in the coroutine panel in the IDEA debugger.
167 * 2) An array of last observed threads.
168 * 3) An array of last observed frames.
169 * 4) An array of DebugCoroutineInfo.
170 *
171 * ### Implementation note
172 * For methods like `dumpCoroutinesInfo` JDWP provides `com.sun.jdi.ObjectReference`
173 * that does a roundtrip to client VM for *each* field or property read.
174 * To avoid that, we serialize most of the critical for UI data into a primitives
175 * to save an exponential number of roundtrips.
176 *
177 * Internal (JVM-public) method used by IDEA debugger as of 1.6.0-RC.
178 * See KTIJ-24102.
179 */
180 @OptIn(ExperimentalStdlibApi::class)
181 fun dumpCoroutinesInfoAsJsonAndReferences(): Array<Any> {
182 val coroutinesInfo = dumpCoroutinesInfo()
183 val size = coroutinesInfo.size
184 val lastObservedThreads = ArrayList<Thread?>(size)
185 val lastObservedFrames = ArrayList<CoroutineStackFrame?>(size)
186 val coroutinesInfoAsJson = ArrayList<String>(size)
187 for (info in coroutinesInfo) {
188 val context = info.context
189 val name = context[CoroutineName.Key]?.name?.toStringRepr()
190 val dispatcher = context[CoroutineDispatcher.Key]?.toStringRepr()
191 coroutinesInfoAsJson.add(
192 """
193 {
194 "name": $name,
195 "id": ${context[CoroutineId.Key]?.id},
196 "dispatcher": $dispatcher,
197 "sequenceNumber": ${info.sequenceNumber},
198 "state": "${info.state}"
199 }
200 """.trimIndent()
201 )
202 lastObservedFrames.add(info.lastObservedFrame)
203 lastObservedThreads.add(info.lastObservedThread)
204 }
205
206 return arrayOf(
207 "[${coroutinesInfoAsJson.joinToString()}]",
208 lastObservedThreads.toTypedArray(),
209 lastObservedFrames.toTypedArray(),
210 coroutinesInfo.toTypedArray()
211 )
212 }
213
214 /*
215 * Internal (JVM-public) method used by IDEA debugger as of 1.6.0-RC, must be kept binary-compatible, see KTIJ-24102
216 */
217 fun enhanceStackTraceWithThreadDumpAsJson(info: DebugCoroutineInfo): String {
218 val stackTraceElements = enhanceStackTraceWithThreadDump(info, info.lastObservedStackTrace)
219 val stackTraceElementsInfoAsJson = mutableListOf<String>()
220 for (element in stackTraceElements) {
221 stackTraceElementsInfoAsJson.add(
222 """
223 {
224 "declaringClass": "${element.className}",
225 "methodName": "${element.methodName}",
226 "fileName": ${element.fileName?.toStringRepr()},
227 "lineNumber": ${element.lineNumber}
228 }
229 """.trimIndent()
230 )
231 }
232
233 return "[${stackTraceElementsInfoAsJson.joinToString()}]"
234 }
235
236 private fun Any.toStringRepr() = toString().repr()
237
238 /*
239 * Internal (JVM-public) method used by IDEA debugger as of 1.4-M3. See KTIJ-24102
240 */
241 fun dumpCoroutinesInfo(): List<DebugCoroutineInfo> =
242 dumpCoroutinesInfoImpl { owner, context -> DebugCoroutineInfo(owner.info, context) }
243
244 /*
245 * Internal (JVM-public) method to be used by IDEA debugger in the future (not used as of 1.4-M3).
246 * It is equivalent to [dumpCoroutinesInfo], but returns serializable (and thus less typed) objects.
247 */
248 fun dumpDebuggerInfo(): List<DebuggerInfo> =
249 dumpCoroutinesInfoImpl { owner, context -> DebuggerInfo(owner.info, context) }
250
251 @JvmName("dumpCoroutines")
252 internal fun dumpCoroutines(out: PrintStream): Unit = synchronized(out) {
253 /*
254 * This method synchronizes both on `out` and `this` for a reason:
255 * 1) Taking a write lock is required to have a consistent snapshot of coroutines.
256 * 2) Synchronization on `out` is not required, but prohibits interleaving with any other
257 * (asynchronous) attempt to write to this `out` (System.out by default).
258 * Yet this prevents the progress of coroutines until they are fully dumped to the out which we find acceptable compromise.
259 */
260 dumpCoroutinesSynchronized(out)
261 }
262
263 /*
264 * Filters out coroutines that do not call probeCoroutineCompleted,
265 * are completed, but not yet garbage collected.
266 *
267 * Typically, we intercept completion of the coroutine so it invokes "probeCoroutineCompleted",
268 * but it's not the case for lazy coroutines that get cancelled before start.
269 */
270 private fun CoroutineOwner<*>.isFinished(): Boolean {
271 // Guarded by lock
272 val job = info.context?.get(Job) ?: return false
273 if (!job.isCompleted) return false
274 capturedCoroutinesMap.remove(this) // Clean it up by the way
275 return true
276 }
277
278 private fun dumpCoroutinesSynchronized(out: PrintStream) {
279 check(isInstalled) { "Debug probes are not installed" }
280 out.print("Coroutines dump ${dateFormat.format(System.currentTimeMillis())}")
281 capturedCoroutines
282 .asSequence()
283 .filter { !it.isFinished() }
284 .sortedBy { it.info.sequenceNumber }
285 .forEach { owner ->
286 val info = owner.info
287 val observedStackTrace = info.lastObservedStackTrace()
288 val enhancedStackTrace = enhanceStackTraceWithThreadDumpImpl(info.state, info.lastObservedThread, observedStackTrace)
289 val state = if (info.state == RUNNING && enhancedStackTrace === observedStackTrace)
290 "${info.state} (Last suspension stacktrace, not an actual stacktrace)"
291 else
292 info.state
293 out.print("\n\nCoroutine ${owner.delegate}, state: $state")
294 if (observedStackTrace.isEmpty()) {
295 out.print("\n\tat $ARTIFICIAL_FRAME")
296 printStackTrace(out, info.creationStackTrace)
297 } else {
298 printStackTrace(out, enhancedStackTrace)
299 }
300 }
301 }
302
303 private fun printStackTrace(out: PrintStream, frames: List<StackTraceElement>) {
304 frames.forEach { frame ->
305 out.print("\n\tat $frame")
306 }
307 }
308
309 /*
310 * Internal (JVM-public) method used by IDEA debugger as of 1.4-M3, must be kept binary-compatible. See KTIJ-24102.
311 * It is similar to [enhanceStackTraceWithThreadDumpImpl], but uses debugger-facing [DebugCoroutineInfo] type.
312 */
313 @Suppress("unused")
314 fun enhanceStackTraceWithThreadDump(
315 info: DebugCoroutineInfo,
316 coroutineTrace: List<StackTraceElement>
317 ): List<StackTraceElement> =
318 enhanceStackTraceWithThreadDumpImpl(info.state, info.lastObservedThread, coroutineTrace)
319
320 /**
321 * Tries to enhance [coroutineTrace] (obtained by call to [DebugCoroutineInfoImpl.lastObservedStackTrace]) with
322 * thread dump of [DebugCoroutineInfoImpl.lastObservedThread].
323 *
324 * Returns [coroutineTrace] if enhancement was unsuccessful or the enhancement result.
325 */
326 private fun enhanceStackTraceWithThreadDumpImpl(
327 state: String,
328 thread: Thread?,
329 coroutineTrace: List<StackTraceElement>
330 ): List<StackTraceElement> {
331 if (state != RUNNING || thread == null) return coroutineTrace
332 // Avoid security manager issues
333 val actualTrace = runCatching { thread.stackTrace }.getOrNull()
334 ?: return coroutineTrace
335
336 /*
337 * Here goes heuristic that tries to merge two stacktraces: real one
338 * (that has at least one but usually not so many suspend function frames)
339 * and coroutine one that has only suspend function frames.
340 *
341 * Heuristic:
342 * 1) Dump lastObservedThread
343 * 2) Find the next frame after BaseContinuationImpl.resumeWith (continuation machinery).
344 * Invariant: this method is called under the lock, so such method **should** be present
345 * in continuation stacktrace.
346 * 3) Find target method in continuation stacktrace (metadata-based)
347 * 4) Prepend dumped stacktrace (trimmed by target frame) to continuation stacktrace
348 *
349 * Heuristic may fail on recursion and overloads, but it will be automatically improved
350 * with KT-29997.
351 */
352 val indexOfResumeWith = actualTrace.indexOfFirst {
353 it.className == "kotlin.coroutines.jvm.internal.BaseContinuationImpl" &&
354 it.methodName == "resumeWith" &&
355 it.fileName == "ContinuationImpl.kt"
356 }
357
358 val (continuationStartFrame, delta) = findContinuationStartIndex(
359 indexOfResumeWith,
360 actualTrace,
361 coroutineTrace
362 )
363
364 if (continuationStartFrame == -1) return coroutineTrace
365
366 val expectedSize = indexOfResumeWith + coroutineTrace.size - continuationStartFrame - 1 - delta
367 val result = ArrayList<StackTraceElement>(expectedSize)
368 for (index in 0 until indexOfResumeWith - delta) {
369 result += actualTrace[index]
370 }
371
372 for (index in continuationStartFrame + 1 until coroutineTrace.size) {
373 result += coroutineTrace[index]
374 }
375
376 return result
377 }
378
379 /**
380 * Tries to find the lowest meaningful frame above `resumeWith` in the real stacktrace and
381 * its match in a coroutines stacktrace (steps 2-3 in heuristic).
382 *
383 * This method does more than just matching `realTrace.indexOf(resumeWith) - 1`:
384 * If method above `resumeWith` has no line number (thus it is `stateMachine.invokeSuspend`),
385 * it's skipped and attempt to match next one is made because state machine could have been missing in the original coroutine stacktrace.
386 *
387 * Returns index of such frame (or -1) and number of skipped frames (up to 2, for state machine and for access$).
388 */
389 private fun findContinuationStartIndex(
390 indexOfResumeWith: Int,
391 actualTrace: Array<StackTraceElement>,
392 coroutineTrace: List<StackTraceElement>
393 ): Pair<Int, Int> {
394 /*
395 * Since Kotlin 1.5.0 we have these access$ methods that we have to skip.
396 * So we have to test next frame for invokeSuspend, for $access and for actual suspending call.
397 */
398 repeat(3) {
399 val result = findIndexOfFrame(indexOfResumeWith - 1 - it, actualTrace, coroutineTrace)
400 if (result != -1) return result to it
401 }
402 return -1 to 0
403 }
404
405 private fun findIndexOfFrame(
406 frameIndex: Int,
407 actualTrace: Array<StackTraceElement>,
408 coroutineTrace: List<StackTraceElement>
409 ): Int {
410 val continuationFrame = actualTrace.getOrNull(frameIndex)
411 ?: return -1
412
413 return coroutineTrace.indexOfFirst {
414 it.fileName == continuationFrame.fileName &&
415 it.className == continuationFrame.className &&
416 it.methodName == continuationFrame.methodName
417 }
418 }
419
420 internal fun probeCoroutineResumed(frame: Continuation<*>) = updateState(frame, RUNNING)
421
422 internal fun probeCoroutineSuspended(frame: Continuation<*>) = updateState(frame, SUSPENDED)
423
424 private fun updateState(frame: Continuation<*>, state: String) {
425 if (!isInstalled) return
426 if (ignoreCoroutinesWithEmptyContext && frame.context === EmptyCoroutineContext) return // See ignoreCoroutinesWithEmptyContext
427 if (state == RUNNING) {
428 val stackFrame = frame as? CoroutineStackFrame ?: return
429 updateRunningState(stackFrame, state)
430 return
431 }
432
433 // Find ArtificialStackFrame of the coroutine
434 val owner = frame.owner() ?: return
435 updateState(owner, frame, state)
436 }
437
438 // See comment to callerInfoCache
439 private fun updateRunningState(frame: CoroutineStackFrame, state: String) {
440 if (!isInstalled) return
441 // Lookup coroutine info in cache or by traversing stack frame
442 val info: DebugCoroutineInfoImpl
443 val cached = callerInfoCache.remove(frame)
444 val shouldBeMatchedWithProbeSuspended: Boolean
445 if (cached != null) {
446 info = cached
447 shouldBeMatchedWithProbeSuspended = false
448 } else {
449 info = frame.owner()?.info ?: return
450 shouldBeMatchedWithProbeSuspended = true
451 // Guard against improper implementations of CoroutineStackFrame and bugs in the compiler
452 val realCaller = info.lastObservedFrame?.realCaller()
453 if (realCaller != null) callerInfoCache.remove(realCaller)
454 }
455 info.updateState(state, frame as Continuation<*>, shouldBeMatchedWithProbeSuspended)
456 // Do not cache it for proxy-classes such as ScopeCoroutines
457 val caller = frame.realCaller() ?: return
458 callerInfoCache[caller] = info
459 }
460
461 private tailrec fun CoroutineStackFrame.realCaller(): CoroutineStackFrame? {
462 val caller = callerFrame ?: return null
463 return if (caller.getStackTraceElement() != null) caller else caller.realCaller()
464 }
465
466 private fun updateState(owner: CoroutineOwner<*>, frame: Continuation<*>, state: String) {
467 if (!isInstalled) return
468 owner.info.updateState(state, frame, true)
469 }
470
471 private fun Continuation<*>.owner(): CoroutineOwner<*>? = (this as? CoroutineStackFrame)?.owner()
472
473 private tailrec fun CoroutineStackFrame.owner(): CoroutineOwner<*>? =
474 if (this is CoroutineOwner<*>) this else callerFrame?.owner()
475
476 // Not guarded by the lock at all, does not really affect consistency
477 internal fun <T> probeCoroutineCreated(completion: Continuation<T>): Continuation<T> {
478 if (!isInstalled) return completion
479 // See DebugProbes.ignoreCoroutinesWithEmptyContext for the additional details.
480 if (ignoreCoroutinesWithEmptyContext && completion.context === EmptyCoroutineContext) return completion
481 /*
482 * If completion already has an owner, it means that we are in scoped coroutine (coroutineScope, withContext etc.),
483 * then piggyback on its already existing owner and do not replace completion
484 */
485 val owner = completion.owner()
486 if (owner != null) return completion
487 /*
488 * Here we replace completion with a sequence of StackTraceFrame objects
489 * which represents creation stacktrace, thus making stacktrace recovery mechanism
490 * even more verbose (it will attach coroutine creation stacktrace to all exceptions),
491 * and then using CoroutineOwner completion as unique identifier of coroutineSuspended/resumed calls.
492 */
493 val frame = if (enableCreationStackTraces) {
494 sanitizeStackTrace(Exception()).toStackTraceFrame()
495 } else {
496 null
497 }
498 return createOwner(completion, frame)
499 }
500
501 private fun List<StackTraceElement>.toStackTraceFrame(): StackTraceFrame =
502 StackTraceFrame(
503 foldRight<StackTraceElement, StackTraceFrame?>(null) { frame, acc ->
504 StackTraceFrame(acc, frame)
505 }, ARTIFICIAL_FRAME
506 )
507
508 private fun <T> createOwner(completion: Continuation<T>, frame: StackTraceFrame?): Continuation<T> {
509 if (!isInstalled) return completion
510 val info = DebugCoroutineInfoImpl(completion.context, frame, sequenceNumber.incrementAndGet())
511 val owner = CoroutineOwner(completion, info)
512 capturedCoroutinesMap[owner] = true
513 if (!isInstalled) capturedCoroutinesMap.clear()
514 return owner
515 }
516
517 // Not guarded by the lock at all, does not really affect consistency
518 private fun probeCoroutineCompleted(owner: CoroutineOwner<*>) {
519 capturedCoroutinesMap.remove(owner)
520 /*
521 * This removal is a guard against improperly implemented CoroutineStackFrame
522 * and bugs in the compiler.
523 */
524 val caller = owner.info.lastObservedFrame?.realCaller() ?: return
525 callerInfoCache.remove(caller)
526 }
527
528 /**
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 public class CoroutineOwner<T> internal constructor(
533 @JvmField internal val delegate: Continuation<T>,
534 // Used by the IDEA debugger via reflection and must be kept binary-compatible, see KTIJ-24102
535 @JvmField public val info: DebugCoroutineInfoImpl
536 ) : Continuation<T> by delegate, CoroutineStackFrame {
537 private val frame get() = info.creationStackBottom
538
539 override val callerFrame: CoroutineStackFrame?
540 get() = frame?.callerFrame
541
542 override fun getStackTraceElement(): StackTraceElement? = frame?.getStackTraceElement()
543
544 override fun resumeWith(result: Result<T>) {
545 probeCoroutineCompleted(this)
546 delegate.resumeWith(result)
547 }
548
549 override fun toString(): String = delegate.toString()
550 }
551
552 private fun <T : Throwable> sanitizeStackTrace(throwable: T): List<StackTraceElement> {
553 val stackTrace = throwable.stackTrace
554 val size = stackTrace.size
555 val traceStart = 1 + stackTrace.indexOfLast { it.className == "kotlin.coroutines.jvm.internal.DebugProbesKt" }
556
557 if (!sanitizeStackTraces) {
558 return List(size - traceStart) { stackTrace[it + traceStart] }
559 }
560
561 /*
562 * Trim intervals of internal methods from the stacktrace (bounds are excluded from trimming)
563 * E.g. for sequence [e, i1, i2, i3, e, i4, e, i5, i6, i7]
564 * output will be [e, i1, i3, e, i4, e, i5, i7]
565 *
566 * If an interval of internal methods ends in a synthetic method, the outermost non-synthetic method in that
567 * interval will also be included.
568 */
569 val result = ArrayList<StackTraceElement>(size - traceStart + 1)
570 var i = traceStart
571 while (i < size) {
572 if (stackTrace[i].isInternalMethod) {
573 result += stackTrace[i] // we include the boundary of the span in any case
574 // first index past the end of the span of internal methods that starts from `i`
575 var j = i + 1
576 while (j < size && stackTrace[j].isInternalMethod) {
577 ++j
578 }
579 // index of the last non-synthetic internal methods in this span, or `i` if there are no such methods
580 var k = j - 1
581 while (k > i && stackTrace[k].fileName == null) {
582 k -= 1
583 }
584 if (k > i && k < j - 1) {
585 /* there are synthetic internal methods at the end of this span, but there is a non-synthetic method
586 after `i`, so we include it. */
587 result += stackTrace[k]
588 }
589 result += stackTrace[j - 1] // we include the other boundary of this span in any case, too
590 i = j
591 } else {
592 result += stackTrace[i]
593 ++i
594 }
595 }
596 return result
597 }
598
599 private val StackTraceElement.isInternalMethod: Boolean get() = className.startsWith("kotlinx.coroutines")
600 }
601
reprnull602 private fun String.repr(): String = buildString {
603 append('"')
604 for (c in this@repr) {
605 when (c) {
606 '"' -> append("\\\"")
607 '\\' -> append("\\\\")
608 '\b' -> append("\\b")
609 '\n' -> append("\\n")
610 '\r' -> append("\\r")
611 '\t' -> append("\\t")
612 else -> append(c)
613 }
614 }
615 append('"')
616 }
617