• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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