diff --git a/README.md b/README.md index e2e9364d07..72ddc6b266 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ GlobalScope.launch { * [core](core/README.md) — Kotlin/JVM implementation of common coroutines with additional features: * `Dispatchers.IO` dispatcher for blocking coroutines; * `Executor.asCoroutineDispatcher()` extension, custom thread pools, and more. +* [debug](core/README.md) — debug utilities for coroutines. + * `DebugProbes` API to probe, keep track of, print and dump active coroutines. * [js](js/README.md) — Kotlin/JS implementation of common coroutines with `Promise` support. * [native](native/README.md) — Kotlin/Native implementation of common coroutines with `runBlocking` single-threaded event loop. * [reactive](reactive/README.md) — modules that provide builders and iteration support for various reactive streams libraries: diff --git a/RELEASE.md b/RELEASE.md index 11c5af519b..3ab6c94de3 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -12,7 +12,7 @@ To release new `` of `kotlinx-coroutines`: `git merge origin/master` 4. Search & replace `` with `` across the project files. Should replace in: - * [`README.md`](README.md) + * [`README.md`](README.md) (native, core, test, debug, modules) * [`coroutines-guide.md`](docs/coroutines-guide.md) * [`gradle.properties`](gradle.properties) * [`ui/kotlinx-coroutines-android/example-app/gradle.properties`](ui/kotlinx-coroutines-android/example-app/gradle.properties) diff --git a/binary-compatibility-validator/build.gradle b/binary-compatibility-validator/build.gradle index 785848c142..2aa9e145da 100644 --- a/binary-compatibility-validator/build.gradle +++ b/binary-compatibility-validator/build.gradle @@ -13,6 +13,7 @@ dependencies { testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" testArtifacts project(':kotlinx-coroutines-core') + testArtifacts project(':kotlinx-coroutines-debug') testArtifacts project(':kotlinx-coroutines-reactive') testArtifacts project(':kotlinx-coroutines-reactor') 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 bbb4b1d998..9e6112d0cc 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 final class kotlinx/coroutines/CancellableContinuation$DefaultImpls { 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 (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; @@ -347,6 +349,7 @@ public class kotlinx/coroutines/JobSupport : kotlinx/coroutines/ChildJob, kotlin public fun plus (Lkotlinx/coroutines/Job;)Lkotlinx/coroutines/Job; public final fun registerSelectClause0 (Lkotlinx/coroutines/selects/SelectInstance;Lkotlin/jvm/functions/Function1;)V public final fun start ()Z + public final fun toDebugString ()Ljava/lang/String; public fun toString ()Ljava/lang/String; } diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt new file mode 100644 index 0000000000..4f63d7c3e1 --- /dev/null +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt @@ -0,0 +1,38 @@ +public final class kotlinx/coroutines/debug/CoroutineState { + public final fun component1 ()Lkotlin/coroutines/Continuation; + public final fun copy (Lkotlin/coroutines/Continuation;Lkotlin/coroutines/jvm/internal/CoroutineStackFrame;J)Lkotlinx/coroutines/debug/CoroutineState; + public static synthetic fun copy$default (Lkotlinx/coroutines/debug/CoroutineState;Lkotlin/coroutines/Continuation;Lkotlin/coroutines/jvm/internal/CoroutineStackFrame;JILjava/lang/Object;)Lkotlinx/coroutines/debug/CoroutineState; + public fun equals (Ljava/lang/Object;)Z + public final fun getContinuation ()Lkotlin/coroutines/Continuation; + public final fun getCreationStackTrace ()Ljava/util/List; + public final fun getJob ()Lkotlinx/coroutines/Job; + public final fun getJobOrNull ()Lkotlinx/coroutines/Job; + public final fun getState ()Lkotlinx/coroutines/debug/State; + public fun hashCode ()I + public final fun lastObservedStackTrace ()Ljava/util/List; + public fun toString ()Ljava/lang/String; +} + +public final class kotlinx/coroutines/debug/DebugProbes { + public static final field INSTANCE Lkotlinx/coroutines/debug/DebugProbes; + public final fun dumpCoroutines (Ljava/io/PrintStream;)V + public static synthetic fun dumpCoroutines$default (Lkotlinx/coroutines/debug/DebugProbes;Ljava/io/PrintStream;ILjava/lang/Object;)V + public final fun dumpCoroutinesState ()Ljava/util/List; + public final fun getSanitizeStackTraces ()Z + public final fun hierarchyToString (Lkotlinx/coroutines/Job;)Ljava/lang/String; + public final fun install ()V + public final fun printHierarchy (Lkotlinx/coroutines/Job;Ljava/io/PrintStream;)V + public static synthetic fun printHierarchy$default (Lkotlinx/coroutines/debug/DebugProbes;Lkotlinx/coroutines/Job;Ljava/io/PrintStream;ILjava/lang/Object;)V + public final fun setSanitizeStackTraces (Z)V + public final fun uninstall ()V + public final fun withDebugProbes (Lkotlin/jvm/functions/Function0;)V +} + +public final class kotlinx/coroutines/debug/State : java/lang/Enum { + public static final field CREATED Lkotlinx/coroutines/debug/State; + public static final field RUNNING Lkotlinx/coroutines/debug/State; + public static final field SUSPENDED Lkotlinx/coroutines/debug/State; + public static fun valueOf (Ljava/lang/String;)Lkotlinx/coroutines/debug/State; + public static fun values ()[Lkotlinx/coroutines/debug/State; +} + diff --git a/build.gradle b/build.gradle index ea4b83038e..2bda08cea0 100644 --- a/build.gradle +++ b/build.gradle @@ -40,7 +40,7 @@ buildscript { classpath "com.moowork.gradle:gradle-node-plugin:$gradle_node_version" // JMH plugins - classpath "com.github.jengelman.gradle.plugins:shadow:2.0.2" + classpath "com.github.jengelman.gradle.plugins:shadow:4.0.2" classpath "me.champeau.gradle:jmh-gradle-plugin:0.4.7" classpath "net.ltgt.gradle:gradle-apt-plugin:0.10" } diff --git a/common/kotlinx-coroutines-core-common/src/AbstractContinuation.kt b/common/kotlinx-coroutines-core-common/src/AbstractContinuation.kt index ef6f398efd..961f15e322 100644 --- a/common/kotlinx-coroutines-core-common/src/AbstractContinuation.kt +++ b/common/kotlinx-coroutines-core-common/src/AbstractContinuation.kt @@ -134,7 +134,7 @@ internal abstract class AbstractContinuation( if (trySuspend()) return COROUTINE_SUSPENDED // otherwise, onCompletionInternal was already invoked & invoked tryResume, and the result is in the state val state = this.state - if (state is CompletedExceptionally) throw state.cause + if (state is CompletedExceptionally) throw recoverStackTrace(state.cause, this) return getSuccessfulResult(state) } diff --git a/common/kotlinx-coroutines-core-common/src/CancellableContinuation.kt b/common/kotlinx-coroutines-core-common/src/CancellableContinuation.kt index b50ca7a79a..5c5d0881a6 100644 --- a/common/kotlinx-coroutines-core-common/src/CancellableContinuation.kt +++ b/common/kotlinx-coroutines-core-common/src/CancellableContinuation.kt @@ -218,10 +218,15 @@ private class DisposeOnCancel(private val handle: DisposableHandle) : CancelHand internal open class CancellableContinuationImpl( delegate: Continuation, resumeMode: Int -) : AbstractContinuation(delegate, resumeMode), CancellableContinuation, Runnable { +) : AbstractContinuation(delegate, resumeMode), CancellableContinuation, 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 8a788ecc4d..cf9198847d 100644 --- a/common/kotlinx-coroutines-core-common/src/Dispatched.kt +++ b/common/kotlinx-coroutines-core-common/src/Dispatched.kt @@ -82,10 +82,12 @@ internal object UndispatchedEventLoop { internal class DispatchedContinuation( @JvmField val dispatcher: CoroutineDispatcher, @JvmField val continuation: Continuation -) : DispatchedTask(MODE_ATOMIC_DEFAULT), Continuation by continuation { +) : DispatchedTask(MODE_ATOMIC_DEFAULT), CoroutineStackFrame, Continuation 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) @@ -168,7 +170,7 @@ internal class DispatchedContinuation( @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) } } @@ -191,7 +193,7 @@ internal fun Continuation.resumeCancellable(value: T) = when (this) { internal fun Continuation.resumeCancellableWithException(exception: Throwable) = when (this) { is DispatchedContinuation -> resumeCancellableWithException(exception) - else -> resumeWithException(exception) + else -> resumeWithStackTrace(exception) } internal fun Continuation.resumeDirect(value: T) = when (this) { @@ -200,8 +202,8 @@ internal fun Continuation.resumeDirect(value: T) = when (this) { } internal fun Continuation.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( @@ -232,7 +234,7 @@ internal abstract class DispatchedTask( else { val exception = getExceptionalResult(state) if (exception != null) - continuation.resumeWithException(exception) + continuation.resumeWithStackTrace(exception) else continuation.resume(getSuccessfulResult(state)) } @@ -276,3 +278,9 @@ internal fun DispatchedTask.resume(delegate: Continuation, useMode: In delegate.resumeMode(getSuccessfulResult(state), useMode) } } + + +@Suppress("NOTHING_TO_INLINE") +internal 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 da4e261f8b..326cd88077 100644 --- a/common/kotlinx-coroutines-core-common/src/JobSupport.kt +++ b/common/kotlinx-coroutines-core-common/src/JobSupport.kt @@ -247,8 +247,9 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren val seenExceptions = identitySet(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(unwrapped)) { + rootCause.addSuppressedThrowable(unwrapped) suppressed = true } } @@ -929,7 +930,10 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // for nicer debugging public override fun toString(): String = - "${nameString()}{${stateString(state)}}@$hexAddress" + "${toDebugString()}@$hexAddress" + + @InternalCoroutinesApi + public fun toDebugString(): String = "${nameString()}{${stateString(state)}}" /** * @suppress **This is unstable API and it is subject to change.** @@ -1083,7 +1087,9 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren 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 + recoverAndThrow(state.cause) + } return state.unboxState() } diff --git a/common/kotlinx-coroutines-core-common/src/Timeout.kt b/common/kotlinx-coroutines-core-common/src/Timeout.kt index 5edea9fa19..d84ec10559 100644 --- a/common/kotlinx-coroutines-core-common/src/Timeout.kt +++ b/common/kotlinx-coroutines-core-common/src/Timeout.kt @@ -4,6 +4,7 @@ package kotlinx.coroutines +import kotlinx.coroutines.internal.* import kotlinx.coroutines.intrinsics.* import kotlinx.coroutines.selects.* import kotlin.coroutines.* @@ -80,9 +81,10 @@ private fun setupTimeout( private open class TimeoutCoroutine( @JvmField val time: Long, @JvmField val uCont: Continuation // unintercepted continuation -) : AbstractCoroutine(uCont.context, active = true), Runnable, Continuation { +) : AbstractCoroutine(uCont.context, active = true), Runnable, Continuation, CoroutineStackFrame { override val defaultResumeMode: Int get() = MODE_DIRECT - + override val callerFrame: CoroutineStackFrame? get() = (uCont as? CoroutineStackFrame)?.callerFrame + override fun getStackTraceElement(): StackTraceElement? = (uCont as? CoroutineStackFrame)?.getStackTraceElement() @Suppress("LeakingThis", "Deprecation") override fun run() { cancel(TimeoutCancellationException(time, this)) diff --git a/common/kotlinx-coroutines-core-common/src/channels/AbstractChannel.kt b/common/kotlinx-coroutines-core-common/src/channels/AbstractChannel.kt index d25231be81..2fa60dbc7e 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 @@ internal abstract class AbstractSendChannel : SendChannel { 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 @@ internal abstract class AbstractSendChannel : SendChannel { 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 @@ internal abstract class AbstractSendChannel : SendChannel { 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 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel) throw result.receiveException + if (result is Closed<*>) throw recoverStackTrace(result.receiveException) return result as E } @@ -620,7 +620,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel) { - 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 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel 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 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel { // selected successfully @@ -858,7 +859,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel) { - if (result.closeCause != null) throw result.receiveException + if (result.closeCause != null) throw recoverStackTrace(result.receiveException) return false } return true @@ -892,7 +893,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel) 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 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel) { - 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/ProbesSupport.common.kt b/common/kotlinx-coroutines-core-common/src/internal/ProbesSupport.common.kt new file mode 100644 index 0000000000..1124ff313b --- /dev/null +++ b/common/kotlinx-coroutines-core-common/src/internal/ProbesSupport.common.kt @@ -0,0 +1,9 @@ +/* + * 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 expect inline fun probeCoroutineCreated(completion: Continuation): Continuation diff --git a/common/kotlinx-coroutines-core-common/src/internal/Scopes.kt b/common/kotlinx-coroutines-core-common/src/internal/Scopes.kt index 4c4f9dd486..56a32bc075 100644 --- a/common/kotlinx-coroutines-core-common/src/internal/Scopes.kt +++ b/common/kotlinx-coroutines-core-common/src/internal/Scopes.kt @@ -14,18 +14,27 @@ import kotlin.jvm.* internal open class ScopeCoroutine( context: CoroutineContext, @JvmField val uCont: Continuation // unintercepted continuation -) : AbstractCoroutine(context, true) { +) : AbstractCoroutine(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") internal override fun onCompletionInternal(state: Any?, mode: Int, suppressed: Boolean) { - if (state is CompletedExceptionally) - uCont.resumeUninterceptedWithExceptionMode(state.cause, mode) - else + if (state is CompletedExceptionally) { + val exception = if (mode == MODE_IGNORE) state.cause else recoverStackTrace(state.cause, uCont) + uCont.resumeUninterceptedWithExceptionMode(exception, mode) + } else { uCont.resumeUninterceptedMode(state as T, mode) + } } } +internal fun AbstractCoroutine<*>.tryRecover(exception: Throwable): Throwable { + val cont = (this as? ScopeCoroutine<*>)?.uCont ?: return exception + return recoverStackTrace(exception, cont) +} + internal class ContextScope(context: CoroutineContext) : CoroutineScope { override val coroutineContext: CoroutineContext = context } diff --git a/common/kotlinx-coroutines-core-common/src/internal/StackTraceRecovery.common.kt b/common/kotlinx-coroutines-core-common/src/internal/StackTraceRecovery.common.kt new file mode 100644 index 0000000000..8ce0fcd261 --- /dev/null +++ b/common/kotlinx-coroutines-core-common/src/internal/StackTraceRecovery.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 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 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 unwrap(exception: E): E + +internal 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/intrinsics/Undispatched.kt b/common/kotlinx-coroutines-core-common/src/intrinsics/Undispatched.kt index 5a30d5e3a8..5ebf953669 100644 --- a/common/kotlinx-coroutines-core-common/src/intrinsics/Undispatched.kt +++ b/common/kotlinx-coroutines-core-common/src/intrinsics/Undispatched.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines.intrinsics import kotlinx.coroutines.* +import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* @@ -14,8 +15,8 @@ import kotlin.coroutines.intrinsics.* * It does not use [ContinuationInterceptor] and does not update context of the current thread. */ internal fun (suspend () -> T).startCoroutineUnintercepted(completion: Continuation) { - startDirect(completion) { - startCoroutineUninterceptedOrReturn(completion) + startDirect(completion) { actualCompletion -> + startCoroutineUninterceptedOrReturn(actualCompletion) } } @@ -25,8 +26,8 @@ internal fun (suspend () -> T).startCoroutineUnintercepted(completion: Conti * It does not use [ContinuationInterceptor] and does not update context of the current thread. */ internal fun (suspend (R) -> T).startCoroutineUnintercepted(receiver: R, completion: Continuation) { - startDirect(completion) { - startCoroutineUninterceptedOrReturn(receiver, completion) + startDirect(completion) { actualCompletion -> + startCoroutineUninterceptedOrReturn(receiver, actualCompletion) } } @@ -36,9 +37,9 @@ internal fun (suspend (R) -> T).startCoroutineUnintercepted(receiver: R, * It does not use [ContinuationInterceptor], but updates the context of the current thread for the new coroutine. */ internal fun (suspend () -> T).startCoroutineUndispatched(completion: Continuation) { - startDirect(completion) { + startDirect(completion) { actualCompletion -> withCoroutineContext(completion.context, null) { - startCoroutineUninterceptedOrReturn(completion) + startCoroutineUninterceptedOrReturn(actualCompletion) } } } @@ -49,23 +50,29 @@ internal fun (suspend () -> T).startCoroutineUndispatched(completion: Contin * It does not use [ContinuationInterceptor], but updates the context of the current thread for the new coroutine. */ internal fun (suspend (R) -> T).startCoroutineUndispatched(receiver: R, completion: Continuation) { - startDirect(completion) { + startDirect(completion) { actualCompletion -> withCoroutineContext(completion.context, null) { - startCoroutineUninterceptedOrReturn(receiver, completion) + startCoroutineUninterceptedOrReturn(receiver, actualCompletion) } } } -private inline fun startDirect(completion: Continuation, block: () -> Any?) { +/** + * Starts given [block] immediately in the current stack-frame until first suspension point. + * This method supports debug probes and thus can intercept completion, thus completion is provide + * as the parameter of [block]. + */ +private inline fun startDirect(completion: Continuation, block: (Continuation) -> Any?) { + val actualCompletion = probeCoroutineCreated(completion) val value = try { - block() + block(actualCompletion) } catch (e: Throwable) { - completion.resumeWithException(e) + actualCompletion.resumeWithException(e) return } if (value !== COROUTINE_SUSPENDED) { @Suppress("UNCHECKED_CAST") - completion.resume(value as T) + actualCompletion.resume(value as T) } } @@ -126,8 +133,8 @@ private inline fun AbstractCoroutine.undispatchedResult( val state = state if (state is CompletedExceptionally) { when { - shouldThrow(state.cause) -> throw state.cause - result is CompletedExceptionally -> throw result.cause + shouldThrow(state.cause) -> throw tryRecover(state.cause) + result is CompletedExceptionally -> throw tryRecover(result.cause) else -> result } } else { diff --git a/common/kotlinx-coroutines-core-common/test/TestBase.common.kt b/common/kotlinx-coroutines-core-common/test/TestBase.common.kt index 335c7480a1..f705c79e73 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 expect open class TestBase constructor() { ) } -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) : RuntimeException(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/README.md b/core/README.md index 4f353a01d3..0386912b2a 100644 --- a/core/README.md +++ b/core/README.md @@ -5,5 +5,5 @@ Module name below corresponds to the artifact name in Maven/Gradle. ## Modules -* [kotlinx-coroutines-core](kotlinx-coroutines-core/README.md) -- core coroutine builders and synchronization primitives. - +* [kotlinx-coroutines-core](kotlinx-coroutines-core/README.md) — core coroutine builders and synchronization primitives. +* [kotlinx-coroutines-debug](kotlinx-coroutines-debug/README.md) — coroutines debug utilities. \ No newline at end of file diff --git a/core/kotlinx-coroutines-core/src/Debug.kt b/core/kotlinx-coroutines-core/src/Debug.kt index fc19feed67..a1f53cd776 100644 --- a/core/kotlinx-coroutines-core/src/Debug.kt +++ b/core/kotlinx-coroutines-core/src/Debug.kt @@ -12,6 +12,20 @@ import kotlinx.coroutines.internal.* */ 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]. */ @@ -27,6 +41,7 @@ public const val DEBUG_PROPERTY_VALUE_ON = "on" */ public const val DEBUG_PROPERTY_VALUE_OFF = "off" +@JvmField internal val DEBUG = systemProp(DEBUG_PROPERTY_NAME).let { value -> when (value) { DEBUG_PROPERTY_VALUE_AUTO, null -> CoroutineId::class.java.desiredAssertionStatus() @@ -36,6 +51,9 @@ internal val DEBUG = systemProp(DEBUG_PROPERTY_NAME).let { value -> } } +@JvmField +internal val RECOVER_STACKTRACES = systemProp(STACKTRACE_RECOVERY_PROPERTY_NAME, true) + // internal debugging tools internal actual val Any.hexAddress: String diff --git a/core/kotlinx-coroutines-core/src/internal/ExceptionsConstuctor.kt b/core/kotlinx-coroutines-core/src/internal/ExceptionsConstuctor.kt new file mode 100644 index 0000000000..5695060785 --- /dev/null +++ b/core/kotlinx-coroutines-core/src/internal/ExceptionsConstuctor.kt @@ -0,0 +1,45 @@ +/* + * 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 java.util.* +import java.util.concurrent.locks.* +import kotlin.concurrent.* + +private val cacheLock = ReentrantReadWriteLock() +// Replace it with ClassValue when Java 6 support is over +private val exceptionConstructors: WeakHashMap, (Throwable) -> Throwable?> = WeakHashMap() + +@Suppress("UNCHECKED_CAST") +internal fun tryCopyException(exception: E): E? { + val cachedCtor = cacheLock.read { + exceptionConstructors[exception.javaClass] + } + + if (cachedCtor != null) return cachedCtor(exception) as 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 ctor: ((Throwable) -> Throwable?)? = null + 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) { + ctor = { e -> runCatching { constructor.newInstance(e.message, e) as E }.getOrNull() } + break + } else if (parameters.size == 1 && parameters[0] == Throwable::class.java) { + ctor = { e -> runCatching { constructor.newInstance(e) as E }.getOrNull() } + break + } else if (parameters.isEmpty()) { + ctor = { e -> runCatching { constructor.newInstance() as E }.getOrNull()?.also { it.initCause(e) } } + break + } + } + + cacheLock.write { exceptionConstructors[exception.javaClass] = (ctor ?: { null }) } + return ctor?.invoke(exception) as E? +} diff --git a/core/kotlinx-coroutines-core/src/internal/ProbesSupport.kt b/core/kotlinx-coroutines-core/src/internal/ProbesSupport.kt new file mode 100644 index 0000000000..f3c548e008 --- /dev/null +++ b/core/kotlinx-coroutines-core/src/internal/ProbesSupport.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +@file:Suppress("NOTHING_TO_INLINE", "INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") + +package kotlinx.coroutines.internal + +import kotlin.coroutines.* +import kotlin.coroutines.jvm.internal.probeCoroutineCreated as probe + +internal actual inline fun probeCoroutineCreated(completion: Continuation): Continuation = probe(completion) \ No newline at end of file diff --git a/core/kotlinx-coroutines-core/src/internal/StackTraceRecovery.kt b/core/kotlinx-coroutines-core/src/internal/StackTraceRecovery.kt new file mode 100644 index 0000000000..9457130164 --- /dev/null +++ b/core/kotlinx-coroutines-core/src/internal/StackTraceRecovery.kt @@ -0,0 +1,215 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("UNCHECKED_CAST") + +package kotlinx.coroutines.internal + +import kotlinx.coroutines.* +import java.util.* +import kotlin.coroutines.* +import kotlin.coroutines.intrinsics.* + +internal actual fun recoverStackTrace(exception: E): E { + if (recoveryDisabled(exception)) { + return exception + } + // No unwrapping on continuation-less path: exception is not reported multiple times via slow paths + val copy = tryCopyException(exception) ?: return exception + return copy.sanitizeStackTrace() +} + +private fun E.sanitizeStackTrace(): E { + val stackTrace = stackTrace + val size = stackTrace.size + + val lastIntrinsic = stackTrace.frameIndex("kotlinx.coroutines.internal.StackTraceRecoveryKt") + val startIndex = lastIntrinsic + 1 + val endIndex = stackTrace.frameIndex("kotlin.coroutines.jvm.internal.BaseContinuationImpl") + val adjustment = if (endIndex == -1) 0 else size - endIndex + val trace = Array(size - lastIntrinsic - adjustment) { + if (it == 0) { + artificialFrame("Coroutine boundary") + } else { + stackTrace[startIndex + it - 1] + } + } + + setStackTrace(trace) + return this +} + +internal actual fun recoverStackTrace(exception: E, continuation: Continuation<*>): E { + if (recoveryDisabled(exception) || continuation !is CoroutineStackFrame) { + return exception + } + + return recoverFromStackFrame(exception, continuation) +} + +private fun recoverFromStackFrame(exception: E, continuation: CoroutineStackFrame): E { + /* + * Here we are checking whether exception has already recovered stacktrace. + * If so, we extract initial and merge recovered stacktrace and current one + */ + val (cause, recoveredStacktrace) = exception.causeAndStacktrace() + + // Try to create new exception of the same type and get stacktrace from continuation + val newException = tryCopyException(cause) ?: return exception + val stacktrace = createStackTrace(continuation) + if (stacktrace.isEmpty()) return exception + + // Merge if necessary + if (cause !== exception) { + mergeRecoveredTraces(recoveredStacktrace, stacktrace) + } + + // Take recovered stacktrace, merge it with existing one if necessary and return + return createFinalException(cause, newException, stacktrace) +} + +/* + * Here we partially copy original exception stackTrace to make current one much prettier. + * E.g. for + * ``` + * fun foo() = async { error(...) } + * suspend fun bar() = foo().await() + * ``` + * we would like to produce following exception: + * IllegalStateException + * at foo + * at kotlin.coroutines.resumeWith + * (Coroutine boundary) + * at bar + * ...real stackTrace... + * caused by "IllegalStateException" (original one) + */ +private fun createFinalException(cause: E, result: E, resultStackTrace: ArrayDeque): E { + resultStackTrace.addFirst(artificialFrame("Coroutine boundary")) + val causeTrace = cause.stackTrace + val size = causeTrace.frameIndex("kotlin.coroutines.jvm.internal.BaseContinuationImpl") + if (size == -1) { + result.stackTrace = resultStackTrace.toTypedArray() + return result + } + + val mergedStackTrace = arrayOfNulls(resultStackTrace.size + size) + for (i in 0 until size) { + mergedStackTrace[i] = causeTrace[i] + } + + for ((index, element) in resultStackTrace.withIndex()) { + mergedStackTrace[size + index] = element + } + + result.stackTrace = mergedStackTrace + return result +} + +/** + * Find initial cause of the exception without restored stacktrace. + * Returns intermediate stacktrace as well in order to avoid excess cloning of array as an optimization. + */ +private fun E.causeAndStacktrace(): Pair> { + val cause = cause + return if (cause != null && cause.javaClass == javaClass) { + val currentTrace = stackTrace + if (currentTrace.any { it.isArtificial() }) + cause as E to currentTrace + else this to emptyArray() + } else { + this to emptyArray() + } +} + +private fun mergeRecoveredTraces(recoveredStacktrace: Array, result: ArrayDeque) { + // Merge two stacktraces and trim common prefix + val startIndex = recoveredStacktrace.indexOfFirst { it.isArtificial() } + 1 + val lastFrameIndex = recoveredStacktrace.size - 1 + for (i in lastFrameIndex downTo startIndex) { + val element = recoveredStacktrace[i] + if (element.elementWiseEquals(result.last)) { + result.removeLast() + } + result.addFirst(recoveredStacktrace[i]) + } +} + +@Suppress("NOTHING_TO_INLINE") +internal actual suspend inline fun recoverAndThrow(exception: Throwable): Nothing { + if (recoveryDisabled(exception)) throw exception + suspendCoroutineUninterceptedOrReturn { + if (it !is CoroutineStackFrame) throw exception + throw recoverFromStackFrame(exception, it) + } +} + +internal actual fun unwrap(exception: E): E { + if (recoveryDisabled(exception)) { + return exception + } + + val cause = exception.cause + // Fast-path to avoid array cloning + if (cause == null || cause.javaClass != exception.javaClass) { + return exception + } + + if (exception.stackTrace.any { it.isArtificial() }) { + @Suppress("UNCHECKED_CAST") + return exception.cause as? E ?: exception + } else { + return exception + } +} + +private fun recoveryDisabled(exception: E) = + !RECOVER_STACKTRACES || !DEBUG || exception is CancellationException || exception is NonRecoverableThrowable + +private fun createStackTrace(continuation: CoroutineStackFrame): ArrayDeque { + val stack = ArrayDeque() + continuation.getStackTraceElement()?.let { stack.add(sanitize(it)) } + + var last = continuation + while (true) { + last = (last as? CoroutineStackFrame)?.callerFrame ?: break + last.getStackTraceElement()?.let { stack.add(sanitize(it)) } + } + return stack +} + +/** + * @suppress + */ +@InternalCoroutinesApi +public fun sanitize(element: StackTraceElement): StackTraceElement { + if (!element.className.contains('/')) { + return element + } + // KT-28237: STE generated with debug metadata contains '/' as separators in FQN, while Java contains dots + return StackTraceElement(element.className.replace('/', '.'), element.methodName, element.fileName, element.lineNumber) +} + +/** + * @suppress + */ +@InternalCoroutinesApi +public fun artificialFrame(message: String) = java.lang.StackTraceElement("\b\b\b($message", "\b", "\b", -1) +internal fun StackTraceElement.isArtificial() = className.startsWith("\b\b\b") +private fun Array.frameIndex(methodName: String) = indexOfFirst { methodName == it.className } + +private fun StackTraceElement.elementWiseEquals(e: StackTraceElement): Boolean { + /* + * In order to work on Java 9 where modules and classloaders of enclosing class + * are part of the comparison + */ + return lineNumber == e.lineNumber && methodName == e.methodName + && fileName == e.fileName && className == e.className +} + +@Suppress("ACTUAL_WITHOUT_EXPECT") +internal actual typealias CoroutineStackFrame = kotlin.coroutines.jvm.internal.CoroutineStackFrame + +@Suppress("ACTUAL_WITHOUT_EXPECT") +internal actual typealias StackTraceElement = java.lang.StackTraceElement diff --git a/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryNestedChannelsTest.kt b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryNestedChannelsTest.kt new file mode 100644 index 0000000000..d922edef0c --- /dev/null +++ b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryNestedChannelsTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("DeferredResultUnused", "DEPRECATION") + +package kotlinx.coroutines.exceptions + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import org.junit.* +import kotlin.coroutines.* + +class StackTraceRecoveryNestedChannelsTest : TestBase() { + + private val channel = Channel(0) + + private suspend fun sendWithContext(ctx: CoroutineContext) = withContext(ctx) { + sendInChannel() + yield() // TCE + } + + private suspend fun sendInChannel() { + channel.send(42) + yield() // TCE + } + + private suspend fun sendFromScope() = coroutineScope { + sendWithContext(wrapperDispatcher(coroutineContext)) + } + + @Test + fun testOfferWithCurrentContext() = runTest { + channel.close(RecoverableTestException()) + + try { + yield() // Will be fixed in 1.3.20 after KT-27190 + sendWithContext(coroutineContext) + } catch (e: Exception) { + verifyStackTrace(e, + "kotlinx.coroutines.RecoverableTestException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferWithCurrentContext\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:34)\n" + + "\t(Coroutine boundary)\n" + + "\tat kotlinx.coroutines.channels.AbstractSendChannel.offer(AbstractChannel.kt:180)\n" + + "\tat kotlinx.coroutines.channels.AbstractSendChannel.send(AbstractChannel.kt:168)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest.sendInChannel(StackTraceRecoveryNestedChannelsTest.kt:24)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$sendWithContext\$2.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:19)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$sendWithContext\$2.invoke(StackTraceRecoveryNestedChannelsTest.kt)\n" + + "\tat kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:85)\n" + + "\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:146)\n" + + "\tat kotlinx.coroutines.BuildersKt.withContext(Unknown Source)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest.sendWithContext(StackTraceRecoveryNestedChannelsTest.kt:18)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferWithCurrentContext\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:37)\n" + + "Caused by: kotlinx.coroutines.RecoverableTestException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferWithCurrentContext\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:34)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n") + } + } + + @Test + fun testOfferWithContextWrapped() = runTest { + channel.close(RecoverableTestException()) + + try { + sendWithContext(wrapperDispatcher(coroutineContext)) + } catch (e: Exception) { + verifyStackTrace(e, + "kotlinx.coroutines.RecoverableTestException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferWithContextWrapped\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:59)\n" + + "\t(Coroutine boundary)\n" + + "\tat kotlinx.coroutines.channels.AbstractSendChannel.offer(AbstractChannel.kt:180)\n" + + "\tat kotlinx.coroutines.channels.AbstractSendChannel.send(AbstractChannel.kt:168)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest.sendInChannel(StackTraceRecoveryNestedChannelsTest.kt:24)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$sendWithContext\$2.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:19)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferWithContextWrapped\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:62)\n" + + "Caused by: kotlinx.coroutines.RecoverableTestException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferWithContextWrapped\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:59)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)") + } + } + + @Test + fun testOfferFromScope() = runTest { + channel.close(RecoverableTestException()) + + try { + sendFromScope() + } catch (e: Exception) { + verifyStackTrace(e, + "kotlinx.coroutines.RecoverableTestException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferFromScope\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:81)\n" + + "\t(Coroutine boundary)\n" + + "\tat kotlinx.coroutines.channels.AbstractSendChannel.offer(AbstractChannel.kt:180)\n" + + "\tat kotlinx.coroutines.channels.AbstractSendChannel.send(AbstractChannel.kt:168)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest.sendInChannel(StackTraceRecoveryNestedChannelsTest.kt:24)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$sendWithContext\$2.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:19)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$sendFromScope\$2.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:28)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferFromScope\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:84)\n" + + "Caused by: kotlinx.coroutines.RecoverableTestException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferFromScope\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:81)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)") + } + } + + // Slow path via suspending send + @Test + fun testSendFromScope() = runTest { + val deferred = async { + try { + expect(1) + sendFromScope() + } catch (e: Exception) { + verifyStackTrace(e, + "kotlinx.coroutines.RecoverableTestException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testSendFromScope\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:118)\n" + + "\t(Coroutine boundary)\n" + + "\tat kotlinx.coroutines.channels.AbstractSendChannel.offer(AbstractChannel.kt:180)\n" + + "\tat kotlinx.coroutines.channels.AbstractSendChannel.send(AbstractChannel.kt:168)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest.sendInChannel(StackTraceRecoveryNestedChannelsTest.kt:24)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$sendWithContext\$2.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:19)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$sendFromScope\$2.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:29)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testSendFromScope\$1\$deferred\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:109)\n" + + "Caused by: kotlinx.coroutines.RecoverableTestException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testSendFromScope\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:118)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)") + } + } + + yield() + expect(2) + // Cancel is an analogue of `produce` failure, just a shorthand + channel.cancel(RecoverableTestException()) + finish(3) + deferred.await() + } +} diff --git a/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryNestedScopesTest.kt b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryNestedScopesTest.kt new file mode 100644 index 0000000000..bea18a431e --- /dev/null +++ b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryNestedScopesTest.kt @@ -0,0 +1,99 @@ +package kotlinx.coroutines.exceptions + +import kotlinx.coroutines.* +import org.junit.* +import kotlin.coroutines.* + +class StackTraceRecoveryNestedScopesTest : TestBase() { + + private val TEST_MACROS = "TEST_NAME" + + private val expectedTrace = "kotlinx.coroutines.RecoverableTestException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest.failure(StackTraceRecoveryNestedScopesTest.kt:9)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest.access\$failure(StackTraceRecoveryNestedScopesTest.kt:7)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$createFailingAsync\$1.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:12)\n" + + "\t(Coroutine boundary)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$callWithTimeout\$2.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:23)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$callCoroutineScope\$2.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:29)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$$TEST_MACROS\$1.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:36)\n" + + "Caused by: kotlinx.coroutines.RecoverableTestException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest.failure(StackTraceRecoveryNestedScopesTest.kt:9)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest.access\$failure(StackTraceRecoveryNestedScopesTest.kt:7)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$createFailingAsync\$1.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:12)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)" + + private fun failure(): String = throw RecoverableTestException() + + private fun CoroutineScope.createFailingAsync() = async { + failure() + } + + private suspend fun callWithContext(doYield: Boolean) = withContext(wrapperDispatcher(coroutineContext)) { + if (doYield) yield() + createFailingAsync().await() + yield() + } + + private suspend fun callWithTimeout(doYield: Boolean) = withTimeout(Long.MAX_VALUE) { + if (doYield) yield() + callWithContext(doYield) + yield() + } + + private suspend fun callCoroutineScope(doYield: Boolean) = coroutineScope { + if (doYield) yield() + callWithTimeout(doYield) + yield() + } + + @Test + fun testNestedScopes() = runTest { + try { + callCoroutineScope(false) + } catch (e: Exception) { + verifyStackTrace(e, expectedTrace.replace(TEST_MACROS, "testNestedScopes")) + } + } + + @Test + fun testNestedScopesYield() = runTest { + try { + callCoroutineScope(true) + } catch (e: Exception) { + verifyStackTrace(e, expectedTrace.replace(TEST_MACROS, "testNestedScopesYield")) + } + } + + @Test + fun testAwaitNestedScopes() = runTest { + val deferred = async(NonCancellable) { + callCoroutineScope(false) + } + + verifyAwait(deferred) + } + + private suspend fun verifyAwait(deferred: Deferred) { + try { + deferred.await() + } catch (e: Exception) { + verifyStackTrace(e, + "kotlinx.coroutines.RecoverableTestException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest.failure(StackTraceRecoveryNestedScopesTest.kt:23)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest.access\$failure(StackTraceRecoveryNestedScopesTest.kt:7)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$createFailingAsync\$1.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:26)\n" + + "\t(Coroutine boundary)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$callWithTimeout\$2.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:37)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$callCoroutineScope\$2.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:43)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$testAwaitNestedScopes\$1\$deferred\$1.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:68)\n" + + "\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt:99)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest.verifyAwait(StackTraceRecoveryNestedScopesTest.kt:76)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$testAwaitNestedScopes\$1.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:71)\n" + + "Caused by: kotlinx.coroutines.RecoverableTestException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest.failure(StackTraceRecoveryNestedScopesTest.kt:23)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest.access\$failure(StackTraceRecoveryNestedScopesTest.kt:7)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedScopesTest\$createFailingAsync\$1.invokeSuspend(StackTraceRecoveryNestedScopesTest.kt:26)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)") + } + } +} diff --git a/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryNestedTest.kt b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryNestedTest.kt new file mode 100644 index 0000000000..5073b7fdfa --- /dev/null +++ b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryNestedTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("DeferredResultUnused") + +package kotlinx.coroutines.exceptions + +import kotlinx.coroutines.* +import org.junit.Test +import kotlin.test.* + +class StackTraceRecoveryNestedTest : 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 0000000000..f96da49e86 --- /dev/null +++ b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryTest.kt @@ -0,0 +1,195 @@ +/* + * 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" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testAsync\$1\$1\$1.invokeSuspend(StackTraceRecoveryTest.kt:99)\n" + + "\t(Coroutine boundary)\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.toTypedArray()) + deferred.join() + } + + @Test + fun testCompletedAsync() = runTest { + val deferred = async(coroutineContext + NonCancellable) { + throw ExecutionException(null) + } + + deferred.join() + val stacktrace = listOf( + "java.util.concurrent.ExecutionException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testCompletedAsync\$1\$deferred\$1.invokeSuspend(StackTraceRecoveryTest.kt:44)\n" + + "\t(Coroutine boundary)\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.toTypedArray()) + } + + private suspend fun nestedMethod(deferred: Deferred<*>, vararg traces: String) { + oneMoreNestedMethod(deferred, *traces) + assertTrue(true) // Prevent tail-call optimization + } + + private suspend fun oneMoreNestedMethod(deferred: Deferred<*>, vararg traces: String) { + try { + deferred.await() + expectUnreached() + } catch (e: ExecutionException) { + verifyStackTrace(e, *traces) + } + } + + @Test + fun testReceiveFromChannel() = runTest { + val channel = Channel() + val job = launch { + expect(2) + channel.close(IllegalArgumentException()) + } + + expect(1) + channelNestedMethod( + channel, + "java.lang.IllegalArgumentException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testReceiveFromChannel\$1\$job\$1.invokeSuspend(StackTraceRecoveryTest.kt:93)\n" + + "\t(Coroutine boundary)\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.run(Dispatched.kt:152)") + expect(3) + job.join() + finish(4) + } + + @Test + fun testReceiveFromClosedChannel() = runTest { + val channel = Channel() + channel.close(IllegalArgumentException()) + channelNestedMethod( + channel, + "java.lang.IllegalArgumentException\n" + + "\t(Coroutine boundary)\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", + "Caused by: java.lang.IllegalArgumentException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testReceiveFromClosedChannel\$1.invokeSuspend(StackTraceRecoveryTest.kt:110)") + } + + private suspend fun channelNestedMethod(channel: Channel, vararg traces: 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, + "kotlinx.coroutines.RecoverableTestException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testWithContext\$1\$deferred\$1.invokeSuspend(StackTraceRecoveryTest.kt:143)\n" + + "\t(Coroutine boundary)\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, vararg traces: String) { + withContext(Dispatchers.IO) { + innerMethod(deferred, *traces) + } + + assertTrue(true) + } + + private suspend fun innerMethod(deferred: Deferred, vararg traces: 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, + "kotlinx.coroutines.RecoverableTestException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testCoroutineScope\$1\$deferred\$1.invokeSuspend(StackTraceRecoveryTest.kt:143)\n" + + "\t(Coroutine boundary)\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, vararg traces: String) = coroutineScope { + innerMethod(deferred, *traces) + assertTrue(true) + } +} diff --git a/core/kotlinx-coroutines-core/test/exceptions/Stacktraces.kt b/core/kotlinx-coroutines-core/test/exceptions/Stacktraces.kt new file mode 100644 index 0000000000..51e71d5138 --- /dev/null +++ b/core/kotlinx-coroutines-core/test/exceptions/Stacktraces.kt @@ -0,0 +1,47 @@ +package kotlinx.coroutines.exceptions + +import java.io.* +import kotlin.test.* + +public fun verifyStackTrace(e: Throwable, vararg traces: 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(traces.map { it.count("Caused by") }.sum(), causes) +} + +public fun toStackTrace(t: Throwable): String { + val sw = StringWriter() as Writer + t.printStackTrace(PrintWriter(sw)) + return sw.toString() +} + +public fun String.trimStackTrace(): String { + return applyBackspace(trimIndent().replace(Regex(":[0-9]+"), "") + .replace("kotlinx_coroutines_core_main", "") // yay source sets + .replace("kotlinx_coroutines_core", "")) +} + +public 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) +} + +public fun String.count(substring: String): Int = split(substring).size - 1 \ No newline at end of file diff --git a/core/kotlinx-coroutines-core/test/exceptions/SuppressionTests.kt b/core/kotlinx-coroutines-core/test/exceptions/SuppressionTests.kt index 2313dcc91a..e5f96f78d1 100644 --- a/core/kotlinx-coroutines-core/test/exceptions/SuppressionTests.kt +++ b/core/kotlinx-coroutines-core/test/exceptions/SuppressionTests.kt @@ -5,13 +5,11 @@ package kotlinx.coroutines.exceptions import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* import java.io.* import kotlin.coroutines.* import kotlin.test.* -/* - * Set of counterparts to common tests which check suppressed exceptions - */ @Suppress("DEPRECATION") class SuppressionTests : TestBase() { @@ -23,11 +21,11 @@ class SuppressionTests : TestBase() { } expect(1) - deferred.cancel(IOException()) + deferred.cancel(TestException("Message")) try { deferred.await() - } catch (e: IOException) { + } catch (e: TestException) { checkException(e.suppressed[0]) finish(3) } @@ -79,4 +77,28 @@ class SuppressionTests : TestBase() { coroutine.resumeWithException(IOException()) finish(10) } + + @Test + fun testExceptionUnwrapping() = runTest { + val channel = Channel() + + 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 e3896d34ea..19448b7bd7 100644 --- a/core/kotlinx-coroutines-core/test/exceptions/WithContextExceptionHandlingTest.kt +++ b/core/kotlinx-coroutines-core/test/exceptions/WithContextExceptionHandlingTest.kt @@ -27,11 +27,11 @@ class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() { 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()) @@ -41,30 +41,30 @@ class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() { @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()) @@ -90,12 +90,12 @@ class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() { @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) @@ -107,13 +107,13 @@ class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() { @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()) @@ -123,11 +123,11 @@ class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() { @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) @@ -162,9 +162,9 @@ class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() { @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) } @@ -180,7 +180,7 @@ class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() { @Test fun testCancelWithCause() = runTest { - val cause = IOException() + val cause = TestException() runOnlyCancellation(cause) { e -> assertSame(cause, e) assertTrue(e.suppressed.isEmpty()) @@ -207,7 +207,7 @@ class WithContextExceptionHandlingTest(private val mode: Mode) : TestBase() { } 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 c5145dacb1..25b909148d 100644 --- a/core/kotlinx-coroutines-core/test/test/TestCoroutineContextTest.kt +++ b/core/kotlinx-coroutines-core/test/test/TestCoroutineContextTest.kt @@ -269,7 +269,7 @@ class TestCoroutineContextTest { @Test fun testExceptionHandlingWithLaunchingChildCoroutines() = withTestContext(injectedContext) { val delay = 1000L - val expectedError = IllegalAccessError("hello") + val expectedError = TestException("hello") val expectedValue = 12 launch { @@ -299,7 +299,7 @@ class TestCoroutineContextTest { @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 @@ class TestCoroutineContextTest { @Test fun testExceptionHandlingWithRunBlockingAndWaitForException() = withTestContext(injectedContext) { val delay = 1000L - val expectedError = IllegalAccessError("hello") + val expectedError = TestException("hello") val expectedValue = 12 try { diff --git a/core/kotlinx-coroutines-debug/README.md b/core/kotlinx-coroutines-debug/README.md new file mode 100644 index 0000000000..e12c742647 --- /dev/null +++ b/core/kotlinx-coroutines-debug/README.md @@ -0,0 +1,120 @@ +# Module kotlinx-coroutines-debug + +Debugging facilities for `kotlinx.coroutines` on JVM. + +### Overview + +This module provides a debug JVM agent which allows to track and trace existing coroutines. +The main entry point to debug facilities is [DebugProbes] API. +Call to [DebugProbes.install] installs debug agent via ByteBuddy and starts to spy on coroutines when they are created, suspended or resumed. + +After that, you can use [DebugProbes.dumpCoroutines] to print all active (suspended or running) coroutines, including their state, creation and +suspension stacktraces. +Additionally, it is possible to process the list of such coroutines via [DebugProbes.dumpCoroutinesState] or dump isolated parts +of coroutines hierarchy referenced by a [Job] instance using [DebugProbes.printHierarchy]. + +### Using as JVM agent + +It is possible to use this module as a standalone JVM agent to enable debug probes on the application startup. +You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.1.0.jar`. +Additionally, on Linux and Mac OS X you can use `kill -5 $pid` command in order to force your application to print all alive coroutines. + + +### Example of usage + +Capabilities of this module can be demonstrated by the following example: +```kotlin +class Computation { + public fun computeValue(): Deferred = GlobalScope.async { + val firstPart = computeFirstPart() + val secondPart = computeSecondPart() + + combineResults(firstPart, secondPart) + } + + private suspend fun combineResults(firstPart: Deferred, secondPart: Deferred): String { + return firstPart.await() + secondPart.await() + } + + + private suspend fun CoroutineScope.computeFirstPart() = async { + delay(5000) + "4" + } + + private suspend fun CoroutineScope.computeSecondPart() = async { + delay(5000) + "2" + } +} + +fun main(args: Array) = runBlocking { + DebugProbes.install() + val computation = Computation() + val deferred = computation.computeValue() + + // Delay for some time + delay(1000) + + DebugProbes.dumpCoroutines() + + println("\nDumping only deferred") + DebugProbes.printHierarchy(deferred) +} +``` + +Printed result will be: +``` +Coroutines dump 2018/11/12 21:44:02 + +Coroutine "coroutine#2":DeferredCoroutine{Active}@1b26f7b2, state: SUSPENDED + at kotlinx.coroutines.DeferredCoroutine.await$suspendImpl(Builders.common.kt:99) + at Computation.combineResults(Example.kt:18) + at Computation$computeValue$1.invokeSuspend(Example.kt:14) + (Coroutine creation stacktrace) + at kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116) + at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23) + at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109) + at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:160) + at kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:88) + at kotlinx.coroutines.BuildersKt.async(Unknown Source) + at kotlinx.coroutines.BuildersKt__Builders_commonKt.async$default(Builders.common.kt:81) + at kotlinx.coroutines.BuildersKt.async$default(Unknown Source) + at Computation.computeValue(Example.kt:10) + at ExampleKt$main$1.invokeSuspend(Example.kt:36) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32) + at kotlinx.coroutines.DispatchedTask$DefaultImpls.run(Dispatched.kt:237) + at kotlinx.coroutines.DispatchedContinuation.run(Dispatched.kt:81) + at kotlinx.coroutines.EventLoopBase.processNextEvent(EventLoop.kt:123) + at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:69) + at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:45) + at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source) + at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:35) + at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source) + at ExampleKt.main(Example.kt:33) + +... More coroutines here ... + +Dumping only deferred +"coroutine#2":DeferredCoroutine{Active}, continuation is SUSPENDED at line kotlinx.coroutines.DeferredCoroutine.await$suspendImpl(Builders.common.kt:99) + "coroutine#3":DeferredCoroutine{Active}, continuation is SUSPENDED at line Computation$computeFirstPart$2.invokeSuspend(Example.kt:23) + "coroutine#4":DeferredCoroutine{Active}, continuation is SUSPENDED at line Computation$computeSecondPart$2.invokeSuspend(Example.kt:28) +``` + + +### Status of the API + +API is purely experimental and it is not guaranteed that it won't be changed (while it is marked as `@ExperimentalCoroutinesApi`). +Do not use this module in production environment and do not rely on the format of the data produced by [DebugProbes]. + + + +[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html + + +[DebugProbes]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/index.html +[DebugProbes.install]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/install.html +[DebugProbes.dumpCoroutines]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/dump-coroutines.html +[DebugProbes.dumpCoroutinesState]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/dump-coroutines-state.html +[DebugProbes.printHierarchy]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/print-hierarchy.html + diff --git a/core/kotlinx-coroutines-debug/build.gradle b/core/kotlinx-coroutines-debug/build.gradle new file mode 100644 index 0000000000..d54b2b67df --- /dev/null +++ b/core/kotlinx-coroutines-debug/build.gradle @@ -0,0 +1,27 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +apply plugin: "com.github.johnrengelman.shadow" + +dependencies { + compile "net.bytebuddy:byte-buddy:$byte_buddy_version" + compile "net.bytebuddy:byte-buddy-agent:$byte_buddy_version" +} + +jar { + manifest { + attributes "Premain-Class": "kotlinx.coroutines.debug.AgentPremain" + attributes "Can-Redefine-Classes": "true" + } +} + +shadowJar { + classifier null + // Shadow only byte buddy, do not package kotlin stdlib + dependencies { + include(dependency("net.bytebuddy:byte-buddy:$byte_buddy_version")) + include(dependency("net.bytebuddy:byte-buddy-agent:$byte_buddy_version")) + } + relocate 'net.bytebuddy', 'kotlinx.coroutines.repackaged.net.bytebuddy' +} diff --git a/core/kotlinx-coroutines-debug/src/AgentPremain.kt b/core/kotlinx-coroutines-debug/src/AgentPremain.kt new file mode 100644 index 0000000000..1ff996e5aa --- /dev/null +++ b/core/kotlinx-coroutines-debug/src/AgentPremain.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug + +import net.bytebuddy.agent.* +import sun.misc.* +import java.lang.instrument.* + +@Suppress("unused") +internal object AgentPremain { + + @JvmStatic + public fun premain(args: String?, instrumentation: Instrumentation) { + Installer.premain(args, instrumentation) + DebugProbes.install() + installSignalHandler() + } + + private fun installSignalHandler() { + try { + Signal.handle(Signal("TRAP")) { // kill -5 + DebugProbes.dumpCoroutines() + } + } catch (t: Throwable) { + System.err.println("Failed to install signal handler: $t") + } + } +} diff --git a/core/kotlinx-coroutines-debug/src/CoroutineState.kt b/core/kotlinx-coroutines-debug/src/CoroutineState.kt new file mode 100644 index 0000000000..ca8e14691b --- /dev/null +++ b/core/kotlinx-coroutines-debug/src/CoroutineState.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("PropertyName") + +package kotlinx.coroutines.debug + +import kotlinx.coroutines.* +import kotlinx.coroutines.internal.sanitize +import kotlin.coroutines.* +import kotlin.coroutines.jvm.internal.* + +/** + * Class describing coroutine state. + */ +@ExperimentalCoroutinesApi +public data class CoroutineState internal constructor( + public val continuation: Continuation<*>, + private val creationStackBottom: CoroutineStackFrame, + @JvmField internal val sequenceNumber: Long +) { + + /** + * [Job] associated with a current coroutine or [IllegalStateException] otherwise. + * May be later used in [DebugProbes.printHierarchy] + */ + public val job: Job get() = continuation.context[Job] ?: error("Continuation $continuation does not have a job") + + /** + * [Job] associated with a current coroutine or null. + * May be later used in [DebugProbes.printHierarchy] + */ + public val jobOrNull: Job? get() = continuation.context[Job] + + /** + * Creation stacktrace of coroutine + */ + public val creationStackTrace: List get() = creationStackTrace() + + /** + * Last observed [state][State] of the coroutine. + */ + public val state: State get() = _state + + private var _state: State = State.CREATED + + private var lastObservedFrame: CoroutineStackFrame? = null + + // Copy constructor + internal constructor(coroutine: Continuation<*>, state: CoroutineState) : this( + coroutine, + state.creationStackBottom, + state.sequenceNumber + ) { + _state = state.state + this.lastObservedFrame = state.lastObservedFrame + } + + private fun creationStackTrace(): List { + // Skip "Coroutine creation stacktrace" frame + return sequence { yieldFrames(creationStackBottom.callerFrame) }.toList() + } + + private tailrec suspend fun SequenceScope.yieldFrames(frame: CoroutineStackFrame?) { + if (frame == null) return + frame.getStackTraceElement()?.let { yield(it) } + val caller = frame.callerFrame + if (caller != null) { + yieldFrames(caller) + } + } + + internal fun updateState(state: State, frame: Continuation<*>) { + if (_state == state && lastObservedFrame != null) return + _state = state + lastObservedFrame = frame as? CoroutineStackFrame + } + + /** + * Last observed stacktrace of the coroutine captured on its suspension or resumption point. + * It means that for [running][State.RUNNING] coroutines resulting stacktrace is inaccurate and + * reflects stacktrace of the resumption point, not the actual current stacktrace + */ + public fun lastObservedStackTrace(): List { + var frame: CoroutineStackFrame? = lastObservedFrame ?: return emptyList() + val result = ArrayList() + while (frame != null) { + frame.getStackTraceElement()?.let { result.add(sanitize(it)) } + frame = frame.callerFrame + } + + return result + } +} + +/** + * Current state of the coroutine. + */ +public enum class State { + /** + * Created, but not yet started + */ + CREATED, + /** + * Started and running + */ + RUNNING, + /** + * Suspended + */ + SUSPENDED +} diff --git a/core/kotlinx-coroutines-debug/src/DebugProbes.kt b/core/kotlinx-coroutines-debug/src/DebugProbes.kt new file mode 100644 index 0000000000..12fb9a7957 --- /dev/null +++ b/core/kotlinx-coroutines-debug/src/DebugProbes.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("unused") + +package kotlinx.coroutines.debug + +import kotlinx.coroutines.* +import kotlinx.coroutines.debug.internal.* +import java.io.* +import java.lang.management.* +import java.util.* +import kotlin.coroutines.* + +/** + * Debug probes support. + * + * Debug probes is a dynamic attach mechanism which installs multiple hooks into coroutines machinery. + * It slows down all coroutine-related code, but in return provides a lot of diagnostic information, including + * asynchronous stack-traces and coroutine dumps (similar to [ThreadMXBean.dumpAllThreads] and `jstack` via [DebugProbes.dumpCoroutines]. + * + * Installed hooks: + * + * * `probeCoroutineResumed` is invoked on every [Continuation.resume]. + * * `probeCoroutineSuspended` is invoked on every continuation suspension. + * * `probeCoroutineCreated` is invoked on every coroutine creation using stdlib intrinsics. + * + * Overhead: + * * Every created coroutine is stored in a weak hash map, thus adding additional GC pressure. + * * On every created coroutine, stacktrace of the current thread is dumped. + * * On every `resume` and `suspend`, [WeakHashMap] is updated under a global lock. + */ +@ExperimentalCoroutinesApi +public object DebugProbes { + + /** + * Whether coroutine creation stacktraces should be sanitized. + * Sanitization removes all frames from `kotlinx.coroutines` package except + * the first one and the last one to simplify diagnostic. + */ + public var sanitizeStackTraces: Boolean = true + + /** + * Installs a [DebugProbes] instead of no-op stdlib probes by redefining + * debug probes class using the same class loader as one loaded [DebugProbes] class. + */ + public fun install() { + DebugProbesImpl.install() + } + + /** + * Uninstall debug probes. + */ + public fun uninstall() { + DebugProbesImpl.uninstall() + } + + /** + * Invokes given block of code with installed debug probes and uninstall probes in the end. + */ + public inline fun withDebugProbes(block: () -> Unit) { + install() + try { + block() + } finally { + uninstall() + } + } + + /** + * Returns string representation of the coroutines [job] hierarchy with additional debug information. + * Hierarchy is printed from the [job] as a root transitively to all children. + */ + public fun hierarchyToString(job: Job): String = DebugProbesImpl.hierarchyToString(job) + + /** + * Prints [job] hierarchy representation from [hierarchyToString] to the given [out]. + */ + public fun printHierarchy(job: Job, out: PrintStream = System.out) = + out.println(DebugProbesImpl.hierarchyToString(job)) + + /** + * Returns all existing coroutine states. + * The resulting collection represents a consistent snapshot of all existing coroutines at the moment of invocation. + */ + public fun dumpCoroutinesState(): List = DebugProbesImpl.dumpCoroutinesState() + + /** + * Dumps all active coroutines into the given output stream, providing a consistent snapshot of all existing coroutines at the moment of invocation. + * The output of this method is similar to `jstack` or a full thread dump. It can be used as the replacement to + * "Dump threads" action. + * + * Example of the output: + * ``` + * Coroutines dump 2018/11/12 19:45:14 + * + * Coroutine "coroutine#42":StandaloneCoroutine{Active}@58fdd99, state: SUSPENDED + * at MyClass$awaitData.invokeSuspend(MyClass.kt:37) + * (Coroutine creation stacktrace) + * at MyClass.createIoRequest(MyClass.kt:142) + * at MyClass.fetchData(MyClass.kt:154) + * at MyClass.showData(MyClass.kt:31) + * + * ... + * ``` + */ + public fun dumpCoroutines(out: PrintStream = System.out) = DebugProbesImpl.dumpCoroutines(out) +} + +// Stubs which are injected as coroutine probes. Require direct match of signatures +internal fun probeCoroutineResumed(frame: Continuation<*>) = DebugProbesImpl.probeCoroutineResumed(frame) + +internal fun probeCoroutineSuspended(frame: Continuation<*>) = DebugProbesImpl.probeCoroutineSuspended(frame) +internal fun probeCoroutineCreated(completion: kotlin.coroutines.Continuation): kotlin.coroutines.Continuation = + DebugProbesImpl.probeCoroutineCreated(completion) diff --git a/core/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt b/core/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt new file mode 100644 index 0000000000..5c1aec1c7b --- /dev/null +++ b/core/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt @@ -0,0 +1,257 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug.internal + +import kotlinx.coroutines.* +import kotlinx.coroutines.debug.* +import kotlinx.coroutines.internal.artificialFrame +import net.bytebuddy.* +import net.bytebuddy.agent.* +import net.bytebuddy.dynamic.loading.* +import java.io.* +import java.text.* +import java.util.* +import kotlin.collections.ArrayList +import kotlin.coroutines.* +import kotlin.coroutines.jvm.internal.* + +/** + * Mirror of [DebugProbes] with actual implementation. + * [DebugProbes] are implemented with pimpl to simplify user-facing class and make it look simple and + * documented. + */ +internal object DebugProbesImpl { + private const val ARTIFICIAL_FRAME_MESSAGE = "Coroutine creation stacktrace" + private val dateFormat = SimpleDateFormat("yyyy/MM/dd HH:mm:ss") + private val capturedCoroutines = WeakHashMap, CoroutineState>() + @Volatile + private var installations = 0 + private val isInstalled: Boolean get() = installations > 0 + // To sort coroutines by creation order, used as unique id + private var sequenceNumber: Long = 0 + + @Synchronized + public fun install() { + if (++installations > 1) return + + ByteBuddyAgent.install() + val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") + val cl2 = Class.forName("kotlinx.coroutines.debug.DebugProbesKt") + + ByteBuddy() + .redefine(cl2) + .name(cl.name) + .make() + .load(cl.classLoader, ClassReloadingStrategy.fromInstalledAgent()) + } + + @Synchronized + public fun uninstall() { + check(isInstalled) { "Agent was not installed" } + if (--installations != 0) return + + capturedCoroutines.clear() + val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") + val cl2 = Class.forName("kotlinx.coroutines.debug.internal.NoOpProbesKt") + + ByteBuddy() + .redefine(cl2) + .name(cl.name) + .make() + .load(cl.classLoader, ClassReloadingStrategy.fromInstalledAgent()) + } + + @Synchronized + public fun hierarchyToString(job: Job): String { + check(isInstalled) { "Debug probes are not installed" } + val jobToStack = capturedCoroutines + .filterKeys { it.delegate.context[Job] != null } + .mapKeys { it.key.delegate.context[Job]!! } + return buildString { + job.build(jobToStack, this, "") + } + } + + private fun Job.build(map: Map, builder: StringBuilder, indent: String) { + val state = map[this] + builder.append(indent) + @Suppress("DEPRECATION_ERROR") + val str = if (this !is JobSupport) toString() else toDebugString() + if (state == null) { + @Suppress("INVISIBLE_REFERENCE") + if (this !is kotlinx.coroutines.internal.ScopeCoroutine<*>) { // Do not print scoped coroutines + builder.append("$str\n") + } + } else { + val element = state.lastObservedStackTrace().firstOrNull() + val contState = state.state + builder.append("$str, continuation is $contState at line $element\n") + } + for (child in children) { + child.build(map, builder, indent + "\t") + } + } + + @Synchronized + public fun dumpCoroutinesState(): List { + check(isInstalled) { "Debug probes are not installed" } + return capturedCoroutines.entries.asSequence() + .map { CoroutineState(it.key.delegate, it.value) } + .sortedBy { it.sequenceNumber } + .toList() + } + + public fun dumpCoroutines(out: PrintStream) { + check(isInstalled) { "Debug probes are not installed" } + // Avoid inference with other out/err invocations by creating a string first + dumpCoroutines().let { out.println(it) } + } + + @Synchronized + private fun dumpCoroutines(): String = buildString { + // Synchronization window can be reduce even more, but no need to do it here + append("Coroutines dump ${dateFormat.format(System.currentTimeMillis())}") + capturedCoroutines + .asSequence() + .sortedBy { it.value.sequenceNumber } + .forEach { (key, value) -> + val state = if (value.state == State.RUNNING) + "${value.state} (Last suspension stacktrace, not an actual stacktrace)" + else + value.state.toString() + + append("\n\nCoroutine $key, state: $state") + val observedStackTrace = value.lastObservedStackTrace() + if (observedStackTrace.isEmpty()) { + append("\n\tat ${artificialFrame(ARTIFICIAL_FRAME_MESSAGE)}") + printStackTrace(value.creationStackTrace) + } else { + printStackTrace(value.lastObservedStackTrace()) + } + } + } + + private fun StringBuilder.printStackTrace(frames: List) { + frames.forEach { frame -> + append("\n\tat $frame") + } + } + + internal fun probeCoroutineResumed(frame: Continuation<*>) = updateState(frame, State.RUNNING) + + internal fun probeCoroutineSuspended(frame: Continuation<*>) = updateState(frame, State.SUSPENDED) + + private fun updateState(frame: Continuation<*>, state: State) { + if (!isInstalled) return + // Find ArtificialStackFrame of the coroutine + val owner = frame.owner() + updateState(owner, frame, state) + } + + @Synchronized + private fun updateState(owner: ArtificialStackFrame<*>?, frame: Continuation<*>, state: State) { + val coroutineState = capturedCoroutines[owner] ?: return + coroutineState.updateState(state, frame) + } + + private fun Continuation<*>.owner(): ArtificialStackFrame<*>? = + (this as? CoroutineStackFrame)?.owner() + + private tailrec fun CoroutineStackFrame.owner(): ArtificialStackFrame<*>? = + if (this is ArtificialStackFrame<*>) this else callerFrame?.owner() + + internal fun probeCoroutineCreated(completion: Continuation): Continuation { + if (!isInstalled) return completion + /* + * If completion already has an owner, it means that we are in scoped coroutine (coroutineScope, withContext etc.), + * then piggyback on its already existing owner and do not replace completion + */ + val owner = completion.owner() + if (owner != null) return completion + /* + * Here we replace completion with a sequence of CoroutineStackFrame objects + * which represents creation stacktrace, thus making stacktrace recovery mechanism + * even more verbose (it will attach coroutine creation stacktrace to all exceptions), + * and then using this artificial frame as an identifier of coroutineSuspended/resumed calls. + */ + val stacktrace = sanitizeStackTrace(Exception()) + val frame = stacktrace.foldRight(null) { frame, acc -> + object : CoroutineStackFrame { + override val callerFrame: CoroutineStackFrame? = acc + override fun getStackTraceElement(): StackTraceElement = frame + } + } + return ArtificialStackFrame(completion, frame!!).also { + storeFrame(it, completion) + } + } + + @Synchronized + private fun storeFrame(frame: ArtificialStackFrame, completion: Continuation) { + capturedCoroutines[frame] = CoroutineState(completion, frame, ++sequenceNumber) + } + + @Synchronized + private fun probeCoroutineCompleted(coroutine: ArtificialStackFrame<*>) { + capturedCoroutines.remove(coroutine) + } + + private class ArtificialStackFrame( + @JvmField val delegate: Continuation, + frame: CoroutineStackFrame + ) : Continuation by delegate, CoroutineStackFrame by frame { + override fun resumeWith(result: Result) { + probeCoroutineCompleted(this) + delegate.resumeWith(result) + } + + override fun toString(): String = delegate.toString() + } + + private fun sanitizeStackTrace(throwable: T): List { + val stackTrace = throwable.stackTrace + val size = stackTrace.size + val probeIndex = stackTrace.indexOfLast { it.className == "kotlin.coroutines.jvm.internal.DebugProbesKt" } + + if (!DebugProbes.sanitizeStackTraces) { + return List(size - probeIndex) { + if (it == 0) artificialFrame(ARTIFICIAL_FRAME_MESSAGE) else stackTrace[it + probeIndex] + } + } + + /* + * Trim intervals of internal methods from the stacktrace (bounds are excluded from trimming) + * E.g. for sequence [e, i1, i2, i3, e, i4, e, i5, i6, e7] + * output will be [e, i1, i3, e, i4, e, i5, i7] + */ + val result = ArrayList(size - probeIndex + 1) + result += artificialFrame(ARTIFICIAL_FRAME_MESSAGE) + var includeInternalFrame = true + for (i in (probeIndex + 1) until size - 1) { + val element = stackTrace[i] + if (!element.isInternalMethod) { + includeInternalFrame = true + result += element + continue + } + + if (includeInternalFrame) { + result += element + includeInternalFrame = false + } else if (stackTrace[i + 1].isInternalMethod) { + continue + } else { + result += element + includeInternalFrame = true + } + + } + + result += stackTrace[size - 1] + return result + } + + private val StackTraceElement.isInternalMethod: Boolean get() = className.startsWith("kotlinx.coroutines") +} diff --git a/core/kotlinx-coroutines-debug/src/internal/NoOpProbes.kt b/core/kotlinx-coroutines-debug/src/internal/NoOpProbes.kt new file mode 100644 index 0000000000..d32eeb674b --- /dev/null +++ b/core/kotlinx-coroutines-debug/src/internal/NoOpProbes.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("unused", "UNUSED_PARAMETER") + +package kotlinx.coroutines.debug.internal + +import kotlin.coroutines.* + +/* + * Empty class used to replace installed agent in the end of debug session + */ +@JvmName("probeCoroutineResumed") +internal fun probeCoroutineResumedNoOp(frame: Continuation<*>) = Unit +@JvmName("probeCoroutineSuspended") +internal fun probeCoroutineSuspendedNoOp(frame: Continuation<*>) = Unit +@JvmName("probeCoroutineCreated") +internal fun probeCoroutineCreatedNoOp(completion: kotlin.coroutines.Continuation): kotlin.coroutines.Continuation = completion diff --git a/core/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt b/core/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt new file mode 100644 index 0000000000..dea77f1af9 --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt @@ -0,0 +1,177 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug + +import kotlinx.coroutines.* +import org.junit.* +import org.junit.Test +import kotlin.coroutines.* +import kotlin.test.* + +@Suppress("SUSPENSION_POINT_INSIDE_MONITOR") // bug in 1.3.0 FE +class CoroutinesDumpTest : TestBase() { + + private val monitor = Any() + + @Before + fun setUp() { + before() + DebugProbes.sanitizeStackTraces = false + DebugProbes.install() + } + + @After + fun tearDown() { + try { + DebugProbes.uninstall() + } finally { + onCompletion() + } + } + + @Test + fun testSuspendedCoroutine() = synchronized(monitor) { + val deferred = GlobalScope.async { + sleepingOuterMethod() + } + + awaitCoroutineStarted() + Thread.sleep(100) // Let delay be invoked + verifyDump( + "Coroutine \"coroutine#1\":DeferredCoroutine{Active}@1e4a7dd4, state: SUSPENDED\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.sleepingNestedMethod(CoroutinesDumpTest.kt:95)\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.sleepingOuterMethod(CoroutinesDumpTest.kt:88)\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest\$testSuspendedCoroutine\$1\$deferred\$1.invokeSuspend(CoroutinesDumpTest.kt:29)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + + "\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n") + + val found = DebugProbes.dumpCoroutinesState().single { it.jobOrNull === deferred } + assertSame(deferred, found.job) + runBlocking { deferred.cancelAndJoin() } + } + + @Test + fun testRunningCoroutine() = synchronized(monitor) { + val deferred = GlobalScope.async { + activeMethod(shouldSuspend = false) + } + + awaitCoroutineStarted() + verifyDump( + "Coroutine \"coroutine#1\":DeferredCoroutine{Active}@1e4a7dd4, state: RUNNING (Last suspension stacktrace, not an actual stacktrace)\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest\$testRunningCoroutine\$1\$deferred\$1.invokeSuspend(CoroutinesDumpTest.kt:49)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + + "\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n" + + "\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:148)\n" + + "\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt)\n" + + "\tat kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" + + "\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt)\n" + + "\tat kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.testRunningCoroutine(CoroutinesDumpTest.kt:49)") + runBlocking { deferred.cancelAndJoin() } + } + + @Test + fun testRunningCoroutineWithSuspensionPoint() = synchronized(monitor) { + val deferred = GlobalScope.async { + activeMethod(shouldSuspend = true) + } + + awaitCoroutineStarted() + verifyDump( + "Coroutine \"coroutine#1\":DeferredCoroutine{Active}@1e4a7dd4, state: RUNNING (Last suspension stacktrace, not an actual stacktrace)\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.nestedActiveMethod(CoroutinesDumpTest.kt:111)\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.activeMethod(CoroutinesDumpTest.kt:106)\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest\$testRunningCoroutineWithSuspensionPoint\$1\$deferred\$1.invokeSuspend(CoroutinesDumpTest.kt:71)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + + "\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n" + + "\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:148)\n" + + "\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt)\n" + + "\tat kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" + + "\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt)\n" + + "\tat kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.testRunningCoroutineWithSuspensionPoint(CoroutinesDumpTest.kt:71)" + ) + runBlocking { deferred.cancelAndJoin() } + } + + @Test + fun testCreationStackTrace() = synchronized(monitor) { + val deferred = GlobalScope.async { + activeMethod(shouldSuspend = true) + } + + awaitCoroutineStarted() + val coroutine = DebugProbes.dumpCoroutinesState().first() + val result = coroutine.creationStackTrace.fold(StringBuilder()) { acc, element -> + acc.append(element.toString()) + acc.append('\n') + }.toString().trimStackTrace() + + runBlocking { deferred.cancelAndJoin() } + + val expected = ("kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + + "kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + + "kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109)\n" + + "kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:160)\n" + + "kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:88)\n" + + "kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" + + "kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt:81)\n" + + "kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" + + "kotlinx.coroutines.debug.CoroutinesDumpTest.testCreationStackTrace(CoroutinesDumpTest.kt:109)").trimStackTrace() + assertTrue(result.startsWith(expected)) + } + + @Test + fun testFinishedCoroutineRemoved() = synchronized(monitor) { + val deferred = GlobalScope.async { + activeMethod(shouldSuspend = true) + } + + awaitCoroutineStarted() + runBlocking { deferred.cancelAndJoin() } + verifyDump() + } + + private suspend fun activeMethod(shouldSuspend: Boolean) { + nestedActiveMethod(shouldSuspend) + delay(1) + } + + private suspend fun nestedActiveMethod(shouldSuspend: Boolean) { + if (shouldSuspend) delay(1) + notifyTest() + while (coroutineContext[Job]!!.isActive) { + Thread.sleep(100) + } + } + + private suspend fun sleepingOuterMethod() { + sleepingNestedMethod() + delay(1) + } + + private suspend fun sleepingNestedMethod() { + delay(1) + notifyTest() + delay(Long.MAX_VALUE) + } + + private fun awaitCoroutineStarted() { + (monitor as Object).wait() + } + + private fun notifyTest() { + synchronized(monitor) { + (monitor as Object).notify() + } + } +} diff --git a/core/kotlinx-coroutines-debug/test/DebugProbesTest.kt b/core/kotlinx-coroutines-debug/test/DebugProbesTest.kt new file mode 100644 index 0000000000..9dd4d7cec0 --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/DebugProbesTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.coroutines.debug + +import kotlinx.coroutines.* +import org.junit.Test +import java.util.concurrent.* +import kotlin.test.* + +class DebugProbesTest : TestBase() { + + private fun CoroutineScope.createDeferred(): Deferred<*> = async(NonCancellable) { + throw ExecutionException(null) + } + + @Test + fun testAsync() = runTest { + val deferred = createDeferred() + val traces = listOf( + "java.util.concurrent.ExecutionException\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:14)\n" + + "\t(Coroutine boundary)\n" + + "\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.oneMoreNestedMethod(DebugProbesTest.kt:49)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.nestedMethod(DebugProbesTest.kt:44)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$testAsync\$1.invokeSuspend(DebugProbesTest.kt:17)\n", + "Caused by: java.util.concurrent.ExecutionException\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:14)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)" + ) + nestedMethod(deferred, traces) + deferred.join() + } + + @Test + fun testAsyncWithProbes() = DebugProbes.withDebugProbes { + DebugProbes.sanitizeStackTraces = false + runTest { + val deferred = createDeferred() + val traces = listOf( + "java.util.concurrent.ExecutionException\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:16)\n" + + "\t(Coroutine boundary)\n" + + "\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.oneMoreNestedMethod(DebugProbesTest.kt:71)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.nestedMethod(DebugProbesTest.kt:66)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$testAsyncWithProbes\$1\$1.invokeSuspend(DebugProbesTest.kt:43)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + + "\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n" + + "\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:148)\n" + + "\tat kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:45)\n" + + "\tat kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)\n" + + "\tat kotlinx.coroutines.TestBase.runTest(TestBase.kt:138)\n" + + "\tat kotlinx.coroutines.TestBase.runTest\$default(TestBase.kt:19)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.testAsyncWithProbes(DebugProbesTest.kt:38)", + "Caused by: java.util.concurrent.ExecutionException\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:16)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n") + nestedMethod(deferred, traces) + deferred.join() + } + } + + @Test + fun testAsyncWithSanitizedProbes() = DebugProbes.withDebugProbes { + DebugProbes.sanitizeStackTraces = true + runTest { + val deferred = createDeferred() + val traces = listOf( + "java.util.concurrent.ExecutionException\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:16)\n" + + "\t(Coroutine boundary)\n" + + "\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.oneMoreNestedMethod(DebugProbesTest.kt:71)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.nestedMethod(DebugProbesTest.kt:66)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$testAsyncWithSanitizedProbes\$1\$1.invokeSuspend(DebugProbesTest.kt:43)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.testAsyncWithSanitizedProbes(DebugProbesTest.kt:38)", + "Caused by: java.util.concurrent.ExecutionException\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:16)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n") + nestedMethod(deferred, traces) + deferred.join() + } + } + + private suspend fun nestedMethod(deferred: Deferred<*>, traces: List) { + oneMoreNestedMethod(deferred, traces) + assertTrue(true) // Prevent tail-call optimization + } + + private suspend fun oneMoreNestedMethod(deferred: Deferred<*>, traces: List) { + try { + deferred.await() + expectUnreached() + } catch (e: ExecutionException) { + verifyStackTrace(e, traces) + } + } +} diff --git a/core/kotlinx-coroutines-debug/test/HierarchyToStringTest.kt b/core/kotlinx-coroutines-debug/test/HierarchyToStringTest.kt new file mode 100644 index 0000000000..6a6b4feda5 --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/HierarchyToStringTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import org.junit.* +import org.junit.Test +import kotlin.coroutines.* +import kotlin.test.* + +class HierarchyToStringTest : TestBase() { + + @Before + fun setUp() { + before() + DebugProbes.sanitizeStackTraces = false + DebugProbes.install() + } + + @After + fun tearDown() { + try { + DebugProbes.uninstall() + } finally { + onCompletion() + } + } + + @Test + fun testCompletingHierarchy() = runBlocking { + val tab = '\t' + val expectedString = """ + "coroutine#2":StandaloneCoroutine{Completing} + $tab"foo#3":DeferredCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1${'$'}1.invokeSuspend(HierarchyToStringTest.kt:30) + $tab"coroutine#4":ActorCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1${'$'}2${'$'}1.invokeSuspend(HierarchyToStringTest.kt:40) + $tab$tab"coroutine#5":StandaloneCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1${'$'}2${'$'}job$1.invokeSuspend(HierarchyToStringTest.kt:37) + """.trimIndent() + + checkHierarchy(isCompleting = true, expectedString = expectedString) + } + + @Test + fun testActiveHierarchy() = runBlocking { + val tab = '\t' + val expectedString = """ + "coroutine#2":StandaloneCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1.invokeSuspend(HierarchyToStringTest.kt:94) + $tab"foo#3":DeferredCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1${'$'}1.invokeSuspend(HierarchyToStringTest.kt:30) + $tab"coroutine#4":ActorCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1${'$'}2${'$'}1.invokeSuspend(HierarchyToStringTest.kt:40) + $tab$tab"coroutine#5":StandaloneCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1${'$'}2${'$'}job$1.invokeSuspend(HierarchyToStringTest.kt:37) + """.trimIndent() + checkHierarchy(isCompleting = false, expectedString = expectedString) + } + + private suspend fun CoroutineScope.checkHierarchy(isCompleting: Boolean, expectedString: String) { + val root = launchHierarchy(isCompleting) + repeat(4) { yield() } + expect(6) + assertEquals( + expectedString.trimStackTrace().trimPackage(), + DebugProbes.hierarchyToString(root).trimEnd().trimStackTrace().trimPackage() + ) + root.cancel() + root.join() + finish(7) + } + + private fun CoroutineScope.launchHierarchy(isCompleting: Boolean): Job { + return launch { + expect(1) + async(CoroutineName("foo")) { + expect(2) + delay(Long.MAX_VALUE) + } + + actor { + expect(3) + val job = launch { + expect(4) + delay(Long.MAX_VALUE) + } + + withContext(wrapperDispatcher(coroutineContext)) { + expect(5) + job.join() + } + } + + if (!isCompleting) { + delay(Long.MAX_VALUE) + } + } + } + + private fun wrapperDispatcher(context: CoroutineContext): CoroutineContext { + val dispatcher = context[ContinuationInterceptor] as CoroutineDispatcher + return object : CoroutineDispatcher() { + override fun dispatch(context: CoroutineContext, block: Runnable) { + dispatcher.dispatch(context, block) + } + } + } +} diff --git a/core/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt b/core/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt new file mode 100644 index 0000000000..925f2f7219 --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("PackageDirectoryMismatch") +package definitely.not.kotlinx.coroutines + +import kotlinx.coroutines.* +import kotlinx.coroutines.debug.* +import org.junit.* +import org.junit.Test +import java.util.concurrent.* +import kotlin.test.* + +class SanitizedProbesTest : TestBase() { + @Before + fun setUp() { + before() + DebugProbes.sanitizeStackTraces = true + DebugProbes.install() + } + + @After + fun tearDown() { + try { + DebugProbes.uninstall() + } finally { + onCompletion() + } + } + + @Test + fun testRecoveredStackTrace() = runTest { + val deferred = createDeferred() + val traces = listOf( + "java.util.concurrent.ExecutionException\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest\$createDeferredNested\$1.invokeSuspend(SanitizedProbesTest.kt:97)\n" + + "\t(Coroutine boundary)\n" + + "\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt:99)\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest.oneMoreNestedMethod(SanitizedProbesTest.kt:67)\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest.nestedMethod(SanitizedProbesTest.kt:61)\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest\$testRecoveredStackTrace\$1.invokeSuspend(SanitizedProbesTest.kt:50)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + + "\tat kotlinx.coroutines.TestBase.runTest\$default(TestBase.kt:141)\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest.testRecoveredStackTrace(SanitizedProbesTest.kt:33)", + "Caused by: java.util.concurrent.ExecutionException\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest\$createDeferredNested\$1.invokeSuspend(SanitizedProbesTest.kt:57)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n" + ) + nestedMethod(deferred, traces) + deferred.join() + } + + @Test + fun testCoroutinesDump() = runTest { + val deferred = createActiveDeferred() + yield() + verifyDump( + "Coroutine \"coroutine#3\":BlockingCoroutine{Active}@7d68ef40, state: RUNNING (Last suspension stacktrace, not an actual stacktrace)\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest\$testCoroutinesDump\$1.invokeSuspend(SanitizedProbesTest.kt:58)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + + "\tat kotlinx.coroutines.TestBase.runTest\$default(TestBase.kt:141)\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest.testCoroutinesDump(SanitizedProbesTest.kt:56)", + + "Coroutine \"coroutine#4\":DeferredCoroutine{Active}@75c072cb, state: SUSPENDED\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest\$createActiveDeferred\$1.invokeSuspend(SanitizedProbesTest.kt:63)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + + "\tat kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest.createActiveDeferred(SanitizedProbesTest.kt:62)\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest.access\$createActiveDeferred(SanitizedProbesTest.kt:16)\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest\$testCoroutinesDump\$1.invokeSuspend(SanitizedProbesTest.kt:57)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n" + + "\tat kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:237)\n" + + "\tat kotlinx.coroutines.TestBase.runTest\$default(TestBase.kt:141)\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest.testCoroutinesDump(SanitizedProbesTest.kt:56)" + ) + deferred.cancelAndJoin() + } + + private fun CoroutineScope.createActiveDeferred(): Deferred<*> = async { + suspendingMethod() + assertTrue(true) + } + + private suspend fun suspendingMethod() { + delay(Long.MAX_VALUE) + } + + private fun CoroutineScope.createDeferred(): Deferred<*> = createDeferredNested() + + private fun CoroutineScope.createDeferredNested(): Deferred<*> = async(NonCancellable) { + throw ExecutionException(null) + } + + private suspend fun nestedMethod(deferred: Deferred<*>, traces: List) { + oneMoreNestedMethod(deferred, traces) + assertTrue(true) // Prevent tail-call optimization + } + + private suspend fun oneMoreNestedMethod(deferred: Deferred<*>, traces: List) { + try { + deferred.await() + expectUnreached() + } catch (e: ExecutionException) { + verifyStackTrace(e, traces) + } + } +} diff --git a/core/kotlinx-coroutines-debug/test/ScopedBuildersTest.kt b/core/kotlinx-coroutines-debug/test/ScopedBuildersTest.kt new file mode 100644 index 0000000000..d0657d7a5b --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/ScopedBuildersTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug + +import kotlinx.coroutines.* +import org.junit.* +import kotlin.coroutines.* + +class ScopedBuildersTest : TestBase() { + @Before + fun setUp() { + before() + DebugProbes.sanitizeStackTraces = false + DebugProbes.install() + } + + @After + fun tearDown() { + try { + DebugProbes.uninstall() + } finally { + onCompletion() + } + } + + @Test + fun testNestedScopes() = runBlocking { + val job = launch { doInScope() } + yield() + yield() + verifyDump( + "Coroutine \"coroutine#1\":BlockingCoroutine{Active}@16612a51, state: RUNNING (Last suspension stacktrace, not an actual stacktrace)\n" + + "\tat kotlinx.coroutines.debug.ScopedBuildersTest\$testNestedScopes\$1.invokeSuspend(ScopedBuildersTest.kt:32)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n", + + "Coroutine \"coroutine#2\":StandaloneCoroutine{Active}@6b53e23f, state: SUSPENDED\n" + + "\tat kotlinx.coroutines.debug.ScopedBuildersTest\$doWithContext\$2.invokeSuspend(ScopedBuildersTest.kt:49)\n" + + "\tat kotlinx.coroutines.debug.ScopedBuildersTest.doWithContext(ScopedBuildersTest.kt:47)\n" + + "\tat kotlinx.coroutines.debug.ScopedBuildersTest\$doInScope\$2.invokeSuspend(ScopedBuildersTest.kt:41)\n" + + "\tat kotlinx.coroutines.debug.ScopedBuildersTest\$testNestedScopes\$1\$job\$1.invokeSuspend(ScopedBuildersTest.kt:30)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)") + job.cancelAndJoin() + finish(4) + } + + private suspend fun doInScope() = coroutineScope { + expect(1) + doWithContext() + expectUnreached() + } + + private suspend fun doWithContext() { + expect(2) + withContext(wrapperDispatcher(coroutineContext)) { + expect(3) + delay(Long.MAX_VALUE) + } + expectUnreached() + } +} \ No newline at end of file diff --git a/core/kotlinx-coroutines-debug/test/StartModeProbesTest.kt b/core/kotlinx-coroutines-debug/test/StartModeProbesTest.kt new file mode 100644 index 0000000000..bd33c5c9d2 --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/StartModeProbesTest.kt @@ -0,0 +1,157 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug + +import kotlinx.coroutines.* +import org.junit.* +import org.junit.Test +import kotlin.test.* + +class StartModeProbesTest : TestBase() { + + @Before + fun setUp() { + before() + DebugProbes.sanitizeStackTraces = false + DebugProbes.install() + } + + @After + fun tearDown() { + try { + DebugProbes.uninstall() + } finally { + onCompletion() + } + } + + @Test + fun testUndispatched() = runTest { + expect(1) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + undispatchedSleeping() + assertTrue(true) + } + + yield() + expect(3) + verifyPartialDump(2, "StartModeProbesTest.undispatchedSleeping") + job.cancelAndJoin() + verifyPartialDump(1, "StartModeProbesTest\$testUndispatched") + finish(4) + } + + private suspend fun undispatchedSleeping() { + delay(Long.MAX_VALUE) + assertTrue(true) + } + + @Test + fun testWithTimeoutWithUndispatched() = runTest { + expect(1) + val job = launchUndispatched() + + yield() + expect(3) + verifyPartialDump( + 2, + "StartModeProbesTest\$launchUndispatched\$1.invokeSuspend", + "StartModeProbesTest.withTimeoutHelper", + "StartModeProbesTest\$withTimeoutHelper\$2.invokeSuspend" + ) + job.cancelAndJoin() + verifyPartialDump(1, "StartModeProbesTest\$testWithTimeoutWithUndispatched") + finish(4) + } + + private fun CoroutineScope.launchUndispatched(): Job { + return launch(start = CoroutineStart.UNDISPATCHED) { + withTimeoutHelper() + assertTrue(true) + } + } + + private suspend fun withTimeoutHelper() { + withTimeout(Long.MAX_VALUE) { + expect(2) + delay(Long.MAX_VALUE) + } + + assertTrue(true) + } + + @Test + fun testWithTimeout() = runTest { + withTimeout(Long.MAX_VALUE) { + testActiveDump( + false, + "StartModeProbesTest\$testWithTimeout\$1.invokeSuspend", + "state: RUNNING" + ) + } + } + + @Test + fun testWithTimeoutAfterYield() = runTest { + withTimeout(Long.MAX_VALUE) { + testActiveDump( + true, + "StartModeProbesTest\$testWithTimeoutAfterYield\$1.invokeSuspend", + "StartModeProbesTest\$testWithTimeoutAfterYield\$1\$1.invokeSuspend", + "StartModeProbesTest.testActiveDump", + "state: RUNNING" + ) + } + } + + private suspend fun testActiveDump(shouldYield: Boolean, vararg expectedFrames: String) { + if (shouldYield) yield() + verifyPartialDump(1, *expectedFrames) + assertTrue(true) + } + + @Test + fun testWithTailCall() = runTest { + expect(1) + val job = tailCallMethod() + yield() + expect(3) + verifyPartialDump(2, "StartModeProbesTest\$launchFromTailCall\$2") + job.cancelAndJoin() + verifyPartialDump(1, "StartModeProbesTest\$testWithTailCall") + finish(4) + } + + private suspend fun CoroutineScope.tailCallMethod(): Job = launchFromTailCall() + private suspend fun CoroutineScope.launchFromTailCall(): Job = launch { + expect(2) + delay(Long.MAX_VALUE) + } + + @Test + fun testCoroutineScope() = runTest { + expect(1) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + runScope() + } + + yield() + expect(3) + verifyPartialDump( + 2, + "StartModeProbesTest\$runScope\$2.invokeSuspend", + "StartModeProbesTest\$testCoroutineScope\$1\$job\$1.invokeSuspend") + job.cancelAndJoin() + finish(4) + } + + private suspend fun runScope() { + coroutineScope { + expect(2) + delay(Long.MAX_VALUE) + } + } +} diff --git a/core/kotlinx-coroutines-debug/test/StracktraceUtils.kt b/core/kotlinx-coroutines-debug/test/StracktraceUtils.kt new file mode 100644 index 0000000000..baa48b038d --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/StracktraceUtils.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug + +import java.io.* +import kotlin.test.* + +public fun String.trimStackTrace(): String = + trimIndent() + .replace(Regex(":[0-9]+"), "") + .replace(Regex("#[0-9]+"), "") + .applyBackspace() + +public fun String.applyBackspace(): String { + val array = 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 + 1) +} + +public fun verifyStackTrace(e: Throwable, traces: List) { + val stacktrace = toStackTrace(e) + traces.forEach { + val expectedLines = it.trimStackTrace().split("\n") + for (i in 0 until expectedLines.size) { + 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()) +} + +public fun toStackTrace(t: Throwable): String { + val sw = StringWriter() + t.printStackTrace(PrintWriter(sw)) + return sw.toString() +} + +public fun String.count(substring: String): Int = split(substring).size - 1 + +public fun verifyDump(vararg traces: String) { + val baos = ByteArrayOutputStream() + DebugProbes.dumpCoroutines(PrintStream(baos)) + val trace = baos.toString().split("\n\n") + if (traces.isEmpty()) { + assertEquals(1, trace.size) + assertTrue(trace[0].startsWith("Coroutines dump")) + return + } + // Drop "Coroutine dump" line + trace.withIndex().drop(1).forEach { (index, value) -> + val expected = traces[index - 1].applyBackspace().split("\n\t(Coroutine creation stacktrace)\n", limit = 2) + val actual = value.applyBackspace().split("\n\t(Coroutine creation stacktrace)\n", limit = 2) + assertEquals(expected.size, actual.size) + + expected.withIndex().forEach { (index, trace) -> + val actualTrace = actual[index].trimStackTrace().sanitizeAddresses() + val expectedTrace = trace.trimStackTrace().sanitizeAddresses() + val actualLines = actualTrace.split("\n") + val expectedLines = expectedTrace.split("\n") + for (i in 0 until expectedLines.size) { + assertEquals(expectedLines[i], actualLines[i]) + } + } + } +} + +public fun String.trimPackage() = replace("kotlinx.coroutines.debug.", "") + +public fun verifyPartialDump(createdCoroutinesCount: Int, vararg frames: String) { + val baos = ByteArrayOutputStream() + DebugProbes.dumpCoroutines(PrintStream(baos)) + val dump = baos.toString() + val trace = dump.split("\n\n") + val matches = frames.all { frame -> + trace.any { tr -> tr.contains(frame) } + } + + assertEquals(createdCoroutinesCount, DebugProbes.dumpCoroutinesState().size) + assertTrue(matches) +} + +private fun String.sanitizeAddresses(): String { + val index = indexOf("coroutine\"") + val next = indexOf(',', index) + if (index == -1 || next == -1) return this + return substring(0, index) + substring(next, length) +} diff --git a/gradle.properties b/gradle.properties index df587f53ea..51834fa113 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,6 +11,7 @@ html_version=0.6.8 lincheck_version=1.9 dokka_version=0.9.16-rdev-2-mpp-hacks bintray_version=1.8.2-SNAPSHOT +byte_buddy_version=1.9.3 artifactory_plugin_version=4.7.3 # JS diff --git a/gradle/publish-bintray.gradle b/gradle/publish-bintray.gradle index ffb43a2730..23c38bd267 100644 --- a/gradle/publish-bintray.gradle +++ b/gradle/publish-bintray.gradle @@ -8,6 +8,7 @@ apply plugin: 'maven' apply plugin: 'maven-publish' apply plugin: 'com.jfrog.bintray' apply plugin: 'com.jfrog.artifactory' +apply plugin: "com.github.johnrengelman.shadow" apply from: project.rootProject.file('gradle/maven-central.gradle') @@ -16,13 +17,13 @@ def coroutines_core = platformLib("kotlinx-coroutines-core", platform) // ------------- tasks -def isNative = project.name.endsWith("native") +def isNative() { return project.name.endsWith("native") } def bUser = project.hasProperty('bintrayUser') ? project.property('bintrayUser') : System.getenv('BINTRAY_USER') def bKey = project.hasProperty('bintrayApiKey') ? project.property('bintrayApiKey') : System.getenv('BINTRAY_API_KEY') task sourcesJar(type: Jar) { classifier = 'sources' - if (!isNative) { + if (!isNative()) { from sourceSets.main.allSource } @@ -32,19 +33,15 @@ task sourcesJar(type: Jar) { } } + publishing { repositories { maven { url = 'https://kotlin.bintray.com/kotlinx' } } publications { - maven(MavenPublication) { - if (!isNative) { - from components.java - artifact javadocJar - artifact sourcesJar - } - pom.withXml(configureMavenCentralMetadata) + maven(MavenPublication) { publication -> + preparePublication(publication) } } } @@ -58,15 +55,8 @@ artifactory { password = bKey } - publications { - maven(MavenPublication) { - if (!isNative) { - from components.java - artifact javadocJar - artifact sourcesJar - } - pom.withXml(configureMavenCentralMetadata) - } + maven(MavenPublication) { publication -> + preparePublication(publication) } defaults { @@ -75,6 +65,19 @@ artifactory { } } +def preparePublication(MavenPublication publication) { + if (!isNative()) { + if (project.name == "kotlinx-coroutines-debug") { + project.shadow.component(publication) + } else { + publication.from components.java + } + publication.artifact javadocJar + publication.artifact sourcesJar + } + publication.pom.withXml(configureMavenCentralMetadata) +} + task publishDevelopSnapshot() { def branch = System.getenv('currentBranch') if (branch == "develop") { @@ -111,7 +114,7 @@ bintrayUpload.doFirst { } // TODO :kludge this is required to disable publish of metadata for all but native -if (!isNative) { +if (!isNative()) { afterEvaluate { publishing.publications.each { pub -> pub.gradleModuleMetadataFile = null diff --git a/integration/kotlinx-coroutines-jdk8/test/future/FutureTest.kt b/integration/kotlinx-coroutines-jdk8/test/future/FutureTest.kt index 9ec2514c56..bed4c9119a 100644 --- a/integration/kotlinx-coroutines-jdk8/test/future/FutureTest.kt +++ b/integration/kotlinx-coroutines-jdk8/test/future/FutureTest.kt @@ -303,17 +303,14 @@ class FutureTest : TestBase() { assertFalse(deferred.isCompleted) lock.unlock() - try { deferred.await() fail("deferred.await() should throw an exception") - } catch (e: Exception) { + } catch (e: CompletionException) { assertTrue(deferred.isCancelled) - assertTrue(e is CompletionException) // that's how supplyAsync wraps it - val cause = e.cause!! + val cause = e.cause?.cause!! // Stacktrace augmentation assertTrue(cause is TestException) assertEquals("something went wrong", cause.message) - assertSame(e, deferred.getCompletionExceptionOrNull()) // same exception is returns as thrown } } diff --git a/integration/kotlinx-coroutines-play-services/test/TaskTest.kt b/integration/kotlinx-coroutines-play-services/test/TaskTest.kt index 06be2436d9..3274c51aa3 100644 --- a/integration/kotlinx-coroutines-play-services/test/TaskTest.kt +++ b/integration/kotlinx-coroutines-play-services/test/TaskTest.kt @@ -4,22 +4,12 @@ package kotlinx.coroutines.tasks -import com.google.android.gms.tasks.RuntimeExecutionException -import com.google.android.gms.tasks.Tasks -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.TestBase -import kotlinx.coroutines.async -import kotlinx.coroutines.delay -import kotlinx.coroutines.ignoreLostThreads -import org.hamcrest.core.IsEqual -import org.junit.Assert -import org.junit.Before -import org.junit.Test -import java.util.concurrent.locks.ReentrantLock -import kotlin.concurrent.withLock +import com.google.android.gms.tasks.* +import kotlinx.coroutines.* +import org.hamcrest.core.* +import org.junit.* +import java.util.concurrent.locks.* +import kotlin.concurrent.* class TaskTest : TestBase() { @Before @@ -79,7 +69,7 @@ class TaskTest : TestBase() { runTest { task.await() } } catch (e: RuntimeExecutionException) { Assert.assertFalse(task.isSuccessful) - Assert.assertTrue(e.cause is OutOfMemoryError) + Assert.assertTrue(e.cause?.cause is OutOfMemoryError) } } diff --git a/js/kotlinx-coroutines-core-js/src/internal/ProbesSupport.kt b/js/kotlinx-coroutines-core-js/src/internal/ProbesSupport.kt new file mode 100644 index 0000000000..81b6476bc2 --- /dev/null +++ b/js/kotlinx-coroutines-core-js/src/internal/ProbesSupport.kt @@ -0,0 +1,10 @@ +/* + * 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.* + +@Suppress("NOTHING_TO_INLINE") +internal actual inline fun probeCoroutineCreated(completion: Continuation): Continuation = completion diff --git a/js/kotlinx-coroutines-core-js/src/internal/StackTraceRecovery.kt b/js/kotlinx-coroutines-core-js/src/internal/StackTraceRecovery.kt new file mode 100644 index 0000000000..57c6247b5e --- /dev/null +++ b/js/kotlinx-coroutines-core-js/src/internal/StackTraceRecovery.kt @@ -0,0 +1,22 @@ +/* + * 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 recoverStackTrace(exception: E, continuation: Continuation<*>): E = exception +internal actual fun recoverStackTrace(exception: E): E = exception +internal actual suspend inline fun recoverAndThrow(exception: Throwable): Nothing = throw exception + +internal actual fun unwrap(exception: E): E = exception + +@Suppress("UNUSED") +internal actual interface CoroutineStackFrame { + public actual val callerFrame: CoroutineStackFrame? + public actual fun getStackTraceElement(): StackTraceElement? +} + +@Suppress("ACTUAL_WITHOUT_EXPECT") +internal actual typealias StackTraceElement = Any diff --git a/knit/resources/knit.properties b/knit/resources/knit.properties index aa639bd36e..4384efd47d 100644 --- a/knit/resources/knit.properties +++ b/knit/resources/knit.properties @@ -4,6 +4,6 @@ site.root=https://kotlin.github.io/kotlinx.coroutines -module.roots=common js core integration native reactive ui +module.roots=common js core debug integration native reactive ui module.marker=build.gradle module.docs=build/dokka diff --git a/native/kotlinx-coroutines-core-native/src/internal/ProbesSupport.kt b/native/kotlinx-coroutines-core-native/src/internal/ProbesSupport.kt new file mode 100644 index 0000000000..c2daab50b7 --- /dev/null +++ b/native/kotlinx-coroutines-core-native/src/internal/ProbesSupport.kt @@ -0,0 +1,10 @@ +/* + * 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.* + +@Suppress("NOTHING_TO_INLINE") +internal actual inline fun probeCoroutineCreated(completion: Continuation): Continuation = completion \ No newline at end of file diff --git a/native/kotlinx-coroutines-core-native/src/internal/StackTraceRecovery.kt b/native/kotlinx-coroutines-core-native/src/internal/StackTraceRecovery.kt new file mode 100644 index 0000000000..4faf16ac1d --- /dev/null +++ b/native/kotlinx-coroutines-core-native/src/internal/StackTraceRecovery.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 recoverStackTrace(exception: E, continuation: Continuation<*>): E = exception +internal actual fun recoverStackTrace(exception: E): E = exception +internal actual fun 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? +} + +@Suppress("ACTUAL_WITHOUT_EXPECT") +internal actual typealias StackTraceElement = Any diff --git a/settings.gradle b/settings.gradle index 449f7cb2d9..46f3bb598c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,6 +23,7 @@ module('binary-compatibility-validator') module('common/kotlinx-coroutines-core-common') module('core/kotlinx-coroutines-core') +module('core/kotlinx-coroutines-debug') module('core/stdlib-stubs') module('integration/kotlinx-coroutines-guava')