Basic exception stacktrace recovery mechanism
* Implement CoroutineStackFrame in CancellableContinuationImpl, DispatchedContinuation and ScopeCoroutine
* On coroutine resumption try to reflectively instantiate exception instance of the same type, but with augmented stacktrace
* Recover stacktrace by walking over CoroutineStackFrame
* Recover stacktrace on fast-path exceptions without CoroutineStackFrame walking to provide more context to an exception
* Unwrap exceptions when doing aggregation in JobSupport
* Add kill-switch to disable stacktrace recovery, introduce method to recover stacktrace on the exceptional fast-path
* Add `suspendCoroutineOrReturn` on exceptional fast-path in await in order to provide "real" stacktrace
Design rationale:
All recovery of *suspended* continuations takes place in Dispatched.kt file, the only place where all calls to "resume*" ends up, so we don't have to remember about stacktrace recovery in every primitive we are implementing. But to provide more accurate stacktraces we *have to* recover it on every fast-path for better debuggability.
Fixes #493
diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt
index 152f378..d7b990c 100644
--- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt
+++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt
@@ -50,10 +50,12 @@
public static synthetic fun tryResume$default (Lkotlinx/coroutines/CancellableContinuation;Ljava/lang/Object;Ljava/lang/Object;ILjava/lang/Object;)Ljava/lang/Object;
}
-public class kotlinx/coroutines/CancellableContinuationImpl : java/lang/Runnable, kotlinx/coroutines/CancellableContinuation {
+public class kotlinx/coroutines/CancellableContinuationImpl : java/lang/Runnable, kotlin/coroutines/jvm/internal/CoroutineStackFrame, kotlinx/coroutines/CancellableContinuation {
public fun <init> (Lkotlin/coroutines/Continuation;I)V
public fun completeResume (Ljava/lang/Object;)V
+ public fun getCallerFrame ()Lkotlin/coroutines/jvm/internal/CoroutineStackFrame;
public fun getContext ()Lkotlin/coroutines/CoroutineContext;
+ public fun getStackTraceElement ()Ljava/lang/StackTraceElement;
public fun getSuccessfulResult (Ljava/lang/Object;)Ljava/lang/Object;
public fun initCancellability ()V
protected fun nameString ()Ljava/lang/String;
diff --git a/common/kotlinx-coroutines-core-common/src/CancellableContinuation.kt b/common/kotlinx-coroutines-core-common/src/CancellableContinuation.kt
index b50ca7a..5c5d088 100644
--- a/common/kotlinx-coroutines-core-common/src/CancellableContinuation.kt
+++ b/common/kotlinx-coroutines-core-common/src/CancellableContinuation.kt
@@ -218,10 +218,15 @@
internal open class CancellableContinuationImpl<in T>(
delegate: Continuation<T>,
resumeMode: Int
-) : AbstractContinuation<T>(delegate, resumeMode), CancellableContinuation<T>, Runnable {
+) : AbstractContinuation<T>(delegate, resumeMode), CancellableContinuation<T>, Runnable, CoroutineStackFrame {
public override val context: CoroutineContext = delegate.context
+ override val callerFrame: CoroutineStackFrame?
+ get() = delegate as? CoroutineStackFrame
+
+ override fun getStackTraceElement(): StackTraceElement? = null
+
override fun initCancellability() {
initParentJobInternal(delegate.context[Job])
}
diff --git a/common/kotlinx-coroutines-core-common/src/Dispatched.kt b/common/kotlinx-coroutines-core-common/src/Dispatched.kt
index 5de38ef..897b1e1 100644
--- a/common/kotlinx-coroutines-core-common/src/Dispatched.kt
+++ b/common/kotlinx-coroutines-core-common/src/Dispatched.kt
@@ -81,10 +81,12 @@
internal class DispatchedContinuation<in T>(
@JvmField val dispatcher: CoroutineDispatcher,
@JvmField val continuation: Continuation<T>
-) : DispatchedTask<T>(MODE_ATOMIC_DEFAULT), Continuation<T> by continuation {
+) : DispatchedTask<T>(MODE_ATOMIC_DEFAULT), CoroutineStackFrame, Continuation<T> by continuation {
@JvmField
@Suppress("PropertyName")
internal var _state: Any? = UNDEFINED
+ override val callerFrame: CoroutineStackFrame? = continuation as? CoroutineStackFrame
+ override fun getStackTraceElement(): StackTraceElement? = null
@JvmField // pre-cached value to avoid ctx.fold on every resumption
internal val countOrElement = threadContextElements(context)
@@ -167,7 +169,7 @@
@Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack
inline fun resumeUndispatchedWithException(exception: Throwable) {
withCoroutineContext(context, countOrElement) {
- continuation.resumeWithException(exception)
+ continuation.resumeWithStackTrace(exception)
}
}
@@ -190,7 +192,7 @@
internal fun <T> Continuation<T>.resumeCancellableWithException(exception: Throwable) = when (this) {
is DispatchedContinuation -> resumeCancellableWithException(exception)
- else -> resumeWithException(exception)
+ else -> resumeWithStackTrace(exception)
}
internal fun <T> Continuation<T>.resumeDirect(value: T) = when (this) {
@@ -199,8 +201,8 @@
}
internal fun <T> Continuation<T>.resumeDirectWithException(exception: Throwable) = when (this) {
- is DispatchedContinuation -> continuation.resumeWithException(exception)
- else -> resumeWithException(exception)
+ is DispatchedContinuation -> continuation.resumeWithStackTrace(exception)
+ else -> resumeWithStackTrace(exception)
}
internal abstract class DispatchedTask<in T>(
@@ -231,7 +233,7 @@
else {
val exception = getExceptionalResult(state)
if (exception != null)
- continuation.resumeWithException(exception)
+ continuation.resumeWithStackTrace(exception)
else
continuation.resume(getSuccessfulResult(state))
}
@@ -275,3 +277,7 @@
delegate.resumeMode(getSuccessfulResult(state), useMode)
}
}
+
+
+@Suppress("NOTHING_TO_INLINE")
+private inline fun Continuation<*>.resumeWithStackTrace(exception: Throwable) = resumeWith(Result.failure(recoverStackTrace(exception, this)))
diff --git a/common/kotlinx-coroutines-core-common/src/JobSupport.kt b/common/kotlinx-coroutines-core-common/src/JobSupport.kt
index b80ce21..3cdc81e 100644
--- a/common/kotlinx-coroutines-core-common/src/JobSupport.kt
+++ b/common/kotlinx-coroutines-core-common/src/JobSupport.kt
@@ -242,8 +242,9 @@
val seenExceptions = identitySet<Throwable>(exceptions.size)
var suppressed = false
for (exception in exceptions) {
- if (exception !== rootCause && exception !is CancellationException && seenExceptions.add(exception)) {
- rootCause.addSuppressedThrowable(exception)
+ val unwrapped = unwrap(exception)
+ if (unwrapped !== rootCause && unwrapped !is CancellationException && seenExceptions.add(exception)) {
+ rootCause.addSuppressedThrowable(unwrapped)
suppressed = true
}
}
@@ -1078,7 +1079,11 @@
val state = this.state
if (state !is Incomplete) {
// already complete -- just return result
- if (state is CompletedExceptionally) throw state.cause
+ if (state is CompletedExceptionally) { // Slow path to recover stacktrace
+ suspendCoroutineUninterceptedOrReturn<Unit> {
+ throw recoverStackTrace(state.cause, it)
+ }
+ }
return state
}
diff --git a/common/kotlinx-coroutines-core-common/src/channels/AbstractChannel.kt b/common/kotlinx-coroutines-core-common/src/channels/AbstractChannel.kt
index 395988f..48692f1 100644
--- a/common/kotlinx-coroutines-core-common/src/channels/AbstractChannel.kt
+++ b/common/kotlinx-coroutines-core-common/src/channels/AbstractChannel.kt
@@ -176,8 +176,8 @@
result === OFFER_SUCCESS -> true
// We should check for closed token on offer as well, otherwise offer won't be linearizable
// in the face of concurrent close()
- result === OFFER_FAILED -> throw closedForSend?.sendException ?: return false
- result is Closed<*> -> throw result.sendException
+ result === OFFER_FAILED -> throw closedForSend?.sendException?.let { recoverStackTrace(it) } ?: return false
+ result is Closed<*> -> throw recoverStackTrace(result.sendException)
else -> error("offerInternal returned $result")
}
}
@@ -408,7 +408,7 @@
when {
enqueueResult === ALREADY_SELECTED -> return
enqueueResult === ENQUEUE_FAILED -> {} // retry
- enqueueResult is Closed<*> -> throw enqueueResult.sendException
+ enqueueResult is Closed<*> -> throw recoverStackTrace(enqueueResult.sendException)
else -> error("performAtomicIfNotSelected(TryEnqueueSendDesc) returned $enqueueResult")
}
} else {
@@ -420,7 +420,7 @@
block.startCoroutineUnintercepted(receiver = this, completion = select.completion)
return
}
- offerResult is Closed<*> -> throw offerResult.sendException
+ offerResult is Closed<*> -> throw recoverStackTrace(offerResult.sendException)
else -> error("offerSelectInternal returned $offerResult")
}
}
@@ -574,7 +574,7 @@
@Suppress("UNCHECKED_CAST")
private fun receiveResult(result: Any?): E {
- if (result is Closed<*>) throw result.receiveException
+ if (result is Closed<*>) throw recoverStackTrace(result.receiveException)
return result as E
}
@@ -620,7 +620,7 @@
@Suppress("UNCHECKED_CAST")
private fun receiveOrNullResult(result: Any?): E? {
if (result is Closed<*>) {
- if (result.closeCause != null) throw result.closeCause
+ if (result.closeCause != null) throw recoverStackTrace(result.closeCause)
return null
}
return result as E
@@ -759,7 +759,7 @@
when {
pollResult === ALREADY_SELECTED -> return
pollResult === POLL_FAILED -> {} // retry
- pollResult is Closed<*> -> throw pollResult.receiveException
+ pollResult is Closed<*> -> throw recoverStackTrace(pollResult.receiveException)
else -> {
block.startCoroutineUnintercepted(pollResult as E, select.completion)
return
@@ -798,8 +798,9 @@
if (select.trySelect(null))
block.startCoroutineUnintercepted(null, select.completion)
return
- } else
- throw pollResult.closeCause
+ } else {
+ throw recoverStackTrace(pollResult.closeCause)
+ }
}
else -> {
// selected successfully
@@ -858,7 +859,7 @@
private fun hasNextResult(result: Any?): Boolean {
if (result is Closed<*>) {
- if (result.closeCause != null) throw result.receiveException
+ if (result.closeCause != null) recoverStackTrace(throw result.receiveException)
return false
}
return true
@@ -892,7 +893,7 @@
@Suppress("UNCHECKED_CAST")
override suspend fun next(): E {
val result = this.result
- if (result is Closed<*>) throw result.receiveException
+ if (result is Closed<*>) throw recoverStackTrace(result.receiveException)
if (result !== POLL_FAILED) {
this.result = POLL_FAILED
return result as E
@@ -944,10 +945,11 @@
}
override fun resumeReceiveClosed(closed: Closed<*>) {
- val token = if (closed.closeCause == null)
+ val token = if (closed.closeCause == null) {
cont.tryResume(false)
- else
- cont.tryResumeWithException(closed.receiveException)
+ } else {
+ cont.tryResumeWithException(recoverStackTrace(closed.receiveException, cont))
+ }
if (token != null) {
iterator.result = closed
cont.completeResume(token)
diff --git a/common/kotlinx-coroutines-core-common/src/internal/Exceptions.common.kt b/common/kotlinx-coroutines-core-common/src/internal/Exceptions.common.kt
new file mode 100644
index 0000000..46ac8e2
--- /dev/null
+++ b/common/kotlinx-coroutines-core-common/src/internal/Exceptions.common.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.internal
+
+import kotlin.coroutines.*
+
+/**
+ * Tries to recover stacktrace for given [exception] and [continuation].
+ * Stacktrace recovery tries to restore [continuation] stack frames using its debug metadata with [CoroutineStackFrame] API
+ * and then reflectively instantiate exception of given type with original exception as a cause and
+ * sets new stacktrace for wrapping exception.
+ * Some frames may be missing due to tail-call elimination.
+ *
+ * Works only on JVM with enabled debug-mode.
+ */
+internal expect fun <E: Throwable> recoverStackTrace(exception: E, continuation: Continuation<*>): E
+
+/**
+ * Tries to recover stacktrace for given [exception]. Used in non-suspendable points of awaiting.
+ * Stacktrace recovery tries to instantiate exception of given type with original exception as a cause.
+ * Wrapping exception will have proper stacktrace as it's instantiated in the right context.
+ *
+ * Works only on JVM with enabled debug-mode.
+ */
+internal expect fun <E: Throwable> recoverStackTrace(exception: E): E
+
+// Name conflict with recoverStackTrace
+@Suppress("NOTHING_TO_INLINE")
+internal expect suspend inline fun recoverAndThrow(exception: Throwable): Nothing
+
+/**
+ * The opposite of [recoverStackTrace].
+ * It is guaranteed that `unwrap(recoverStackTrace(e)) === e`
+ */
+internal expect fun <E: Throwable> unwrap(exception: E): E
+
+expect class StackTraceElement
+
+internal expect interface CoroutineStackFrame {
+ public val callerFrame: CoroutineStackFrame?
+ public fun getStackTraceElement(): StackTraceElement?
+}
+
+/**
+ * Marker that indicates that stacktrace of the exception should not be recovered.
+ * Currently internal, but may become public in the future
+ */
+internal interface NonRecoverableThrowable
diff --git a/common/kotlinx-coroutines-core-common/src/internal/Scopes.kt b/common/kotlinx-coroutines-core-common/src/internal/Scopes.kt
index 4c4f9dd..efa8f04 100644
--- a/common/kotlinx-coroutines-core-common/src/internal/Scopes.kt
+++ b/common/kotlinx-coroutines-core-common/src/internal/Scopes.kt
@@ -14,7 +14,9 @@
internal open class ScopeCoroutine<in T>(
context: CoroutineContext,
@JvmField val uCont: Continuation<T> // unintercepted continuation
-) : AbstractCoroutine<T>(context, true) {
+) : AbstractCoroutine<T>(context, true), CoroutineStackFrame {
+ final override val callerFrame: CoroutineStackFrame? get() = uCont as CoroutineStackFrame?
+ final override fun getStackTraceElement(): StackTraceElement? = null
override val defaultResumeMode: Int get() = MODE_DIRECT
@Suppress("UNCHECKED_CAST")
diff --git a/common/kotlinx-coroutines-core-common/test/TestBase.common.kt b/common/kotlinx-coroutines-core-common/test/TestBase.common.kt
index 335c748..7bfcf29 100644
--- a/common/kotlinx-coroutines-core-common/test/TestBase.common.kt
+++ b/common/kotlinx-coroutines-core-common/test/TestBase.common.kt
@@ -5,6 +5,7 @@
package kotlinx.coroutines
import kotlin.coroutines.*
+import kotlinx.coroutines.internal.*
public expect open class TestBase constructor() {
public val isStressTest: Boolean
@@ -23,13 +24,13 @@
)
}
-public class TestException(message: String? = null) : Throwable(message)
-public class TestException1(message: String? = null) : Throwable(message)
-public class TestException2(message: String? = null) : Throwable(message)
-public class TestException3(message: String? = null) : Throwable(message)
-public class TestRuntimeException(message: String? = null) : RuntimeException(message)
+public class TestException(message: String? = null) : Throwable(message), NonRecoverableThrowable
+public class TestException1(message: String? = null) : Throwable(message), NonRecoverableThrowable
+public class TestException2(message: String? = null) : Throwable(message), NonRecoverableThrowable
+public class TestException3(message: String? = null) : Throwable(message), NonRecoverableThrowable
+public class TestRuntimeException(message: String? = null) : RuntimeException(message), NonRecoverableThrowable
+public class RecoverableTestException(message: String? = null) : Throwable(message)
-// Wrap context to avoid fast-paths on dispatcher comparison
public fun wrapperDispatcher(context: CoroutineContext): CoroutineContext {
val dispatcher = context[ContinuationInterceptor] as CoroutineDispatcher
return object : CoroutineDispatcher() {
diff --git a/core/kotlinx-coroutines-core/src/Debug.kt b/core/kotlinx-coroutines-core/src/Debug.kt
index fc19fee..3796c40 100644
--- a/core/kotlinx-coroutines-core/src/Debug.kt
+++ b/core/kotlinx-coroutines-core/src/Debug.kt
@@ -13,6 +13,20 @@
public const val DEBUG_PROPERTY_NAME = "kotlinx.coroutines.debug"
/**
+ * Name of the boolean property that controls stacktrace recovery (enabled by default) on JVM.
+ * Stacktrace recovery is enabled if both debug and stacktrace recovery modes are enabled.
+ *
+ * Stacktrace recovery mode wraps every exception into the exception of the same type with original exception
+ * as cause, but with stacktrace of the current coroutine.
+ * Exception is instantiated using reflection by using no-arg, cause or cause and message constructor.
+ * Stacktrace is not recovered if exception is an instance of [CancellationException] or [NonRecoverableThrowable].
+ *
+ * This mechanism is currently supported for channels, [async], [launch], [coroutineScope], [supervisorScope]
+ * and [withContext] builders.
+ */
+internal const val STACKTRACE_RECOVERY_PROPERTY_NAME = "kotlinx.coroutines.stacktrace.recovery"
+
+/**
* Automatic debug configuration value for [DEBUG_PROPERTY_NAME]. See [newCoroutineContext][CoroutineScope.newCoroutineContext].
*/
public const val DEBUG_PROPERTY_VALUE_AUTO = "auto"
@@ -36,6 +50,8 @@
}
}
+internal val RECOVER_STACKTRACE = systemProp(STACKTRACE_RECOVERY_PROPERTY_NAME, true)
+
// internal debugging tools
internal actual val Any.hexAddress: String
diff --git a/core/kotlinx-coroutines-core/src/internal/Exceptions.kt b/core/kotlinx-coroutines-core/src/internal/Exceptions.kt
new file mode 100644
index 0000000..cab8e64
--- /dev/null
+++ b/core/kotlinx-coroutines-core/src/internal/Exceptions.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.internal
+
+import kotlinx.coroutines.*
+import java.util.*
+import kotlin.coroutines.*
+import kotlin.coroutines.intrinsics.*
+
+internal actual fun <E : Throwable> recoverStackTrace(exception: E): E {
+ if (recoveryDisabled(exception)) {
+ return exception
+ }
+
+ val copy = tryCopyException(exception) ?: return exception
+ return copy.sanitizeStackTrace()
+}
+
+private fun <E : Throwable> E.sanitizeStackTrace(): E {
+ val size = stackTrace.size
+
+ var lastIntrinsic = -1
+ for (i in 0 until size) {
+ val name = stackTrace[i].className
+ if ("kotlinx.coroutines.internal.ExceptionsKt" == name) {
+ lastIntrinsic = i
+ }
+ }
+
+ val startIndex = lastIntrinsic + 1
+ val trace = Array(size - lastIntrinsic) {
+ if (it == 0) {
+ artificialFrame("Current coroutine stacktrace")
+ } else {
+ stackTrace[startIndex + it - 1]
+ }
+ }
+
+ stackTrace = trace
+ return this
+}
+
+internal actual fun <E : Throwable> recoverStackTrace(exception: E, continuation: Continuation<*>): E {
+ if (recoveryDisabled(exception) || continuation !is CoroutineStackFrame) {
+ return exception
+ }
+
+ return recoverFromStackFrame(exception, continuation)
+}
+
+private fun <E : Throwable> recoverFromStackFrame(exception: E, continuation: CoroutineStackFrame): E {
+ val newException = tryCopyException(exception) ?: return exception
+ val stacktrace = createStackTrace(continuation)
+ if (stacktrace.isEmpty()) return exception
+ stacktrace.add(0, artificialFrame("Current coroutine stacktrace"))
+ newException.stackTrace = stacktrace.toTypedArray()
+ return newException
+}
+
+
+@Suppress("NOTHING_TO_INLINE")
+internal actual suspend inline fun recoverAndThrow(exception: Throwable): Nothing {
+ if (recoveryDisabled(exception)) throw exception
+ suspendCoroutineUninterceptedOrReturn<Nothing> {
+ if (it !is CoroutineStackFrame) throw exception
+ throw recoverFromStackFrame(exception, it)
+ }
+}
+
+internal actual fun <E : Throwable> unwrap(exception: E): E {
+ if (recoveryDisabled(exception)) {
+ return exception
+ }
+
+ val element = exception.stackTrace.firstOrNull() ?: return exception
+ if (element.isArtificial()) {
+ @Suppress("UNCHECKED_CAST")
+ return exception.cause as? E ?: exception
+ } else {
+ return exception
+ }
+}
+
+private fun <E : Throwable> recoveryDisabled(exception: E) =
+ !RECOVER_STACKTRACE || !DEBUG || exception is CancellationException || exception is NonRecoverableThrowable
+
+@Suppress("UNCHECKED_CAST")
+private fun <E : Throwable> tryCopyException(exception: E): E? {
+ /*
+ * Try to reflectively find constructor(), constructor(message, cause) or constructor(cause).
+ * Exceptions are shared among coroutines, so we should copy exception before recovering current stacktrace.
+ */
+ var newException: E? = null
+ try {
+ val constructors = exception.javaClass.constructors.sortedByDescending { it.parameterTypes.size }
+ for (constructor in constructors) {
+ val parameters = constructor.parameterTypes
+ if (parameters.size == 2 && parameters[0] == String::class.java && parameters[1] == Throwable::class.java) {
+ newException = constructor.newInstance(exception.message, exception) as E
+ } else if (parameters.size == 1 && parameters[0] == Throwable::class.java) {
+ newException = constructor.newInstance(exception) as E
+ } else if (parameters.isEmpty()) {
+ newException = (constructor.newInstance() as E).also { it.initCause(exception) }
+ }
+
+ if (newException != null) {
+ break
+ }
+ }
+ } catch (e: Exception) {
+ // Do nothing
+ }
+ return newException
+}
+
+private fun createStackTrace(continuation: CoroutineStackFrame): ArrayList<StackTraceElement> {
+ val stack = ArrayList<StackTraceElement>()
+ continuation.getStackTraceElement()?.let { stack.add(it) }
+
+ var last = continuation
+ while (true) {
+ last = (last as? CoroutineStackFrame)?.callerFrame ?: break
+ last.getStackTraceElement()?.let { stack.add(it) }
+ }
+ return stack
+}
+
+
+internal fun artificialFrame(message: String) = java.lang.StackTraceElement("\b\b\b($message", "\b", "\b", -1)
+internal fun StackTraceElement.isArtificial() = className.startsWith("\b\b\b")
+
+@Suppress("ACTUAL_WITHOUT_EXPECT")
+actual typealias CoroutineStackFrame = kotlin.coroutines.jvm.internal.CoroutineStackFrame
+
+actual typealias StackTraceElement = java.lang.StackTraceElement
diff --git a/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryInHierarchiesTest.kt b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryInHierarchiesTest.kt
new file mode 100644
index 0000000..f243add
--- /dev/null
+++ b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryInHierarchiesTest.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.exceptions
+
+import kotlinx.coroutines.*
+import org.junit.Test
+import kotlin.test.*
+
+class StackTraceRecoveryInHierarchiesTest : TestBase() {
+
+ @Test
+ fun testNestedAsync() = runTest {
+ val rootAsync = async(NonCancellable) {
+ expect(1)
+
+ // Just a noise for unwrapping
+ async {
+ expect(2)
+ delay(Long.MAX_VALUE)
+ }
+
+ // Do not catch, fail on cancellation
+ async {
+ expect(3)
+ async {
+ expect(4)
+ delay(Long.MAX_VALUE)
+ }
+
+ async {
+ expect(5)
+ // 1) await(), catch, verify and rethrow
+ try {
+ val nested = async {
+ expect(6)
+ throw RecoverableTestException()
+ }
+
+ nested.awaitNested()
+ } catch (e: RecoverableTestException) {
+ expect(7)
+ e.verifyException(
+ "await\$suspendImpl",
+ "awaitNested",
+ "\$testNestedAsync\$1\$rootAsync\$1\$2\$2.invokeSuspend"
+ )
+ // Just rethrow it
+ throw e
+ }
+ }
+ }
+ }
+
+ try {
+ rootAsync.awaitRootLevel()
+ } catch (e: RecoverableTestException) {
+ e.verifyException("await\$suspendImpl", "awaitRootLevel")
+ finish(8)
+ }
+ }
+
+ private suspend fun Deferred<*>.awaitRootLevel() {
+ await()
+ assertTrue(true)
+ }
+
+ private suspend fun Deferred<*>.awaitNested() {
+ await()
+ assertTrue(true)
+ }
+
+ private fun RecoverableTestException.verifyException(vararg expectedTraceElements: String) {
+ // It is "recovered" only once
+ assertEquals(1, depth())
+ val stacktrace = stackTrace.map { it.methodName }.toSet()
+ assertTrue(expectedTraceElements.all { stacktrace.contains(it) })
+ }
+
+ private fun Throwable.depth(): Int {
+ val cause = cause ?: return 0
+ return 1 + cause.depth()
+ }
+}
diff --git a/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryTest.kt b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryTest.kt
new file mode 100644
index 0000000..d47ced7
--- /dev/null
+++ b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryTest.kt
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.exceptions
+
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.*
+import org.junit.Test
+import java.io.*
+import java.util.concurrent.*
+import kotlin.test.*
+
+/*
+ * All stacktrace validation skips line numbers
+ */
+class StackTraceRecoveryTest : TestBase() {
+
+ @Test
+ fun testAsync() = runTest {
+ fun createDeferred(depth: Int): Deferred<*> {
+ return if (depth == 0) {
+ async(coroutineContext + NonCancellable) {
+ throw ExecutionException(null)
+ }
+ } else {
+ createDeferred(depth - 1)
+ }
+ }
+
+ val deferred = createDeferred(3)
+ val traces = listOf(
+ "java.util.concurrent.ExecutionException\n" +
+ "\t(Current coroutine stacktrace)\n" +
+ "\tat kotlinx/coroutines/DeferredCoroutine.await\$suspendImpl(Builders.common.kt:99)\n" +
+ "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.oneMoreNestedMethod(StackTraceRecoveryTest.kt:49)\n" +
+ "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.nestedMethod(StackTraceRecoveryTest.kt:44)\n" +
+ "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$testAsync\$1.invokeSuspend(StackTraceRecoveryTest.kt:17)\n",
+ "Caused by: java.util.concurrent.ExecutionException\n" +
+ "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testAsync\$1\$1\$1.invokeSuspend(StackTraceRecoveryTest.kt:21)\n" +
+ "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n"
+ )
+ nestedMethod(deferred, traces)
+ deferred.join()
+ }
+
+ @Test
+ fun testCompletedAsync() = runTest {
+ val deferred = async(coroutineContext + NonCancellable) {
+ throw ExecutionException(null)
+ }
+
+ deferred.join()
+ val stacktrace = listOf(
+ "java.util.concurrent.ExecutionException\n" +
+ "\t(Current coroutine stacktrace)\n" +
+ "\tat kotlinx/coroutines/DeferredCoroutine.await\$suspendImpl(Builders.common.kt:99)\n" +
+ "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.oneMoreNestedMethod(StackTraceRecoveryTest.kt:81)\n" +
+ "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.nestedMethod(StackTraceRecoveryTest.kt:75)\n" +
+ "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$testCompletedAsync\$1.invokeSuspend(StackTraceRecoveryTest.kt:71)",
+ "Caused by: java.util.concurrent.ExecutionException\n" +
+ "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testCompletedAsync\$1\$deferred\$1.invokeSuspend(StackTraceRecoveryTest.kt:44)\n" +
+ "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)"
+ )
+ nestedMethod(deferred, stacktrace)
+ }
+
+ private suspend fun nestedMethod(deferred: Deferred<*>, traces: List<String>) {
+ oneMoreNestedMethod(deferred, traces)
+ assertTrue(true) // Prevent tail-call optimization
+ }
+
+ private suspend fun oneMoreNestedMethod(deferred: Deferred<*>, traces: List<String>) {
+ try {
+ deferred.await()
+ expectUnreached()
+ } catch (e: ExecutionException) {
+ verifyStackTrace(e, traces)
+ }
+ }
+
+ @Test
+ fun testReceiveFromChannel() = runTest {
+ val channel = Channel<Int>()
+ val job = launch {
+ expect(2)
+ channel.close(IllegalArgumentException())
+ }
+
+ expect(1)
+ channelNestedMethod(
+ channel, listOf(
+ "java.lang.IllegalArgumentException\n" +
+ "\t(Current coroutine stacktrace)\n" +
+ "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.channelNestedMethod(StackTraceRecoveryTest.kt:110)\n" +
+ "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$testReceiveFromChannel\$1.invokeSuspend(StackTraceRecoveryTest.kt:89)",
+ "Caused by: java.lang.IllegalArgumentException\n" +
+ "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testReceiveFromChannel\$1\$job\$1.invokeSuspend(StackTraceRecoveryTest.kt:93)\n" +
+ "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n" +
+ "\tat kotlinx.coroutines.DispatchedTask\$DefaultImpls.run(Dispatched.kt:152)"
+ )
+ )
+ expect(3)
+ job.join()
+ finish(4)
+ }
+
+ @Test
+ fun testReceiveFromClosedChannel() = runTest {
+ val channel = Channel<Int>()
+ channel.close(IllegalArgumentException())
+ channelNestedMethod(
+ channel, listOf(
+ "java.lang.IllegalArgumentException\n" +
+ "\t(Current coroutine stacktrace)\n" +
+ "\tat kotlinx.coroutines.channels.AbstractChannel.receiveResult(AbstractChannel.kt:574)\n" +
+ "\tat kotlinx.coroutines.channels.AbstractChannel.receive(AbstractChannel.kt:567)\n" +
+ "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest.channelNestedMethod(StackTraceRecoveryTest.kt:117)\n" +
+ "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testReceiveFromClosedChannel\$1.invokeSuspend(StackTraceRecoveryTest.kt:111)\n" +
+ "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)",
+ "Caused by: java.lang.IllegalArgumentException\n" +
+ "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testReceiveFromClosedChannel\$1.invokeSuspend(StackTraceRecoveryTest.kt:110)"
+ )
+ )
+ }
+
+ private suspend fun channelNestedMethod(channel: Channel<Int>, traces: List<String>) {
+ try {
+ channel.receive()
+ expectUnreached()
+ } catch (e: IllegalArgumentException) {
+ verifyStackTrace(e, traces)
+ }
+ }
+
+ @Test
+ fun testWithContext() = runTest {
+ val deferred = async(NonCancellable, start = CoroutineStart.LAZY) {
+ throw RecoverableTestException()
+ }
+
+ outerMethod(deferred, listOf(
+ "kotlinx.coroutines.RecoverableTestException\n" +
+ "\t(Current coroutine stacktrace)\n" +
+ "\tat kotlinx/coroutines/DeferredCoroutine.await\$suspendImpl(Builders.common.kt:99)\n" +
+ "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.innerMethod(StackTraceRecoveryTest.kt:158)\n" +
+ "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$outerMethod\$2.invokeSuspend(StackTraceRecoveryTest.kt:151)\n" +
+ "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.outerMethod(StackTraceRecoveryTest.kt:150)\n" +
+ "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$testWithContext\$1.invokeSuspend(StackTraceRecoveryTest.kt:141)\n",
+ "Caused by: kotlinx.coroutines.RecoverableTestException\n" +
+ "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testWithContext\$1\$deferred\$1.invokeSuspend(StackTraceRecoveryTest.kt:143)\n" +
+ "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n"))
+ deferred.join()
+ }
+
+ private suspend fun outerMethod(deferred: Deferred<Nothing>, traces: List<String>) {
+ withContext(Dispatchers.IO) {
+ innerMethod(deferred, traces)
+ }
+
+ assertTrue(true)
+ }
+
+ private suspend fun innerMethod(deferred: Deferred<Nothing>, traces: List<String>) {
+ try {
+ deferred.await()
+ } catch (e: RecoverableTestException) {
+ verifyStackTrace(e, traces)
+ }
+ }
+
+ @Test
+ fun testCoroutineScope() = runTest {
+ val deferred = async(NonCancellable, start = CoroutineStart.LAZY) {
+ throw RecoverableTestException()
+ }
+
+ outerScopedMethod(deferred, listOf(
+ "kotlinx.coroutines.RecoverableTestException\n" +
+ "\t(Current coroutine stacktrace)\n" +
+ "\tat kotlinx/coroutines/DeferredCoroutine.await\$suspendImpl(Builders.common.kt:99)\n" +
+ "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.innerMethod(StackTraceRecoveryTest.kt:158)\n" +
+ "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$outerScopedMethod\$2.invokeSuspend(StackTraceRecoveryTest.kt:151)\n" +
+ "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$testCoroutineScope\$1.invokeSuspend(StackTraceRecoveryTest.kt:141)\n",
+ "Caused by: kotlinx.coroutines.RecoverableTestException\n" +
+ "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testCoroutineScope\$1\$deferred\$1.invokeSuspend(StackTraceRecoveryTest.kt:143)\n" +
+ "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n"))
+ deferred.join()
+ }
+
+ private suspend fun outerScopedMethod(deferred: Deferred<Nothing>, traces: List<String>) = coroutineScope {
+ innerMethod(deferred, traces)
+ assertTrue(true)
+ }
+
+ private fun verifyStackTrace(e: Throwable, traces: List<String>) {
+ val stacktrace = toStackTrace(e)
+ traces.forEach {
+ assertTrue(
+ stacktrace.trimStackTrace().contains(it.trimStackTrace()),
+ "\nExpected trace element:\n$it\n\nActual stacktrace:\n$stacktrace"
+ )
+ }
+
+ val causes = stacktrace.count("Caused by")
+ assertNotEquals(0, causes)
+ assertEquals(causes, traces.map { it.count("Caused by") }.sum())
+ }
+
+ private fun toStackTrace(t: Throwable): String {
+ val sw = StringWriter() as Writer
+ t.printStackTrace(PrintWriter(sw))
+ return sw.toString()
+ }
+
+ private fun String.trimStackTrace(): String {
+ return applyBackspace(trimIndent().replace(Regex(":[0-9]+"), "")
+ .replace("kotlinx_coroutines_core_main", "") // yay source sets
+ .replace("kotlinx_coroutines_core", ""))
+ }
+
+ private fun applyBackspace(line: String): String {
+ val array = line.toCharArray()
+ val stack = CharArray(array.size)
+ var stackSize = -1
+ for (c in array) {
+ if (c != '\b') {
+ stack[++stackSize] = c
+ } else {
+ --stackSize
+ }
+ }
+
+ return String(stack, 0, stackSize)
+ }
+
+ private fun String.count(substring: String): Int = split(substring).size - 1
+}
diff --git a/core/kotlinx-coroutines-core/test/exceptions/SuppresionTests.kt b/core/kotlinx-coroutines-core/test/exceptions/SuppresionTests.kt
index ed021b5..b4527c6 100644
--- a/core/kotlinx-coroutines-core/test/exceptions/SuppresionTests.kt
+++ b/core/kotlinx-coroutines-core/test/exceptions/SuppresionTests.kt
@@ -5,15 +5,11 @@
package kotlinx.coroutines.exceptions
import kotlinx.coroutines.*
-import kotlinx.coroutines.exceptions.*
-import kotlinx.coroutines.selects.*
+import kotlinx.coroutines.channels.*
import java.io.*
import kotlin.coroutines.*
import kotlin.test.*
-/*
- * Set of counterparts to common tests which check suppressed exceptions
- */
class SuppresionTests : TestBase() {
@Test
@@ -24,11 +20,11 @@
}
expect(1)
- deferred.cancel(IOException())
+ deferred.cancel(TestException("Message"))
try {
deferred.await()
- } catch (e: IOException) {
+ } catch (e: TestException) {
checkException<ArithmeticException>(e.suppressed[0])
finish(3)
}
@@ -62,13 +58,13 @@
coroutine.invokeOnCompletion(onCancelling = true) {
assertTrue(it is ArithmeticException)
- assertTrue(it!!.suppressed.isEmpty())
+ assertTrue(it.suppressed.isEmpty())
expect(6)
}
coroutine.invokeOnCompletion {
assertTrue(it is ArithmeticException)
- checkException<IOException>(it!!.suppressed[0])
+ checkException<IOException>(it.suppressed[0])
expect(8)
}
@@ -80,4 +76,28 @@
coroutine.resumeWithException(IOException())
finish(10)
}
+
+ @Test
+ fun testExceptionUnwrapping() = runTest {
+ val channel = Channel<Int>()
+
+ val deferred = async(NonCancellable) {
+ launch {
+ while (true) channel.send(1)
+ }
+
+ launch {
+ val exception = RecoverableTestException()
+ channel.cancel(exception)
+ throw exception
+ }
+ }
+
+ try {
+ deferred.await()
+ } catch (e: RecoverableTestException) {
+ assertTrue(e.suppressed.isEmpty())
+ assertTrue(e.cause!!.suppressed.isEmpty())
+ }
+ }
}
\ No newline at end of file
diff --git a/core/kotlinx-coroutines-core/test/exceptions/WithContextExceptionHandlingTest.kt b/core/kotlinx-coroutines-core/test/exceptions/WithContextExceptionHandlingTest.kt
index 46f04b0..de9f6ca 100644
--- a/core/kotlinx-coroutines-core/test/exceptions/WithContextExceptionHandlingTest.kt
+++ b/core/kotlinx-coroutines-core/test/exceptions/WithContextExceptionHandlingTest.kt
@@ -26,11 +26,11 @@
fun testCancellation() = runTest {
/*
* context cancelled without cause
- * code itself throws ISE
- * Result: ISE
+ * code itself throws TE2
+ * Result: TE2
*/
- runCancellation(null, IllegalStateException()) { e ->
- assertTrue(e is IllegalStateException)
+ runCancellation(null, TestException2()) { e ->
+ assertTrue(e is TestException2)
assertNull(e.cause)
val suppressed = e.suppressed
assertTrue(suppressed.isEmpty())
@@ -40,30 +40,30 @@
@Test
fun testCancellationWithException() = runTest {
/*
- * context cancelled with IOE
- * block itself throws ISE
- * Result: IOE with suppressed ISE
+ * context cancelled with TE
+ * block itself throws TE2
+ * Result: TE with suppressed TE2
*/
- val cancellationCause = IOException()
- runCancellation(cancellationCause, IllegalStateException()) { e ->
- assertTrue(e is IOException)
+ val cancellationCause = TestException()
+ runCancellation(cancellationCause, TestException2()) { e ->
+ assertTrue(e is TestException)
assertNull(e.cause)
val suppressed = e.suppressed
assertEquals(suppressed.size, 1)
- assertTrue(suppressed[0] is IllegalStateException)
+ assertTrue(suppressed[0] is TestException2)
}
}
@Test
fun testSameException() = runTest {
/*
- * context cancelled with ISE
- * block itself throws the same ISE
- * Result: ISE
+ * context cancelled with TE
+ * block itself throws the same TE
+ * Result: TE
*/
- val cancellationCause = IllegalStateException()
+ val cancellationCause = TestException()
runCancellation(cancellationCause, cancellationCause) { e ->
- assertTrue(e is IllegalStateException)
+ assertTrue(e is TestException)
assertNull(e.cause)
val suppressed = e.suppressed
assertTrue(suppressed.isEmpty())
@@ -89,12 +89,12 @@
@Test
fun testSameCancellationWithException() = runTest {
/*
- * context cancelled with CancellationException(IOE)
- * block itself throws the same IOE
- * Result: IOE
+ * context cancelled with CancellationException(TE)
+ * block itself throws the same TE
+ * Result: TE
*/
val cancellationCause = CancellationException()
- val exception = IOException()
+ val exception = TestException()
cancellationCause.initCause(exception)
runCancellation(cancellationCause, exception) { e ->
assertSame(exception, e)
@@ -106,13 +106,13 @@
@Test
fun testConflictingCancellation() = runTest {
/*
- * context cancelled with ISE
- * block itself throws CE(IOE)
- * Result: ISE (because cancellation exception is always ignored and not handled)
+ * context cancelled with TE
+ * block itself throws CE(TE)
+ * Result: TE (because cancellation exception is always ignored and not handled)
*/
- val cancellationCause = IllegalStateException()
+ val cancellationCause = TestException()
val thrown = CancellationException()
- thrown.initCause(IOException())
+ thrown.initCause(TestException())
runCancellation(cancellationCause, thrown) { e ->
assertSame(cancellationCause, e)
assertTrue(e.suppressed.isEmpty())
@@ -122,11 +122,11 @@
@Test
fun testConflictingCancellation2() = runTest {
/*
- * context cancelled with ISE
+ * context cancelled with TE
* block itself throws CE
- * Result: ISE
+ * Result: TE
*/
- val cancellationCause = IllegalStateException()
+ val cancellationCause = TestException()
val thrown = CancellationException()
runCancellation(cancellationCause, thrown) { e ->
assertSame(cancellationCause, e)
@@ -161,9 +161,9 @@
@Test
fun testThrowingCancellationWithCause() = runTest {
- // Exception are never unwrapped, so if CE(IOE) is thrown then it is the cancellation cause
+ // Exception are never unwrapped, so if CE(TE) is thrown then it is the cancellation cause
val thrown = CancellationException()
- thrown.initCause(IOException())
+ thrown.initCause(TestException())
runThrowing(thrown) { e ->
assertSame(thrown, e)
}
@@ -179,7 +179,7 @@
@Test
fun testCancelWithCause() = runTest {
- val cause = IOException()
+ val cause = TestException()
runOnlyCancellation(cause) { e ->
assertSame(cause, e)
assertTrue(e.suppressed.isEmpty())
@@ -206,7 +206,7 @@
}
private suspend fun runCancellation(
- cancellationCause: Exception?,
+ cancellationCause: Throwable?,
thrownException: Throwable,
exceptionChecker: (Throwable) -> Unit
) {
diff --git a/core/kotlinx-coroutines-core/test/test/TestCoroutineContextTest.kt b/core/kotlinx-coroutines-core/test/test/TestCoroutineContextTest.kt
index c5145da..25b9091 100644
--- a/core/kotlinx-coroutines-core/test/test/TestCoroutineContextTest.kt
+++ b/core/kotlinx-coroutines-core/test/test/TestCoroutineContextTest.kt
@@ -269,7 +269,7 @@
@Test
fun testExceptionHandlingWithLaunchingChildCoroutines() = withTestContext(injectedContext) {
val delay = 1000L
- val expectedError = IllegalAccessError("hello")
+ val expectedError = TestException("hello")
val expectedValue = 12
launch {
@@ -299,7 +299,7 @@
@Test
fun testExceptionHandlingWithAsyncAndWaitForException() = withTestContext(injectedContext) {
val delay = 1000L
- val expectedError = IllegalAccessError("hello")
+ val expectedError = TestException("hello")
val expectedValue = 12
val result = async {
@@ -330,7 +330,7 @@
@Test
fun testExceptionHandlingWithRunBlockingAndWaitForException() = withTestContext(injectedContext) {
val delay = 1000L
- val expectedError = IllegalAccessError("hello")
+ val expectedError = TestException("hello")
val expectedValue = 12
try {
diff --git a/js/kotlinx-coroutines-core-js/src/internal/Exceptions.kt b/js/kotlinx-coroutines-core-js/src/internal/Exceptions.kt
new file mode 100644
index 0000000..e0435af
--- /dev/null
+++ b/js/kotlinx-coroutines-core-js/src/internal/Exceptions.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.internal
+
+import kotlin.coroutines.*
+
+internal actual fun <E: Throwable> recoverStackTrace(exception: E, continuation: Continuation<*>): E = exception
+internal actual fun <E: Throwable> recoverStackTrace(exception: E): E = exception
+internal actual suspend inline fun recoverAndThrow(exception: Throwable): Nothing = throw exception
+
+internal actual fun <E : Throwable> unwrap(exception: E): E = exception
+
+@Suppress("unused")
+internal actual interface CoroutineStackFrame {
+ public actual val callerFrame: CoroutineStackFrame?
+ public actual fun getStackTraceElement(): StackTraceElement?
+}
+
+actual typealias StackTraceElement = Any
diff --git a/native/kotlinx-coroutines-core-native/src/internal/Exceptions.kt b/native/kotlinx-coroutines-core-native/src/internal/Exceptions.kt
new file mode 100644
index 0000000..923a9b1
--- /dev/null
+++ b/native/kotlinx-coroutines-core-native/src/internal/Exceptions.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.internal
+
+import kotlin.coroutines.*
+
+internal actual fun <E: Throwable> recoverStackTrace(exception: E, continuation: Continuation<*>): E = exception
+internal actual fun <E: Throwable> recoverStackTrace(exception: E): E = exception
+internal actual fun <E : Throwable> unwrap(exception: E): E = exception
+internal actual suspend inline fun recoverAndThrow(exception: Throwable): Nothing = throw exception
+
+@Suppress("unused")
+internal actual interface CoroutineStackFrame {
+ public actual val callerFrame: CoroutineStackFrame?
+ public actual fun getStackTraceElement(): StackTraceElement?
+}
+
+actual typealias StackTraceElement = Any