<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