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