From 675c30c4b96533a374ae36b144dbe6b26634b97b Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 2 Nov 2018 15:55:20 +0300 Subject: [PATCH 01/17] Basic exception stacktrace recovery mechanism * Implement CoroutineStackFrame in CancellableContinuationImpl, DispatchedContinuation and ScopeCoroutine * On coroutine resumption try to reflectively instantiate exception instance of the same type, but with augmented stacktrace * Recover stacktrace by walking over CoroutineStackFrame * Recover stacktrace on fast-path exceptions without CoroutineStackFrame walking to provide more context to an exception * Unwrap exceptions when doing aggregation in JobSupport * Add kill-switch to disable stacktrace recovery, introduce method to recover stacktrace on the exceptional fast-path * Add `suspendCoroutineOrReturn` on exceptional fast-path in await in order to provide "real" stacktrace Design rationale: All recovery of *suspended* continuations takes place in Dispatched.kt file, the only place where all calls to "resume*" ends up, so we don't have to remember about stacktrace recovery in every primitive we are implementing. But to provide more accurate stacktraces we *have to* recover it on every fast-path for better debuggability. Fixes #493 --- .../kotlinx-coroutines-core.txt | 4 +- .../src/CancellableContinuation.kt | 7 +- .../src/Dispatched.kt | 18 +- .../src/JobSupport.kt | 11 +- .../src/channels/AbstractChannel.kt | 30 +-- .../src/internal/Exceptions.common.kt | 50 ++++ .../src/internal/Scopes.kt | 4 +- .../test/TestBase.common.kt | 13 +- core/kotlinx-coroutines-core/src/Debug.kt | 16 ++ .../src/internal/Exceptions.kt | 137 ++++++++++ .../StackTraceRecoveryInHierarchiesTest.kt | 85 +++++++ .../test/exceptions/StackTraceRecoveryTest.kt | 238 ++++++++++++++++++ .../test/exceptions/SuppresionTests.kt | 38 ++- .../WithContextExceptionHandlingTest.kt | 64 ++--- .../test/test/TestCoroutineContextTest.kt | 6 +- .../src/internal/Exceptions.kt | 21 ++ .../src/internal/Exceptions.kt | 20 ++ 17 files changed, 686 insertions(+), 76 deletions(-) create mode 100644 common/kotlinx-coroutines-core-common/src/internal/Exceptions.common.kt create mode 100644 core/kotlinx-coroutines-core/src/internal/Exceptions.kt create mode 100644 core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryInHierarchiesTest.kt create mode 100644 core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryTest.kt create mode 100644 js/kotlinx-coroutines-core-js/src/internal/Exceptions.kt create mode 100644 native/kotlinx-coroutines-core-native/src/internal/Exceptions.kt 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 152f378274..d7b990c417 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; 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 5de38ef54c..897b1e12e5 100644 --- a/common/kotlinx-coroutines-core-common/src/Dispatched.kt +++ b/common/kotlinx-coroutines-core-common/src/Dispatched.kt @@ -81,10 +81,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) @@ -167,7 +169,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) } } @@ -190,7 +192,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) { @@ -199,8 +201,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( @@ -231,7 +233,7 @@ internal abstract class DispatchedTask( else { val exception = getExceptionalResult(state) if (exception != null) - continuation.resumeWithException(exception) + continuation.resumeWithStackTrace(exception) else continuation.resume(getSuccessfulResult(state)) } @@ -275,3 +277,7 @@ internal fun DispatchedTask.resume(delegate: Continuation, useMode: In delegate.resumeMode(getSuccessfulResult(state), useMode) } } + + +@Suppress("NOTHING_TO_INLINE") +private inline fun Continuation<*>.resumeWithStackTrace(exception: Throwable) = resumeWith(Result.failure(recoverStackTrace(exception, this))) diff --git a/common/kotlinx-coroutines-core-common/src/JobSupport.kt b/common/kotlinx-coroutines-core-common/src/JobSupport.kt index b80ce21fa0..3cdc81e129 100644 --- a/common/kotlinx-coroutines-core-common/src/JobSupport.kt +++ b/common/kotlinx-coroutines-core-common/src/JobSupport.kt @@ -242,8 +242,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(exception)) { + rootCause.addSuppressedThrowable(unwrapped) suppressed = true } } @@ -1078,7 +1079,11 @@ 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 + suspendCoroutineUninterceptedOrReturn { + throw recoverStackTrace(state.cause, it) + } + } return state } diff --git a/common/kotlinx-coroutines-core-common/src/channels/AbstractChannel.kt b/common/kotlinx-coroutines-core-common/src/channels/AbstractChannel.kt index 395988f733..48692f1d88 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) recoverStackTrace(throw 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/Exceptions.common.kt b/common/kotlinx-coroutines-core-common/src/internal/Exceptions.common.kt new file mode 100644 index 0000000000..46ac8e29ac --- /dev/null +++ b/common/kotlinx-coroutines-core-common/src/internal/Exceptions.common.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +import kotlin.coroutines.* + +/** + * Tries to recover stacktrace for given [exception] and [continuation]. + * Stacktrace recovery tries to restore [continuation] stack frames using its debug metadata with [CoroutineStackFrame] API + * and then reflectively instantiate exception of given type with original exception as a cause and + * sets new stacktrace for wrapping exception. + * Some frames may be missing due to tail-call elimination. + * + * Works only on JVM with enabled debug-mode. + */ +internal expect fun 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 + +expect class StackTraceElement + +internal expect interface CoroutineStackFrame { + public val callerFrame: CoroutineStackFrame? + public fun getStackTraceElement(): StackTraceElement? +} + +/** + * Marker that indicates that stacktrace of the exception should not be recovered. + * Currently internal, but may become public in the future + */ +internal interface NonRecoverableThrowable diff --git a/common/kotlinx-coroutines-core-common/src/internal/Scopes.kt b/common/kotlinx-coroutines-core-common/src/internal/Scopes.kt index 4c4f9dd486..efa8f04ded 100644 --- a/common/kotlinx-coroutines-core-common/src/internal/Scopes.kt +++ b/common/kotlinx-coroutines-core-common/src/internal/Scopes.kt @@ -14,7 +14,9 @@ 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") diff --git a/common/kotlinx-coroutines-core-common/test/TestBase.common.kt b/common/kotlinx-coroutines-core-common/test/TestBase.common.kt index 335c7480a1..7bfcf29998 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) : Throwable(message) -// Wrap context to avoid fast-paths on dispatcher comparison public fun wrapperDispatcher(context: CoroutineContext): CoroutineContext { val dispatcher = context[ContinuationInterceptor] as CoroutineDispatcher return object : CoroutineDispatcher() { diff --git a/core/kotlinx-coroutines-core/src/Debug.kt b/core/kotlinx-coroutines-core/src/Debug.kt index fc19feed67..3796c400db 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]. */ @@ -36,6 +50,8 @@ internal val DEBUG = systemProp(DEBUG_PROPERTY_NAME).let { value -> } } +internal val RECOVER_STACKTRACE = systemProp(STACKTRACE_RECOVERY_PROPERTY_NAME, true) + // internal debugging tools internal actual val Any.hexAddress: String diff --git a/core/kotlinx-coroutines-core/src/internal/Exceptions.kt b/core/kotlinx-coroutines-core/src/internal/Exceptions.kt new file mode 100644 index 0000000000..cab8e64b99 --- /dev/null +++ b/core/kotlinx-coroutines-core/src/internal/Exceptions.kt @@ -0,0 +1,137 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +import kotlinx.coroutines.* +import java.util.* +import kotlin.coroutines.* +import kotlin.coroutines.intrinsics.* + +internal actual fun recoverStackTrace(exception: E): E { + if (recoveryDisabled(exception)) { + return exception + } + + val copy = tryCopyException(exception) ?: return exception + return copy.sanitizeStackTrace() +} + +private fun E.sanitizeStackTrace(): E { + val size = stackTrace.size + + var lastIntrinsic = -1 + for (i in 0 until size) { + val name = stackTrace[i].className + if ("kotlinx.coroutines.internal.ExceptionsKt" == name) { + lastIntrinsic = i + } + } + + val startIndex = lastIntrinsic + 1 + val trace = Array(size - lastIntrinsic) { + if (it == 0) { + artificialFrame("Current coroutine stacktrace") + } else { + stackTrace[startIndex + it - 1] + } + } + + stackTrace = trace + return this +} + +internal actual fun 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 { + val newException = tryCopyException(exception) ?: return exception + val stacktrace = createStackTrace(continuation) + if (stacktrace.isEmpty()) return exception + stacktrace.add(0, artificialFrame("Current coroutine stacktrace")) + newException.stackTrace = stacktrace.toTypedArray() + return newException +} + + +@Suppress("NOTHING_TO_INLINE") +internal actual suspend inline fun recoverAndThrow(exception: Throwable): Nothing { + if (recoveryDisabled(exception)) throw exception + suspendCoroutineUninterceptedOrReturn { + if (it !is CoroutineStackFrame) throw exception + throw recoverFromStackFrame(exception, it) + } +} + +internal actual fun unwrap(exception: E): E { + if (recoveryDisabled(exception)) { + return exception + } + + val element = exception.stackTrace.firstOrNull() ?: return exception + if (element.isArtificial()) { + @Suppress("UNCHECKED_CAST") + return exception.cause as? E ?: exception + } else { + return exception + } +} + +private fun recoveryDisabled(exception: E) = + !RECOVER_STACKTRACE || !DEBUG || exception is CancellationException || exception is NonRecoverableThrowable + +@Suppress("UNCHECKED_CAST") +private fun tryCopyException(exception: E): E? { + /* + * Try to reflectively find constructor(), constructor(message, cause) or constructor(cause). + * Exceptions are shared among coroutines, so we should copy exception before recovering current stacktrace. + */ + var newException: E? = null + try { + val constructors = exception.javaClass.constructors.sortedByDescending { it.parameterTypes.size } + for (constructor in constructors) { + val parameters = constructor.parameterTypes + if (parameters.size == 2 && parameters[0] == String::class.java && parameters[1] == Throwable::class.java) { + newException = constructor.newInstance(exception.message, exception) as E + } else if (parameters.size == 1 && parameters[0] == Throwable::class.java) { + newException = constructor.newInstance(exception) as E + } else if (parameters.isEmpty()) { + newException = (constructor.newInstance() as E).also { it.initCause(exception) } + } + + if (newException != null) { + break + } + } + } catch (e: Exception) { + // Do nothing + } + return newException +} + +private fun createStackTrace(continuation: CoroutineStackFrame): ArrayList { + val stack = ArrayList() + continuation.getStackTraceElement()?.let { stack.add(it) } + + var last = continuation + while (true) { + last = (last as? CoroutineStackFrame)?.callerFrame ?: break + last.getStackTraceElement()?.let { stack.add(it) } + } + return stack +} + + +internal fun artificialFrame(message: String) = java.lang.StackTraceElement("\b\b\b($message", "\b", "\b", -1) +internal fun StackTraceElement.isArtificial() = className.startsWith("\b\b\b") + +@Suppress("ACTUAL_WITHOUT_EXPECT") +actual typealias CoroutineStackFrame = kotlin.coroutines.jvm.internal.CoroutineStackFrame + +actual typealias StackTraceElement = java.lang.StackTraceElement diff --git a/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryInHierarchiesTest.kt b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryInHierarchiesTest.kt new file mode 100644 index 0000000000..f243adde50 --- /dev/null +++ b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryInHierarchiesTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.exceptions + +import kotlinx.coroutines.* +import org.junit.Test +import kotlin.test.* + +class StackTraceRecoveryInHierarchiesTest : TestBase() { + + @Test + fun testNestedAsync() = runTest { + val rootAsync = async(NonCancellable) { + expect(1) + + // Just a noise for unwrapping + async { + expect(2) + delay(Long.MAX_VALUE) + } + + // Do not catch, fail on cancellation + async { + expect(3) + async { + expect(4) + delay(Long.MAX_VALUE) + } + + async { + expect(5) + // 1) await(), catch, verify and rethrow + try { + val nested = async { + expect(6) + throw RecoverableTestException() + } + + nested.awaitNested() + } catch (e: RecoverableTestException) { + expect(7) + e.verifyException( + "await\$suspendImpl", + "awaitNested", + "\$testNestedAsync\$1\$rootAsync\$1\$2\$2.invokeSuspend" + ) + // Just rethrow it + throw e + } + } + } + } + + try { + rootAsync.awaitRootLevel() + } catch (e: RecoverableTestException) { + e.verifyException("await\$suspendImpl", "awaitRootLevel") + finish(8) + } + } + + private suspend fun Deferred<*>.awaitRootLevel() { + await() + assertTrue(true) + } + + private suspend fun Deferred<*>.awaitNested() { + await() + assertTrue(true) + } + + private fun RecoverableTestException.verifyException(vararg expectedTraceElements: String) { + // It is "recovered" only once + assertEquals(1, depth()) + val stacktrace = stackTrace.map { it.methodName }.toSet() + assertTrue(expectedTraceElements.all { stacktrace.contains(it) }) + } + + private fun Throwable.depth(): Int { + val cause = cause ?: return 0 + return 1 + cause.depth() + } +} diff --git a/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryTest.kt b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryTest.kt new file mode 100644 index 0000000000..d47ced714d --- /dev/null +++ b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryTest.kt @@ -0,0 +1,238 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.exceptions + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import org.junit.Test +import java.io.* +import java.util.concurrent.* +import kotlin.test.* + +/* + * All stacktrace validation skips line numbers + */ +class StackTraceRecoveryTest : TestBase() { + + @Test + fun testAsync() = runTest { + fun createDeferred(depth: Int): Deferred<*> { + return if (depth == 0) { + async(coroutineContext + NonCancellable) { + throw ExecutionException(null) + } + } else { + createDeferred(depth - 1) + } + } + + val deferred = createDeferred(3) + val traces = listOf( + "java.util.concurrent.ExecutionException\n" + + "\t(Current coroutine stacktrace)\n" + + "\tat kotlinx/coroutines/DeferredCoroutine.await\$suspendImpl(Builders.common.kt:99)\n" + + "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.oneMoreNestedMethod(StackTraceRecoveryTest.kt:49)\n" + + "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.nestedMethod(StackTraceRecoveryTest.kt:44)\n" + + "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$testAsync\$1.invokeSuspend(StackTraceRecoveryTest.kt:17)\n", + "Caused by: java.util.concurrent.ExecutionException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testAsync\$1\$1\$1.invokeSuspend(StackTraceRecoveryTest.kt:21)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n" + ) + nestedMethod(deferred, traces) + deferred.join() + } + + @Test + fun testCompletedAsync() = runTest { + val deferred = async(coroutineContext + NonCancellable) { + throw ExecutionException(null) + } + + deferred.join() + val stacktrace = listOf( + "java.util.concurrent.ExecutionException\n" + + "\t(Current coroutine stacktrace)\n" + + "\tat kotlinx/coroutines/DeferredCoroutine.await\$suspendImpl(Builders.common.kt:99)\n" + + "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.oneMoreNestedMethod(StackTraceRecoveryTest.kt:81)\n" + + "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.nestedMethod(StackTraceRecoveryTest.kt:75)\n" + + "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$testCompletedAsync\$1.invokeSuspend(StackTraceRecoveryTest.kt:71)", + "Caused by: java.util.concurrent.ExecutionException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testCompletedAsync\$1\$deferred\$1.invokeSuspend(StackTraceRecoveryTest.kt:44)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)" + ) + nestedMethod(deferred, stacktrace) + } + + private suspend fun nestedMethod(deferred: Deferred<*>, traces: List) { + 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) + } + } + + @Test + fun testReceiveFromChannel() = runTest { + val channel = Channel() + val job = launch { + expect(2) + channel.close(IllegalArgumentException()) + } + + expect(1) + channelNestedMethod( + channel, listOf( + "java.lang.IllegalArgumentException\n" + + "\t(Current coroutine stacktrace)\n" + + "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.channelNestedMethod(StackTraceRecoveryTest.kt:110)\n" + + "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$testReceiveFromChannel\$1.invokeSuspend(StackTraceRecoveryTest.kt:89)", + "Caused by: java.lang.IllegalArgumentException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testReceiveFromChannel\$1\$job\$1.invokeSuspend(StackTraceRecoveryTest.kt:93)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n" + + "\tat kotlinx.coroutines.DispatchedTask\$DefaultImpls.run(Dispatched.kt:152)" + ) + ) + expect(3) + job.join() + finish(4) + } + + @Test + fun testReceiveFromClosedChannel() = runTest { + val channel = Channel() + channel.close(IllegalArgumentException()) + channelNestedMethod( + channel, listOf( + "java.lang.IllegalArgumentException\n" + + "\t(Current coroutine stacktrace)\n" + + "\tat kotlinx.coroutines.channels.AbstractChannel.receiveResult(AbstractChannel.kt:574)\n" + + "\tat kotlinx.coroutines.channels.AbstractChannel.receive(AbstractChannel.kt:567)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest.channelNestedMethod(StackTraceRecoveryTest.kt:117)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testReceiveFromClosedChannel\$1.invokeSuspend(StackTraceRecoveryTest.kt:111)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)", + "Caused by: java.lang.IllegalArgumentException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testReceiveFromClosedChannel\$1.invokeSuspend(StackTraceRecoveryTest.kt:110)" + ) + ) + } + + private suspend fun channelNestedMethod(channel: Channel, traces: List) { + try { + channel.receive() + expectUnreached() + } catch (e: IllegalArgumentException) { + verifyStackTrace(e, traces) + } + } + + @Test + fun testWithContext() = runTest { + val deferred = async(NonCancellable, start = CoroutineStart.LAZY) { + throw RecoverableTestException() + } + + outerMethod(deferred, listOf( + "kotlinx.coroutines.RecoverableTestException\n" + + "\t(Current coroutine stacktrace)\n" + + "\tat kotlinx/coroutines/DeferredCoroutine.await\$suspendImpl(Builders.common.kt:99)\n" + + "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.innerMethod(StackTraceRecoveryTest.kt:158)\n" + + "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$outerMethod\$2.invokeSuspend(StackTraceRecoveryTest.kt:151)\n" + + "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.outerMethod(StackTraceRecoveryTest.kt:150)\n" + + "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$testWithContext\$1.invokeSuspend(StackTraceRecoveryTest.kt:141)\n", + "Caused by: kotlinx.coroutines.RecoverableTestException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testWithContext\$1\$deferred\$1.invokeSuspend(StackTraceRecoveryTest.kt:143)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n")) + deferred.join() + } + + private suspend fun outerMethod(deferred: Deferred, traces: List) { + withContext(Dispatchers.IO) { + innerMethod(deferred, traces) + } + + assertTrue(true) + } + + private suspend fun innerMethod(deferred: Deferred, traces: List) { + try { + deferred.await() + } catch (e: RecoverableTestException) { + verifyStackTrace(e, traces) + } + } + + @Test + fun testCoroutineScope() = runTest { + val deferred = async(NonCancellable, start = CoroutineStart.LAZY) { + throw RecoverableTestException() + } + + outerScopedMethod(deferred, listOf( + "kotlinx.coroutines.RecoverableTestException\n" + + "\t(Current coroutine stacktrace)\n" + + "\tat kotlinx/coroutines/DeferredCoroutine.await\$suspendImpl(Builders.common.kt:99)\n" + + "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.innerMethod(StackTraceRecoveryTest.kt:158)\n" + + "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$outerScopedMethod\$2.invokeSuspend(StackTraceRecoveryTest.kt:151)\n" + + "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$testCoroutineScope\$1.invokeSuspend(StackTraceRecoveryTest.kt:141)\n", + "Caused by: kotlinx.coroutines.RecoverableTestException\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testCoroutineScope\$1\$deferred\$1.invokeSuspend(StackTraceRecoveryTest.kt:143)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n")) + deferred.join() + } + + private suspend fun outerScopedMethod(deferred: Deferred, traces: List) = coroutineScope { + innerMethod(deferred, traces) + assertTrue(true) + } + + private fun verifyStackTrace(e: Throwable, traces: List) { + val stacktrace = toStackTrace(e) + traces.forEach { + assertTrue( + stacktrace.trimStackTrace().contains(it.trimStackTrace()), + "\nExpected trace element:\n$it\n\nActual stacktrace:\n$stacktrace" + ) + } + + val causes = stacktrace.count("Caused by") + assertNotEquals(0, causes) + assertEquals(causes, traces.map { it.count("Caused by") }.sum()) + } + + private fun toStackTrace(t: Throwable): String { + val sw = StringWriter() as Writer + t.printStackTrace(PrintWriter(sw)) + return sw.toString() + } + + private fun String.trimStackTrace(): String { + return applyBackspace(trimIndent().replace(Regex(":[0-9]+"), "") + .replace("kotlinx_coroutines_core_main", "") // yay source sets + .replace("kotlinx_coroutines_core", "")) + } + + private fun applyBackspace(line: String): String { + val array = line.toCharArray() + val stack = CharArray(array.size) + var stackSize = -1 + for (c in array) { + if (c != '\b') { + stack[++stackSize] = c + } else { + --stackSize + } + } + + return String(stack, 0, stackSize) + } + + private fun String.count(substring: String): Int = split(substring).size - 1 +} diff --git a/core/kotlinx-coroutines-core/test/exceptions/SuppresionTests.kt b/core/kotlinx-coroutines-core/test/exceptions/SuppresionTests.kt index ed021b5c9f..b4527c6474 100644 --- a/core/kotlinx-coroutines-core/test/exceptions/SuppresionTests.kt +++ b/core/kotlinx-coroutines-core/test/exceptions/SuppresionTests.kt @@ -5,15 +5,11 @@ package kotlinx.coroutines.exceptions import kotlinx.coroutines.* -import kotlinx.coroutines.exceptions.* -import kotlinx.coroutines.selects.* +import kotlinx.coroutines.channels.* import java.io.* import kotlin.coroutines.* import kotlin.test.* -/* - * Set of counterparts to common tests which check suppressed exceptions - */ class SuppresionTests : TestBase() { @Test @@ -24,11 +20,11 @@ class SuppresionTests : 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) } @@ -62,13 +58,13 @@ class SuppresionTests : TestBase() { coroutine.invokeOnCompletion(onCancelling = true) { assertTrue(it is ArithmeticException) - assertTrue(it!!.suppressed.isEmpty()) + assertTrue(it.suppressed.isEmpty()) expect(6) } coroutine.invokeOnCompletion { assertTrue(it is ArithmeticException) - checkException(it!!.suppressed[0]) + checkException(it.suppressed[0]) expect(8) } @@ -80,4 +76,28 @@ class SuppresionTests : 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 46f04b032a..de9f6ca6ac 100644 --- a/core/kotlinx-coroutines-core/test/exceptions/WithContextExceptionHandlingTest.kt +++ b/core/kotlinx-coroutines-core/test/exceptions/WithContextExceptionHandlingTest.kt @@ -26,11 +26,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()) @@ -40,30 +40,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()) @@ -89,12 +89,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) @@ -106,13 +106,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()) @@ -122,11 +122,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) @@ -161,9 +161,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) } @@ -179,7 +179,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()) @@ -206,7 +206,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/js/kotlinx-coroutines-core-js/src/internal/Exceptions.kt b/js/kotlinx-coroutines-core-js/src/internal/Exceptions.kt new file mode 100644 index 0000000000..e0435af28e --- /dev/null +++ b/js/kotlinx-coroutines-core-js/src/internal/Exceptions.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +import kotlin.coroutines.* + +internal actual fun 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? +} + +actual typealias StackTraceElement = Any diff --git a/native/kotlinx-coroutines-core-native/src/internal/Exceptions.kt b/native/kotlinx-coroutines-core-native/src/internal/Exceptions.kt new file mode 100644 index 0000000000..923a9b1fa4 --- /dev/null +++ b/native/kotlinx-coroutines-core-native/src/internal/Exceptions.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +import kotlin.coroutines.* + +internal actual fun 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? +} + +actual typealias StackTraceElement = Any From db5335a8d68e730010bbf0a6678ed63adad45049 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 2 Nov 2018 19:33:09 +0300 Subject: [PATCH 02/17] Recover stacktrace on state-machine decision in AbstractContinuation --- .../src/AbstractContinuation.kt | 3 ++- common/kotlinx-coroutines-core-common/src/Dispatched.kt | 2 +- .../test/exceptions/StackTraceRecoveryInHierarchiesTest.kt | 2 ++ .../test/exceptions/StackTraceRecoveryTest.kt | 2 +- .../kotlinx-coroutines-jdk8/test/future/FutureTest.kt | 7 ++----- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/common/kotlinx-coroutines-core-common/src/AbstractContinuation.kt b/common/kotlinx-coroutines-core-common/src/AbstractContinuation.kt index 500961ea42..5509f06635 100644 --- a/common/kotlinx-coroutines-core-common/src/AbstractContinuation.kt +++ b/common/kotlinx-coroutines-core-common/src/AbstractContinuation.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines import kotlinx.atomicfu.* +import kotlinx.coroutines.internal.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* import kotlin.jvm.* @@ -133,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/Dispatched.kt b/common/kotlinx-coroutines-core-common/src/Dispatched.kt index 897b1e12e5..f4c1569bd3 100644 --- a/common/kotlinx-coroutines-core-common/src/Dispatched.kt +++ b/common/kotlinx-coroutines-core-common/src/Dispatched.kt @@ -280,4 +280,4 @@ internal fun DispatchedTask.resume(delegate: Continuation, useMode: In @Suppress("NOTHING_TO_INLINE") -private inline fun Continuation<*>.resumeWithStackTrace(exception: Throwable) = resumeWith(Result.failure(recoverStackTrace(exception, this))) +internal inline fun Continuation<*>.resumeWithStackTrace(exception: Throwable) = resumeWith(Result.failure(recoverStackTrace(exception, this))) diff --git a/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryInHierarchiesTest.kt b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryInHierarchiesTest.kt index f243adde50..cd4a882e7d 100644 --- a/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryInHierarchiesTest.kt +++ b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryInHierarchiesTest.kt @@ -2,6 +2,8 @@ * 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.* diff --git a/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryTest.kt b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryTest.kt index d47ced714d..0860a49cf0 100644 --- a/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryTest.kt +++ b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryTest.kt @@ -204,7 +204,7 @@ class StackTraceRecoveryTest : TestBase() { val causes = stacktrace.count("Caused by") assertNotEquals(0, causes) - assertEquals(causes, traces.map { it.count("Caused by") }.sum()) + assertEquals(traces.map { it.count("Caused by") }.sum(), causes) } private fun toStackTrace(t: Throwable): String { 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 } } From e3b80d9c7198728a5566cb2c74da61ee115e61d6 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 2 Nov 2018 20:06:14 +0300 Subject: [PATCH 03/17] Debug optimizations: * Extract call to suspendCoroutineUninterceptedOrReturn behind recoverability check * Cache exceptions constructors into WeakHashMap in order to avoid expensive ctor lookup --- .../src/JobSupport.kt | 4 +- .../src/internal/Exceptions.kt | 31 +------------ .../src/internal/ExceptionsConstuctor.kt | 45 +++++++++++++++++++ 3 files changed, 48 insertions(+), 32 deletions(-) create mode 100644 core/kotlinx-coroutines-core/src/internal/ExceptionsConstuctor.kt diff --git a/common/kotlinx-coroutines-core-common/src/JobSupport.kt b/common/kotlinx-coroutines-core-common/src/JobSupport.kt index 3cdc81e129..16cb9dad7c 100644 --- a/common/kotlinx-coroutines-core-common/src/JobSupport.kt +++ b/common/kotlinx-coroutines-core-common/src/JobSupport.kt @@ -1080,9 +1080,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren if (state !is Incomplete) { // already complete -- just return result if (state is CompletedExceptionally) { // Slow path to recover stacktrace - suspendCoroutineUninterceptedOrReturn { - throw recoverStackTrace(state.cause, it) - } + recoverAndThrow(state.cause) } return state diff --git a/core/kotlinx-coroutines-core/src/internal/Exceptions.kt b/core/kotlinx-coroutines-core/src/internal/Exceptions.kt index cab8e64b99..5e11ae4d28 100644 --- a/core/kotlinx-coroutines-core/src/internal/Exceptions.kt +++ b/core/kotlinx-coroutines-core/src/internal/Exceptions.kt @@ -65,7 +65,7 @@ internal actual suspend inline fun recoverAndThrow(exception: Throwable): Nothin if (recoveryDisabled(exception)) throw exception suspendCoroutineUninterceptedOrReturn { if (it !is CoroutineStackFrame) throw exception - throw recoverFromStackFrame(exception, it) + throw recoverFromStackFrame(exception, it) } } @@ -86,34 +86,7 @@ internal actual fun unwrap(exception: E): E { private fun recoveryDisabled(exception: E) = !RECOVER_STACKTRACE || !DEBUG || exception is CancellationException || exception is NonRecoverableThrowable -@Suppress("UNCHECKED_CAST") -private fun tryCopyException(exception: E): E? { - /* - * Try to reflectively find constructor(), constructor(message, cause) or constructor(cause). - * Exceptions are shared among coroutines, so we should copy exception before recovering current stacktrace. - */ - var newException: E? = null - try { - val constructors = exception.javaClass.constructors.sortedByDescending { it.parameterTypes.size } - for (constructor in constructors) { - val parameters = constructor.parameterTypes - if (parameters.size == 2 && parameters[0] == String::class.java && parameters[1] == Throwable::class.java) { - newException = constructor.newInstance(exception.message, exception) as E - } else if (parameters.size == 1 && parameters[0] == Throwable::class.java) { - newException = constructor.newInstance(exception) as E - } else if (parameters.isEmpty()) { - newException = (constructor.newInstance() as E).also { it.initCause(exception) } - } - - if (newException != null) { - break - } - } - } catch (e: Exception) { - // Do nothing - } - return newException -} + private fun createStackTrace(continuation: CoroutineStackFrame): ArrayList { val stack = ArrayList() 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..0306e09d4f --- /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() +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 } + return ctor?.invoke(exception) as E? +} From 25a886e9ee0047f755e8d32f496ced3760e7ec64 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 12 Nov 2018 20:04:35 +0300 Subject: [PATCH 04/17] Sanitize class names received from Kotlin debug metadata --- .../src/internal/Exceptions.kt | 11 +++++- .../test/exceptions/StackTraceRecoveryTest.kt | 38 +++++++++---------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/core/kotlinx-coroutines-core/src/internal/Exceptions.kt b/core/kotlinx-coroutines-core/src/internal/Exceptions.kt index 5e11ae4d28..7750ff0e15 100644 --- a/core/kotlinx-coroutines-core/src/internal/Exceptions.kt +++ b/core/kotlinx-coroutines-core/src/internal/Exceptions.kt @@ -90,17 +90,24 @@ private fun recoveryDisabled(exception: E) = private fun createStackTrace(continuation: CoroutineStackFrame): ArrayList { val stack = ArrayList() - continuation.getStackTraceElement()?.let { stack.add(it) } + continuation.getStackTraceElement()?.let { stack.add(sanitize(it)) } var last = continuation while (true) { last = (last as? CoroutineStackFrame)?.callerFrame ?: break - last.getStackTraceElement()?.let { stack.add(it) } + last.getStackTraceElement()?.let { stack.add(sanitize(it)) } } return stack } +internal fun sanitize(element: StackTraceElement): StackTraceElement { + if (!element.className.contains('/')) { + return element + } + // 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) +} internal fun artificialFrame(message: String) = java.lang.StackTraceElement("\b\b\b($message", "\b", "\b", -1) internal fun StackTraceElement.isArtificial() = className.startsWith("\b\b\b") diff --git a/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryTest.kt b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryTest.kt index 0860a49cf0..5ad7e97428 100644 --- a/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryTest.kt +++ b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryTest.kt @@ -32,10 +32,10 @@ class StackTraceRecoveryTest : TestBase() { val traces = listOf( "java.util.concurrent.ExecutionException\n" + "\t(Current coroutine stacktrace)\n" + - "\tat kotlinx/coroutines/DeferredCoroutine.await\$suspendImpl(Builders.common.kt:99)\n" + - "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.oneMoreNestedMethod(StackTraceRecoveryTest.kt:49)\n" + - "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.nestedMethod(StackTraceRecoveryTest.kt:44)\n" + - "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$testAsync\$1.invokeSuspend(StackTraceRecoveryTest.kt:17)\n", + "\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" @@ -54,10 +54,10 @@ class StackTraceRecoveryTest : TestBase() { val stacktrace = listOf( "java.util.concurrent.ExecutionException\n" + "\t(Current coroutine stacktrace)\n" + - "\tat kotlinx/coroutines/DeferredCoroutine.await\$suspendImpl(Builders.common.kt:99)\n" + - "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.oneMoreNestedMethod(StackTraceRecoveryTest.kt:81)\n" + - "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.nestedMethod(StackTraceRecoveryTest.kt:75)\n" + - "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$testCompletedAsync\$1.invokeSuspend(StackTraceRecoveryTest.kt:71)", + "\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)" @@ -92,8 +92,8 @@ class StackTraceRecoveryTest : TestBase() { channel, listOf( "java.lang.IllegalArgumentException\n" + "\t(Current coroutine stacktrace)\n" + - "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.channelNestedMethod(StackTraceRecoveryTest.kt:110)\n" + - "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$testReceiveFromChannel\$1.invokeSuspend(StackTraceRecoveryTest.kt:89)", + "\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" + @@ -142,11 +142,11 @@ class StackTraceRecoveryTest : TestBase() { outerMethod(deferred, listOf( "kotlinx.coroutines.RecoverableTestException\n" + "\t(Current coroutine stacktrace)\n" + - "\tat kotlinx/coroutines/DeferredCoroutine.await\$suspendImpl(Builders.common.kt:99)\n" + - "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.innerMethod(StackTraceRecoveryTest.kt:158)\n" + - "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$outerMethod\$2.invokeSuspend(StackTraceRecoveryTest.kt:151)\n" + - "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.outerMethod(StackTraceRecoveryTest.kt:150)\n" + - "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$testWithContext\$1.invokeSuspend(StackTraceRecoveryTest.kt:141)\n", + "\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")) @@ -178,10 +178,10 @@ class StackTraceRecoveryTest : TestBase() { outerScopedMethod(deferred, listOf( "kotlinx.coroutines.RecoverableTestException\n" + "\t(Current coroutine stacktrace)\n" + - "\tat kotlinx/coroutines/DeferredCoroutine.await\$suspendImpl(Builders.common.kt:99)\n" + - "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest.innerMethod(StackTraceRecoveryTest.kt:158)\n" + - "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$outerScopedMethod\$2.invokeSuspend(StackTraceRecoveryTest.kt:151)\n" + - "\tat kotlinx/coroutines/exceptions/StackTraceRecoveryTest\$testCoroutineScope\$1.invokeSuspend(StackTraceRecoveryTest.kt:141)\n", + "\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")) From 1032f5801a9719fbe67325daec944104eff793a9 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 27 Nov 2018 18:13:47 +0300 Subject: [PATCH 05/17] Scoped builders and exception stacktraces synergy * Recover stacktraces via both fast and slow paths in scoped builders * Merge stracktraces of nested scoped builders, eliminate common prefix so it looks like a flat profile * Optimize stacktrace-related machinery * Copy meaningful part of original exception into recovered one, rename "Current coroutine stacktrace" to "Coroutien boundary" * Rename Exception.kt to StackTraceRecovery.kt to avoid name clash with another Exception.kt --- .../src/Dispatched.kt | 4 +- .../src/JobSupport.kt | 2 +- .../src/Timeout.kt | 6 +- .../src/internal/Scopes.kt | 13 +- ...common.kt => StackTraceRecovery.common.kt} | 0 .../src/intrinsics/Undispatched.kt | 5 +- .../test/TestBase.common.kt | 2 +- .../src/internal/Exceptions.kt | 117 ---------- .../src/internal/ExceptionsConstuctor.kt | 4 +- .../src/internal/StackTraceRecovery.kt | 205 ++++++++++++++++++ .../StackTraceRecoveryNestedChannelsTest.kt | 135 ++++++++++++ .../StackTraceRecoveryNestedScopesTest.kt | 99 +++++++++ ...est.kt => StackTraceRecoveryNestedTest.kt} | 2 +- .../test/exceptions/StackTraceRecoveryTest.kt | 111 +++------- .../test/exceptions/Stacktraces.kt | 47 ++++ ...SuppresionTests.kt => SuppressionTests.kt} | 4 +- .../test/TaskTest.kt | 24 +- .../{Exceptions.kt => StackTraceRecovery.kt} | 0 .../{Exceptions.kt => StackTraceRecovery.kt} | 0 19 files changed, 554 insertions(+), 226 deletions(-) rename common/kotlinx-coroutines-core-common/src/internal/{Exceptions.common.kt => StackTraceRecovery.common.kt} (100%) delete mode 100644 core/kotlinx-coroutines-core/src/internal/Exceptions.kt create mode 100644 core/kotlinx-coroutines-core/src/internal/StackTraceRecovery.kt create mode 100644 core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryNestedChannelsTest.kt create mode 100644 core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryNestedScopesTest.kt rename core/kotlinx-coroutines-core/test/exceptions/{StackTraceRecoveryInHierarchiesTest.kt => StackTraceRecoveryNestedTest.kt} (97%) create mode 100644 core/kotlinx-coroutines-core/test/exceptions/Stacktraces.kt rename core/kotlinx-coroutines-core/test/exceptions/{SuppresionTests.kt => SuppressionTests.kt} (96%) rename js/kotlinx-coroutines-core-js/src/internal/{Exceptions.kt => StackTraceRecovery.kt} (100%) rename native/kotlinx-coroutines-core-native/src/internal/{Exceptions.kt => StackTraceRecovery.kt} (100%) diff --git a/common/kotlinx-coroutines-core-common/src/Dispatched.kt b/common/kotlinx-coroutines-core-common/src/Dispatched.kt index f4c1569bd3..91fb662ed9 100644 --- a/common/kotlinx-coroutines-core-common/src/Dispatched.kt +++ b/common/kotlinx-coroutines-core-common/src/Dispatched.kt @@ -280,4 +280,6 @@ internal fun DispatchedTask.resume(delegate: Continuation, useMode: In @Suppress("NOTHING_TO_INLINE") -internal inline fun Continuation<*>.resumeWithStackTrace(exception: Throwable) = resumeWith(Result.failure(recoverStackTrace(exception, this))) +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 16cb9dad7c..011c9cf4f1 100644 --- a/common/kotlinx-coroutines-core-common/src/JobSupport.kt +++ b/common/kotlinx-coroutines-core-common/src/JobSupport.kt @@ -243,7 +243,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren var suppressed = false for (exception in exceptions) { val unwrapped = unwrap(exception) - if (unwrapped !== rootCause && unwrapped !is CancellationException && seenExceptions.add(exception)) { + if (unwrapped !== rootCause && unwrapped !is CancellationException && seenExceptions.add(unwrapped)) { rootCause.addSuppressedThrowable(unwrapped) suppressed = true } diff --git a/common/kotlinx-coroutines-core-common/src/Timeout.kt b/common/kotlinx-coroutines-core-common/src/Timeout.kt index 3a67425dd2..14089907f5 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/internal/Scopes.kt b/common/kotlinx-coroutines-core-common/src/internal/Scopes.kt index efa8f04ded..56a32bc075 100644 --- a/common/kotlinx-coroutines-core-common/src/internal/Scopes.kt +++ b/common/kotlinx-coroutines-core-common/src/internal/Scopes.kt @@ -21,13 +21,20 @@ internal open class ScopeCoroutine( @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/Exceptions.common.kt b/common/kotlinx-coroutines-core-common/src/internal/StackTraceRecovery.common.kt similarity index 100% rename from common/kotlinx-coroutines-core-common/src/internal/Exceptions.common.kt rename to common/kotlinx-coroutines-core-common/src/internal/StackTraceRecovery.common.kt diff --git a/common/kotlinx-coroutines-core-common/src/intrinsics/Undispatched.kt b/common/kotlinx-coroutines-core-common/src/intrinsics/Undispatched.kt index a46fe4ac2d..fcfe8545a6 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.* @@ -126,8 +127,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 7bfcf29998..f705c79e73 100644 --- a/common/kotlinx-coroutines-core-common/test/TestBase.common.kt +++ b/common/kotlinx-coroutines-core-common/test/TestBase.common.kt @@ -29,7 +29,7 @@ public class TestException1(message: String? = null) : Throwable(message), NonRe public class TestException2(message: String? = null) : Throwable(message), NonRecoverableThrowable public class TestException3(message: String? = null) : Throwable(message), NonRecoverableThrowable public class TestRuntimeException(message: String? = null) : RuntimeException(message), NonRecoverableThrowable -public class RecoverableTestException(message: String? = null) : Throwable(message) +public class RecoverableTestException(message: String? = null) : RuntimeException(message) public fun wrapperDispatcher(context: CoroutineContext): CoroutineContext { val dispatcher = context[ContinuationInterceptor] as CoroutineDispatcher diff --git a/core/kotlinx-coroutines-core/src/internal/Exceptions.kt b/core/kotlinx-coroutines-core/src/internal/Exceptions.kt deleted file mode 100644 index 7750ff0e15..0000000000 --- a/core/kotlinx-coroutines-core/src/internal/Exceptions.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.internal - -import kotlinx.coroutines.* -import java.util.* -import kotlin.coroutines.* -import kotlin.coroutines.intrinsics.* - -internal actual fun recoverStackTrace(exception: E): E { - if (recoveryDisabled(exception)) { - return exception - } - - val copy = tryCopyException(exception) ?: return exception - return copy.sanitizeStackTrace() -} - -private fun E.sanitizeStackTrace(): E { - val size = stackTrace.size - - var lastIntrinsic = -1 - for (i in 0 until size) { - val name = stackTrace[i].className - if ("kotlinx.coroutines.internal.ExceptionsKt" == name) { - lastIntrinsic = i - } - } - - val startIndex = lastIntrinsic + 1 - val trace = Array(size - lastIntrinsic) { - if (it == 0) { - artificialFrame("Current coroutine stacktrace") - } else { - stackTrace[startIndex + it - 1] - } - } - - stackTrace = trace - return this -} - -internal actual fun 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 { - val newException = tryCopyException(exception) ?: return exception - val stacktrace = createStackTrace(continuation) - if (stacktrace.isEmpty()) return exception - stacktrace.add(0, artificialFrame("Current coroutine stacktrace")) - newException.stackTrace = stacktrace.toTypedArray() - return newException -} - - -@Suppress("NOTHING_TO_INLINE") -internal actual suspend inline fun recoverAndThrow(exception: Throwable): Nothing { - if (recoveryDisabled(exception)) throw exception - suspendCoroutineUninterceptedOrReturn { - if (it !is CoroutineStackFrame) throw exception - throw recoverFromStackFrame(exception, it) - } -} - -internal actual fun unwrap(exception: E): E { - if (recoveryDisabled(exception)) { - return exception - } - - val element = exception.stackTrace.firstOrNull() ?: return exception - if (element.isArtificial()) { - @Suppress("UNCHECKED_CAST") - return exception.cause as? E ?: exception - } else { - return exception - } -} - -private fun recoveryDisabled(exception: E) = - !RECOVER_STACKTRACE || !DEBUG || exception is CancellationException || exception is NonRecoverableThrowable - - - -private fun createStackTrace(continuation: CoroutineStackFrame): ArrayList { - val stack = ArrayList() - 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 -} - -internal fun sanitize(element: StackTraceElement): StackTraceElement { - if (!element.className.contains('/')) { - return element - } - - // 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) -} -internal fun artificialFrame(message: String) = java.lang.StackTraceElement("\b\b\b($message", "\b", "\b", -1) -internal fun StackTraceElement.isArtificial() = className.startsWith("\b\b\b") - -@Suppress("ACTUAL_WITHOUT_EXPECT") -actual typealias CoroutineStackFrame = kotlin.coroutines.jvm.internal.CoroutineStackFrame - -actual typealias StackTraceElement = java.lang.StackTraceElement diff --git a/core/kotlinx-coroutines-core/src/internal/ExceptionsConstuctor.kt b/core/kotlinx-coroutines-core/src/internal/ExceptionsConstuctor.kt index 0306e09d4f..a73b4d791b 100644 --- a/core/kotlinx-coroutines-core/src/internal/ExceptionsConstuctor.kt +++ b/core/kotlinx-coroutines-core/src/internal/ExceptionsConstuctor.kt @@ -9,7 +9,8 @@ import java.util.concurrent.locks.* import kotlin.concurrent.* private val cacheLock = ReentrantReadWriteLock() -private val exceptionConstructors: WeakHashMap, (Throwable) -> Throwable?> = WeakHashMap() +// 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? { @@ -24,7 +25,6 @@ internal fun tryCopyException(exception: E): E? { * 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 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..fdfa69c401 --- /dev/null +++ b/core/kotlinx-coroutines-core/src/internal/StackTraceRecovery.kt @@ -0,0 +1,205 @@ +/* + * 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_STACKTRACE || !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 +} + +internal 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) +} +internal 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") +actual typealias CoroutineStackFrame = kotlin.coroutines.jvm.internal.CoroutineStackFrame + +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..be63229447 --- /dev/null +++ b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryNestedChannelsTest.kt @@ -0,0 +1,135 @@ +/* + * 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 { + 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:33)\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/StackTraceRecoveryInHierarchiesTest.kt b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryNestedTest.kt similarity index 97% rename from core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryInHierarchiesTest.kt rename to core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryNestedTest.kt index cd4a882e7d..5073b7fdfa 100644 --- a/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryInHierarchiesTest.kt +++ b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryNestedTest.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.* import org.junit.Test import kotlin.test.* -class StackTraceRecoveryInHierarchiesTest : TestBase() { +class StackTraceRecoveryNestedTest : TestBase() { @Test fun testNestedAsync() = runTest { diff --git a/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryTest.kt b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryTest.kt index 5ad7e97428..f96da49e86 100644 --- a/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryTest.kt +++ b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryTest.kt @@ -31,7 +31,8 @@ class StackTraceRecoveryTest : TestBase() { val deferred = createDeferred(3) val traces = listOf( "java.util.concurrent.ExecutionException\n" + - "\t(Current coroutine stacktrace)\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" + @@ -40,7 +41,7 @@ class StackTraceRecoveryTest : TestBase() { "\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) + nestedMethod(deferred, *traces.toTypedArray()) deferred.join() } @@ -53,7 +54,8 @@ class StackTraceRecoveryTest : TestBase() { deferred.join() val stacktrace = listOf( "java.util.concurrent.ExecutionException\n" + - "\t(Current coroutine stacktrace)\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" + @@ -62,20 +64,20 @@ class StackTraceRecoveryTest : TestBase() { "\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) + nestedMethod(deferred, *stacktrace.toTypedArray()) } - private suspend fun nestedMethod(deferred: Deferred<*>, traces: List) { - oneMoreNestedMethod(deferred, traces) + private suspend fun nestedMethod(deferred: Deferred<*>, vararg traces: String) { + oneMoreNestedMethod(deferred, *traces) assertTrue(true) // Prevent tail-call optimization } - private suspend fun oneMoreNestedMethod(deferred: Deferred<*>, traces: List) { + private suspend fun oneMoreNestedMethod(deferred: Deferred<*>, vararg traces: String) { try { deferred.await() expectUnreached() } catch (e: ExecutionException) { - verifyStackTrace(e, traces) + verifyStackTrace(e, *traces) } } @@ -89,17 +91,16 @@ class StackTraceRecoveryTest : TestBase() { expect(1) channelNestedMethod( - channel, listOf( + channel, "java.lang.IllegalArgumentException\n" + - "\t(Current coroutine stacktrace)\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\$DefaultImpls.run(Dispatched.kt:152)" - ) - ) + "\tat kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:152)") expect(3) job.join() finish(4) @@ -110,26 +111,23 @@ class StackTraceRecoveryTest : TestBase() { val channel = Channel() channel.close(IllegalArgumentException()) channelNestedMethod( - channel, listOf( + channel, "java.lang.IllegalArgumentException\n" + - "\t(Current coroutine stacktrace)\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" + - "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)", + "\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)" - ) - ) + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryTest\$testReceiveFromClosedChannel\$1.invokeSuspend(StackTraceRecoveryTest.kt:110)") } - private suspend fun channelNestedMethod(channel: Channel, traces: List) { + private suspend fun channelNestedMethod(channel: Channel, vararg traces: String) { try { channel.receive() expectUnreached() } catch (e: IllegalArgumentException) { - verifyStackTrace(e, traces) + verifyStackTrace(e, *traces) } } @@ -139,9 +137,10 @@ class StackTraceRecoveryTest : TestBase() { throw RecoverableTestException() } - outerMethod(deferred, listOf( + outerMethod(deferred, "kotlinx.coroutines.RecoverableTestException\n" + - "\t(Current coroutine stacktrace)\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" + @@ -149,23 +148,23 @@ class StackTraceRecoveryTest : TestBase() { "\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")) + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n") deferred.join() } - private suspend fun outerMethod(deferred: Deferred, traces: List) { + private suspend fun outerMethod(deferred: Deferred, vararg traces: String) { withContext(Dispatchers.IO) { - innerMethod(deferred, traces) + innerMethod(deferred, *traces) } assertTrue(true) } - private suspend fun innerMethod(deferred: Deferred, traces: List) { + private suspend fun innerMethod(deferred: Deferred, vararg traces: String) { try { deferred.await() } catch (e: RecoverableTestException) { - verifyStackTrace(e, traces) + verifyStackTrace(e, *traces) } } @@ -175,64 +174,22 @@ class StackTraceRecoveryTest : TestBase() { throw RecoverableTestException() } - outerScopedMethod(deferred, listOf( + outerScopedMethod(deferred, "kotlinx.coroutines.RecoverableTestException\n" + - "\t(Current coroutine stacktrace)\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")) + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n") deferred.join() } - private suspend fun outerScopedMethod(deferred: Deferred, traces: List) = coroutineScope { - innerMethod(deferred, traces) + private suspend fun outerScopedMethod(deferred: Deferred, vararg traces: String) = coroutineScope { + innerMethod(deferred, *traces) assertTrue(true) } - - private fun verifyStackTrace(e: Throwable, traces: List) { - 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) - } - - private fun toStackTrace(t: Throwable): String { - val sw = StringWriter() as Writer - t.printStackTrace(PrintWriter(sw)) - return sw.toString() - } - - private fun String.trimStackTrace(): String { - return applyBackspace(trimIndent().replace(Regex(":[0-9]+"), "") - .replace("kotlinx_coroutines_core_main", "") // yay source sets - .replace("kotlinx_coroutines_core", "")) - } - - private fun applyBackspace(line: String): String { - val array = line.toCharArray() - val stack = CharArray(array.size) - var stackSize = -1 - for (c in array) { - if (c != '\b') { - stack[++stackSize] = c - } else { - --stackSize - } - } - - return String(stack, 0, stackSize) - } - - private fun String.count(substring: String): Int = split(substring).size - 1 } diff --git a/core/kotlinx-coroutines-core/test/exceptions/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/SuppresionTests.kt b/core/kotlinx-coroutines-core/test/exceptions/SuppressionTests.kt similarity index 96% rename from core/kotlinx-coroutines-core/test/exceptions/SuppresionTests.kt rename to core/kotlinx-coroutines-core/test/exceptions/SuppressionTests.kt index b4527c6474..4aee03aae9 100644 --- a/core/kotlinx-coroutines-core/test/exceptions/SuppresionTests.kt +++ b/core/kotlinx-coroutines-core/test/exceptions/SuppressionTests.kt @@ -10,7 +10,7 @@ import java.io.* import kotlin.coroutines.* import kotlin.test.* -class SuppresionTests : TestBase() { +class SuppressionTests : TestBase() { @Test fun testCancellationTransparency() = runTest { @@ -41,7 +41,7 @@ class SuppresionTests : TestBase() { override fun onCancellation(cause: Throwable?) { assertTrue(cause is ArithmeticException) - assertTrue(cause!!.suppressed.isEmpty()) + assertTrue(cause.suppressed.isEmpty()) expect(5) } 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/Exceptions.kt b/js/kotlinx-coroutines-core-js/src/internal/StackTraceRecovery.kt similarity index 100% rename from js/kotlinx-coroutines-core-js/src/internal/Exceptions.kt rename to js/kotlinx-coroutines-core-js/src/internal/StackTraceRecovery.kt diff --git a/native/kotlinx-coroutines-core-native/src/internal/Exceptions.kt b/native/kotlinx-coroutines-core-native/src/internal/StackTraceRecovery.kt similarity index 100% rename from native/kotlinx-coroutines-core-native/src/internal/Exceptions.kt rename to native/kotlinx-coroutines-core-native/src/internal/StackTraceRecovery.kt From 41319d58cefda70ef9749c8d16d63559f347534e Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 7 Dec 2018 19:10:59 +0300 Subject: [PATCH 06/17] Cache missing exception constructors to speed-up non-copyable exception recovery --- .../src/internal/ExceptionsConstuctor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/kotlinx-coroutines-core/src/internal/ExceptionsConstuctor.kt b/core/kotlinx-coroutines-core/src/internal/ExceptionsConstuctor.kt index a73b4d791b..5695060785 100644 --- a/core/kotlinx-coroutines-core/src/internal/ExceptionsConstuctor.kt +++ b/core/kotlinx-coroutines-core/src/internal/ExceptionsConstuctor.kt @@ -40,6 +40,6 @@ internal fun tryCopyException(exception: E): E? { } } - cacheLock.write { exceptionConstructors[exception.javaClass] = ctor } + cacheLock.write { exceptionConstructors[exception.javaClass] = (ctor ?: { null }) } return ctor?.invoke(exception) as E? } From 5a22d80aad5677eadf74851f7ef05dadf3860530 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 7 Dec 2018 19:49:37 +0300 Subject: [PATCH 07/17] Add workaround for KT-27190 in test --- .../test/exceptions/StackTraceRecoveryNestedChannelsTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryNestedChannelsTest.kt b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryNestedChannelsTest.kt index be63229447..d922edef0c 100644 --- a/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryNestedChannelsTest.kt +++ b/core/kotlinx-coroutines-core/test/exceptions/StackTraceRecoveryNestedChannelsTest.kt @@ -34,6 +34,7 @@ class StackTraceRecoveryNestedChannelsTest : TestBase() { channel.close(RecoverableTestException()) try { + yield() // Will be fixed in 1.3.20 after KT-27190 sendWithContext(coroutineContext) } catch (e: Exception) { verifyStackTrace(e, @@ -51,7 +52,7 @@ class StackTraceRecoveryNestedChannelsTest : TestBase() { "\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:33)\n" + + "\tat kotlinx.coroutines.exceptions.StackTraceRecoveryNestedChannelsTest\$testOfferWithCurrentContext\$1.invokeSuspend(StackTraceRecoveryNestedChannelsTest.kt:34)\n" + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n") } } From c7239ac4915df344add7386c8aa19038499f5e4b Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 10 Dec 2018 11:41:00 +0300 Subject: [PATCH 08/17] Debug agent to track alive coroutines * Can be installed dynamically or from command line * Captures coroutine creation stacktrace and stores it in completion, automatically enhancing stacktrace recovery mechanism * Allows to dump and introspect all active coroutines * Allows to dump Job hierarchy * When installed from command line, dumps all coroutines on kill -5 * Probe support in undispatched coroutines --- README.md | 2 + RELEASE.md | 2 +- binary-compatibility-validator/build.gradle | 1 + .../kotlinx-coroutines-core.txt | 1 + .../kotlinx-coroutines-debug.txt | 39 +++ .../src/JobSupport.kt | 5 +- .../src/internal/ProbesSupport.common.kt | 9 + .../src/intrinsics/Undispatched.kt | 30 +- core/README.md | 4 +- core/kotlinx-coroutines-core/src/Debug.kt | 4 +- .../src/internal/ProbesSupport.kt | 11 + .../src/internal/StackTraceRecovery.kt | 9 +- core/kotlinx-coroutines-debug/README.md | 118 ++++++++ core/kotlinx-coroutines-debug/build.gradle | 18 ++ .../src/debug/AgentPremain.kt | 25 ++ .../src/debug/CoroutineState.kt | 84 ++++++ .../src/debug/DebugProbes.kt | 123 ++++++++ .../src/debug/internal/DebugProbesImpl.kt | 282 ++++++++++++++++++ .../src/debug/internal/NoOpProbes.kt | 19 ++ .../test/debug/CoroutinesDumpTest.kt | 154 ++++++++++ .../test/debug/DebugProbesTest.kt | 104 +++++++ .../test/debug/HierarchyToStringTest.kt | 81 +++++ .../test/debug/SanitizedProbesTest.kt | 114 +++++++ .../test/debug/StartModeProbesTest.kt | 157 ++++++++++ .../test/debug/StracktraceUtils.kt | 101 +++++++ gradle.properties | 1 + .../src/internal/ProbesSupport.kt | 10 + knit/resources/knit.properties | 2 +- .../src/internal/ProbesSupport.kt | 10 + settings.gradle | 1 + 30 files changed, 1500 insertions(+), 21 deletions(-) create mode 100644 binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt create mode 100644 common/kotlinx-coroutines-core-common/src/internal/ProbesSupport.common.kt create mode 100644 core/kotlinx-coroutines-core/src/internal/ProbesSupport.kt create mode 100644 core/kotlinx-coroutines-debug/README.md create mode 100644 core/kotlinx-coroutines-debug/build.gradle create mode 100644 core/kotlinx-coroutines-debug/src/debug/AgentPremain.kt create mode 100644 core/kotlinx-coroutines-debug/src/debug/CoroutineState.kt create mode 100644 core/kotlinx-coroutines-debug/src/debug/DebugProbes.kt create mode 100644 core/kotlinx-coroutines-debug/src/debug/internal/DebugProbesImpl.kt create mode 100644 core/kotlinx-coroutines-debug/src/debug/internal/NoOpProbes.kt create mode 100644 core/kotlinx-coroutines-debug/test/debug/CoroutinesDumpTest.kt create mode 100644 core/kotlinx-coroutines-debug/test/debug/DebugProbesTest.kt create mode 100644 core/kotlinx-coroutines-debug/test/debug/HierarchyToStringTest.kt create mode 100644 core/kotlinx-coroutines-debug/test/debug/SanitizedProbesTest.kt create mode 100644 core/kotlinx-coroutines-debug/test/debug/StartModeProbesTest.kt create mode 100644 core/kotlinx-coroutines-debug/test/debug/StracktraceUtils.kt create mode 100644 js/kotlinx-coroutines-core-js/src/internal/ProbesSupport.kt create mode 100644 native/kotlinx-coroutines-core-native/src/internal/ProbesSupport.kt 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 d7b990c417..21e8e59076 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt @@ -349,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..fa607fbdfa --- /dev/null +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt @@ -0,0 +1,39 @@ +public final class kotlinx/coroutines/debug/CoroutineState { + public final fun component1 ()Lkotlin/coroutines/Continuation; + public final fun component2 ()Ljava/util/List; + public final fun copy (Lkotlin/coroutines/Continuation;Ljava/util/List;J)Lkotlinx/coroutines/debug/CoroutineState; + public static synthetic fun copy$default (Lkotlinx/coroutines/debug/CoroutineState;Lkotlin/coroutines/Continuation;Ljava/util/List;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/common/kotlinx-coroutines-core-common/src/JobSupport.kt b/common/kotlinx-coroutines-core-common/src/JobSupport.kt index 011c9cf4f1..9ca9385820 100644 --- a/common/kotlinx-coroutines-core-common/src/JobSupport.kt +++ b/common/kotlinx-coroutines-core-common/src/JobSupport.kt @@ -925,7 +925,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.** 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/intrinsics/Undispatched.kt b/common/kotlinx-coroutines-core-common/src/intrinsics/Undispatched.kt index fcfe8545a6..19dc6d28f0 100644 --- a/common/kotlinx-coroutines-core-common/src/intrinsics/Undispatched.kt +++ b/common/kotlinx-coroutines-core-common/src/intrinsics/Undispatched.kt @@ -15,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) } } @@ -26,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) } } @@ -37,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) } } } @@ -50,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) } } 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 3796c400db..a1f53cd776 100644 --- a/core/kotlinx-coroutines-core/src/Debug.kt +++ b/core/kotlinx-coroutines-core/src/Debug.kt @@ -41,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() @@ -50,7 +51,8 @@ internal val DEBUG = systemProp(DEBUG_PROPERTY_NAME).let { value -> } } -internal val RECOVER_STACKTRACE = systemProp(STACKTRACE_RECOVERY_PROPERTY_NAME, true) +@JvmField +internal val RECOVER_STACKTRACES = systemProp(STACKTRACE_RECOVERY_PROPERTY_NAME, true) // internal debugging tools 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 index fdfa69c401..0b3181f302 100644 --- a/core/kotlinx-coroutines-core/src/internal/StackTraceRecovery.kt +++ b/core/kotlinx-coroutines-core/src/internal/StackTraceRecovery.kt @@ -165,7 +165,7 @@ internal actual fun unwrap(exception: E): E { } private fun recoveryDisabled(exception: E) = - !RECOVER_STACKTRACE || !DEBUG || exception is CancellationException || exception is NonRecoverableThrowable + !RECOVER_STACKTRACES || !DEBUG || exception is CancellationException || exception is NonRecoverableThrowable private fun createStackTrace(continuation: CoroutineStackFrame): ArrayDeque { val stack = ArrayDeque() @@ -179,14 +179,17 @@ private fun createStackTrace(continuation: CoroutineStackFrame): ArrayDeque.frameIndex(methodName: String) = indexOfFirst { methodName == it.className } diff --git a/core/kotlinx-coroutines-debug/README.md b/core/kotlinx-coroutines-debug/README.md new file mode 100644 index 0000000000..e97f28f7c6 --- /dev/null +++ b/core/kotlinx-coroutines-debug/README.md @@ -0,0 +1,118 @@ +# 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 alive coroutines. +Main entry point to debug facilities is [DebugProbes]. +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 list of such coroutines via [DebugProbes.dumpCoroutinesState] or dump isolated parts +of coroutines hierarchies referenced by [Job] instance using [DebugProbes.printHierarchy]. + +### Using as JVM agent +Additionally, it is possible to use this module as standalone JVM agent to enable debug probes on the application startup. +You can run your application with 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..0b7a28c68e --- /dev/null +++ b/core/kotlinx-coroutines-debug/build.gradle @@ -0,0 +1,18 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +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" + // For local runs + // attributes "Main-Class": "kotlinx.coroutines.debug.Playground" + // attributes "Class-Path": configurations.compile.collect { it.absolutePath }.join(" ") + } +} diff --git a/core/kotlinx-coroutines-debug/src/debug/AgentPremain.kt b/core/kotlinx-coroutines-debug/src/debug/AgentPremain.kt new file mode 100644 index 0000000000..569134a0f5 --- /dev/null +++ b/core/kotlinx-coroutines-debug/src/debug/AgentPremain.kt @@ -0,0 +1,25 @@ +/* + * 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() { + val signal = Signal("TRAP") // kill -5 + Signal.handle(signal, { DebugProbes.dumpCoroutines() }) + } +} diff --git a/core/kotlinx-coroutines-debug/src/debug/CoroutineState.kt b/core/kotlinx-coroutines-debug/src/debug/CoroutineState.kt new file mode 100644 index 0000000000..eb89a2f240 --- /dev/null +++ b/core/kotlinx-coroutines-debug/src/debug/CoroutineState.kt @@ -0,0 +1,84 @@ +/* + * 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.* +import kotlin.coroutines.* + +/** + * Class describing coroutine state. + */ +@ExperimentalCoroutinesApi +public data class CoroutineState internal constructor( + public val continuation: Continuation<*>, + public val creationStackTrace: List, + 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] + + /** + * Last observed [state][State] of the coroutine. + */ + public val state: State get() = _state + + // Copy constructor + internal constructor(coroutine: Continuation<*>, state: CoroutineState) : this(coroutine, + state.creationStackTrace, + state.sequenceNumber) { + _state = state.state + this.lastObservedFrame = state.lastObservedFrame + } + + private var _state: State = State.CREATED + + private var lastObservedFrame: CoroutineStackFrame? = null + + 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, // Not yet started + RUNNING, + SUSPENDED +} diff --git a/core/kotlinx-coroutines-debug/src/debug/DebugProbes.kt b/core/kotlinx-coroutines-debug/src/debug/DebugProbes.kt new file mode 100644 index 0000000000..168c6072d4 --- /dev/null +++ b/core/kotlinx-coroutines-debug/src/debug/DebugProbes.kt @@ -0,0 +1,123 @@ +/* + * 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 [Continuation] machinery. + * It slows down all coroutine-related code, but in return provides a lot of debug 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 continuation is stored in a weak hash map, thus adding additional GC pressure. + * * On every created continuation, stacktrace of the current thread is dumped. + * * On every `resume` and `suspend`, [WeakHashMap] is updated under a global lock. + * + * **WARNING: DO NOT USE DEBUG PROBES IN PRODUCTION ENVIRONMENT.** + */ +@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 user's 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. + * + * **WARNING: DO NOT USE DEBUG PROBES IN PRODUCTION ENVIRONMENT** + */ + 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. + * + * **WARNING: DO NOT USE DEBUG PROBES IN PRODUCTION ENVIRONMENT** + */ + 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 given [out]. + */ + public fun printHierarchy(job: Job, out: PrintStream = System.out) = + out.println(DebugProbesImpl.hierarchyToString(job)) + + /** + * Returns all alive coroutine states. + * Resulting collection represents a consistent snapshot of all alive coroutines at the moment of invocation. + */ + public fun dumpCoroutinesState(): List = DebugProbesImpl.dumpCoroutinesState() + + /** + * Dumps all active coroutines into given output stream. + * Resulting collection represents a consistent snapshot of all alive coroutines at the moment of invocation. + * Output of this method is similar to `jstack` or full thread dump, so this method can and should be used as 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/debug/internal/DebugProbesImpl.kt b/core/kotlinx-coroutines-debug/src/debug/internal/DebugProbesImpl.kt new file mode 100644 index 0000000000..7f3be13f1b --- /dev/null +++ b/core/kotlinx-coroutines-debug/src/debug/internal/DebugProbesImpl.kt @@ -0,0 +1,282 @@ +/* + * 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.* +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.* + +/** + * 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>() + 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() { + if (installations == 0) error("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 { + if (!isInstalled) { + error("Debug probes are not installed") + } + + val jobToStack = capturedCoroutines + .filterKeys { it.delegate.context[Job] != null } + .mapKeys { it.key.delegate.context[Job]!! } + + val sb = StringBuilder() + job.build(jobToStack, sb, "") + return sb.toString() + } + + 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) { + builder.append("Coroutine: $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 { + if (!isInstalled) { + error("Debug probes are not installed") + } + + return capturedCoroutines.entries.asSequence() + .map { CoroutineState(it.key.delegate, it.value) } + .sortedBy { it.sequenceNumber } + .toList() + } + + @Synchronized + public fun dumpCoroutines(out: PrintStream) { + if (!isInstalled) { + error("Debug probes are not installed") + } + + // Avoid inference with other out/err invocations + val resultingString = buildString { + 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()) + } + } + } + + // Move it out of synchronization? + out.println(resultingString) + } + + private fun StringBuilder.printStackTrace(frames: List) { + frames.forEach { frame -> + append("\n\tat $frame") + } + } + + @Synchronized + internal fun probeCoroutineResumed(frame: Continuation<*>) = updateState(frame, State.RUNNING) + + @Synchronized + 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() + val coroutineState = capturedCoroutines[owner] + if (coroutineState == null) { + warn(frame, state) + return + } + + coroutineState.updateState(state, frame) + } + + private fun Continuation<*>.owner(): ArtificialStackFrame<*>? { + var frame = this as? CoroutineStackFrame ?: return null + while (true) { + if (frame is ArtificialStackFrame<*>) return frame + val completion = frame.callerFrame ?: return null + frame = completion + } + } + + @Synchronized + internal fun probeCoroutineCreated(completion: Continuation): Continuation { + if (!isInstalled) { + 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 = sanitizedStackTrace(Exception()) + val frames = ArrayList(stacktrace.size) + for ((index, frame) in stacktrace.reversed().withIndex()) { + frames += object : CoroutineStackFrame { + override val callerFrame: CoroutineStackFrame? + get() = if (index == 0) null else frames[index - 1] + + override fun getStackTraceElement(): StackTraceElement = frame + } + } + + val result = ArtificialStackFrame(completion, frames.last()!!) + capturedCoroutines[result] = CoroutineState(completion, stacktrace.slice(1 until stacktrace.size), ++sequenceNumber) + return result + } + + @Synchronized + private fun probeCoroutineCompleted(coroutine: ArtificialStackFrame<*>) { + capturedCoroutines.remove(coroutine) + } + + private class ArtificialStackFrame(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 sanitizedStackTrace(throwable: T): Array { + val stackTrace = throwable.stackTrace + val size = stackTrace.size + + var probeIndex = -1 + for (i in 0 until size) { + val name = stackTrace[i].className + if ("kotlin.coroutines.jvm.internal.DebugProbesKt" == name) { + probeIndex = i + } + } + + if (!DebugProbes.sanitizeStackTraces) { + return Array(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) + Thread.sleep(1) + 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.toTypedArray() + } + + private val StackTraceElement.isInternalMethod: Boolean get() = className.startsWith("kotlinx.coroutines") + + private fun warn(frame: Continuation<*>, state: State) { + // TODO make this warning configurable or not a warning at all + System.err.println("Failed to find an owner of the frame $frame while transferring it to the state $state") + } +} diff --git a/core/kotlinx-coroutines-debug/src/debug/internal/NoOpProbes.kt b/core/kotlinx-coroutines-debug/src/debug/internal/NoOpProbes.kt new file mode 100644 index 0000000000..d32eeb674b --- /dev/null +++ b/core/kotlinx-coroutines-debug/src/debug/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/debug/CoroutinesDumpTest.kt b/core/kotlinx-coroutines-debug/test/debug/CoroutinesDumpTest.kt new file mode 100644 index 0000000000..0b97a679c3 --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/debug/CoroutinesDumpTest.kt @@ -0,0 +1,154 @@ +/* + * 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 java.io.* +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) + deferred.cancel() + runBlocking { deferred.join() } + } + + @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)") + deferred.cancel() + runBlocking { deferred.join() } + } + + @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)") + deferred.cancel() + runBlocking { deferred.join() } + } + + @Test + fun testFinishedCoroutineRemoved() = synchronized(monitor) { + val deferred = GlobalScope.async { + activeMethod(shouldSuspend = true) + } + + awaitCoroutineStarted() + deferred.cancel() + runBlocking { deferred.join() } + 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/debug/DebugProbesTest.kt b/core/kotlinx-coroutines-debug/test/debug/DebugProbesTest.kt new file mode 100644 index 0000000000..04c1b05f00 --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/debug/DebugProbesTest.kt @@ -0,0 +1,104 @@ +/* + * 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 { + 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/debug/HierarchyToStringTest.kt b/core/kotlinx-coroutines-debug/test/debug/HierarchyToStringTest.kt new file mode 100644 index 0000000000..a027a54fc0 --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/debug/HierarchyToStringTest.kt @@ -0,0 +1,81 @@ +/* + * 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 testHierarchy() = runBlocking { + val root = 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() + } + } + } + + repeat(4) { yield() } + expect(6) + val tab = '\t' + val expectedString = """ + Coroutine: "coroutine#2":StandaloneCoroutine{Completing} + $tab"foo#3":DeferredCoroutine{Active}, continuation is SUSPENDED at line kotlinx.coroutines.debug.HierarchyToStringTest${'$'}testHierarchy${'$'}1${'$'}root${'$'}1${'$'}1.invokeSuspend(HierarchyToStringTest.kt:30) + $tab"coroutine#4":ActorCoroutine{Active}, continuation is SUSPENDED at line kotlinx.coroutines.debug.HierarchyToStringTest${'$'}testHierarchy${'$'}1${'$'}root${'$'}1${'$'}2.invokeSuspend(HierarchyToStringTest.kt:40) + $tab$tab"coroutine#5":StandaloneCoroutine{Active}, continuation is SUSPENDED at line kotlinx.coroutines.debug.HierarchyToStringTest${'$'}testHierarchy${'$'}1${'$'}root${'$'}1${'$'}2${'$'}job$1.invokeSuspend(HierarchyToStringTest.kt:37) + $tab$tab"coroutine#4":DispatchedCoroutine{Active}, continuation is SUSPENDED at line kotlinx.coroutines.debug.HierarchyToStringTest${'$'}testHierarchy${'$'}1${'$'}root${'$'}1${'$'}2${'$'}1.invokeSuspend(HierarchyToStringTest.kt:42) + """.trimIndent() + + // DebugProbes.printHierarchy(root) // <- use it for manual validation + assertEquals(expectedString.trimStackTrace(), DebugProbes.hierarchyToString(root).trimEnd().trimStackTrace()) + root.cancel() + root.join() + finish(7) + } + + 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/debug/SanitizedProbesTest.kt b/core/kotlinx-coroutines-debug/test/debug/SanitizedProbesTest.kt new file mode 100644 index 0000000000..1d44c06993 --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/debug/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.io.* +import java.util.concurrent.* +import kotlin.test.* + +class SanitizedProbesTest : TestBase() { + @Before + fun setUp() { + before() + 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) + } + } +} \ No newline at end of file diff --git a/core/kotlinx-coroutines-debug/test/debug/StartModeProbesTest.kt b/core/kotlinx-coroutines-debug/test/debug/StartModeProbesTest.kt new file mode 100644 index 0000000000..bd33c5c9d2 --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/debug/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/debug/StracktraceUtils.kt b/core/kotlinx-coroutines-debug/test/debug/StracktraceUtils.kt new file mode 100644 index 0000000000..c85b3cd7e9 --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/debug/StracktraceUtils.kt @@ -0,0 +1,101 @@ +/* + * 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 { + return trimIndent().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() as Writer + 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 + } + + 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 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/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/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/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') From 8e2428c591dad7da8c8d003aa928884e5c7771e5 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 10 Dec 2018 19:12:08 +0300 Subject: [PATCH 09/17] Merge scoped coroutines into one in agent representation to avoid misleading dumps --- .../src/internal/StackTraceRecovery.kt | 6 ++ core/kotlinx-coroutines-debug/README.md | 2 + core/kotlinx-coroutines-debug/build.gradle | 3 - .../src/debug/AgentPremain.kt | 5 +- .../src/debug/CoroutineState.kt | 10 +-- .../src/debug/DebugProbes.kt | 4 +- .../src/debug/internal/DebugProbesImpl.kt | 30 +++++---- .../test/debug/CoroutinesDumpTest.kt | 30 ++++----- .../test/debug/HierarchyToStringTest.kt | 62 ++++++++++++------ .../test/debug/SanitizedProbesTest.kt | 4 +- .../test/debug/ScopedBuildersTest.kt | 64 +++++++++++++++++++ .../test/debug/StracktraceUtils.kt | 8 ++- 12 files changed, 166 insertions(+), 62 deletions(-) create mode 100644 core/kotlinx-coroutines-debug/test/debug/ScopedBuildersTest.kt diff --git a/core/kotlinx-coroutines-core/src/internal/StackTraceRecovery.kt b/core/kotlinx-coroutines-core/src/internal/StackTraceRecovery.kt index 0b3181f302..4c79dcc94e 100644 --- a/core/kotlinx-coroutines-core/src/internal/StackTraceRecovery.kt +++ b/core/kotlinx-coroutines-core/src/internal/StackTraceRecovery.kt @@ -179,6 +179,9 @@ private fun createStackTrace(continuation: CoroutineStackFrame): ArrayDeque, public val creationStackTrace: List, - internal val sequenceNumber: Long) { + @JvmField internal val sequenceNumber: Long +) { /** * [Job] associated with a current coroutine or [IllegalStateException] otherwise. @@ -37,7 +38,8 @@ public data class CoroutineState internal constructor( public val state: State get() = _state // Copy constructor - internal constructor(coroutine: Continuation<*>, state: CoroutineState) : this(coroutine, + internal constructor(coroutine: Continuation<*>, state: CoroutineState) : this( + coroutine, state.creationStackTrace, state.sequenceNumber) { _state = state.state @@ -49,9 +51,7 @@ public data class CoroutineState internal constructor( private var lastObservedFrame: CoroutineStackFrame? = null internal fun updateState(state: State, frame: Continuation<*>) { - if (_state == state && lastObservedFrame != null) { - return - } + if (_state == state && lastObservedFrame != null) return _state = state lastObservedFrame = frame as? CoroutineStackFrame diff --git a/core/kotlinx-coroutines-debug/src/debug/DebugProbes.kt b/core/kotlinx-coroutines-debug/src/debug/DebugProbes.kt index 168c6072d4..0f2c489797 100644 --- a/core/kotlinx-coroutines-debug/src/debug/DebugProbes.kt +++ b/core/kotlinx-coroutines-debug/src/debug/DebugProbes.kt @@ -27,8 +27,8 @@ import kotlin.coroutines.* * * `probeCoroutineCreated` is invoked on every coroutine creation using stdlib intrinsics. * * Overhead: - * * Every created continuation is stored in a weak hash map, thus adding additional GC pressure. - * * On every created continuation, stacktrace of the current thread is dumped. + * * 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. * * **WARNING: DO NOT USE DEBUG PROBES IN PRODUCTION ENVIRONMENT.** diff --git a/core/kotlinx-coroutines-debug/src/debug/internal/DebugProbesImpl.kt b/core/kotlinx-coroutines-debug/src/debug/internal/DebugProbesImpl.kt index 7f3be13f1b..a4484f58de 100644 --- a/core/kotlinx-coroutines-debug/src/debug/internal/DebugProbesImpl.kt +++ b/core/kotlinx-coroutines-debug/src/debug/internal/DebugProbesImpl.kt @@ -84,7 +84,10 @@ internal object DebugProbesImpl { @Suppress("DEPRECATION_ERROR") val str = if (this !is JobSupport) toString() else toDebugString() if (state == null) { - builder.append("Coroutine: $str\n") + @Suppress("INVISIBLE_REFERENCE") + if (this !is ScopeCoroutine<*>) { // Do not print scoped coroutines + builder.append("$str\n") + } } else { val element = state.lastObservedStackTrace().firstOrNull() val contState = state.state @@ -169,14 +172,9 @@ internal object DebugProbesImpl { coroutineState.updateState(state, frame) } - private fun Continuation<*>.owner(): ArtificialStackFrame<*>? { - var frame = this as? CoroutineStackFrame ?: return null - while (true) { - if (frame is ArtificialStackFrame<*>) return frame - val completion = frame.callerFrame ?: return null - frame = completion - } - } + private fun Continuation<*>.owner(): ArtificialStackFrame<*>? = (this as? CoroutineStackFrame)?.owner() + + private tailrec fun CoroutineStackFrame.owner(): ArtificialStackFrame<*>? = if (this is ArtificialStackFrame<*>) this else callerFrame?.owner() @Synchronized internal fun probeCoroutineCreated(completion: Continuation): Continuation { @@ -184,6 +182,15 @@ internal object DebugProbesImpl { 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 @@ -211,8 +218,9 @@ internal object DebugProbesImpl { capturedCoroutines.remove(coroutine) } - private class ArtificialStackFrame(val delegate: Continuation, frame: CoroutineStackFrame) : - Continuation by delegate, CoroutineStackFrame by frame { + private class ArtificialStackFrame( + @JvmField val delegate: Continuation, + frame: CoroutineStackFrame) : Continuation by delegate, CoroutineStackFrame by frame { override fun resumeWith(result: Result) { probeCoroutineCompleted(this) diff --git a/core/kotlinx-coroutines-debug/test/debug/CoroutinesDumpTest.kt b/core/kotlinx-coroutines-debug/test/debug/CoroutinesDumpTest.kt index 0b97a679c3..870036896e 100644 --- a/core/kotlinx-coroutines-debug/test/debug/CoroutinesDumpTest.kt +++ b/core/kotlinx-coroutines-debug/test/debug/CoroutinesDumpTest.kt @@ -7,7 +7,6 @@ package kotlinx.coroutines.debug import kotlinx.coroutines.* import org.junit.* import org.junit.Test -import java.io.* import kotlin.coroutines.* import kotlin.test.* @@ -88,20 +87,21 @@ class CoroutinesDumpTest : TestBase() { 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)") + "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)" + ) deferred.cancel() runBlocking { deferred.join() } } diff --git a/core/kotlinx-coroutines-debug/test/debug/HierarchyToStringTest.kt b/core/kotlinx-coroutines-debug/test/debug/HierarchyToStringTest.kt index a027a54fc0..6a6b4feda5 100644 --- a/core/kotlinx-coroutines-debug/test/debug/HierarchyToStringTest.kt +++ b/core/kotlinx-coroutines-debug/test/debug/HierarchyToStringTest.kt @@ -30,8 +30,45 @@ class HierarchyToStringTest : TestBase() { } @Test - fun testHierarchy() = runBlocking { - val root = launch { + 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) @@ -50,24 +87,11 @@ class HierarchyToStringTest : TestBase() { job.join() } } - } - - repeat(4) { yield() } - expect(6) - val tab = '\t' - val expectedString = """ - Coroutine: "coroutine#2":StandaloneCoroutine{Completing} - $tab"foo#3":DeferredCoroutine{Active}, continuation is SUSPENDED at line kotlinx.coroutines.debug.HierarchyToStringTest${'$'}testHierarchy${'$'}1${'$'}root${'$'}1${'$'}1.invokeSuspend(HierarchyToStringTest.kt:30) - $tab"coroutine#4":ActorCoroutine{Active}, continuation is SUSPENDED at line kotlinx.coroutines.debug.HierarchyToStringTest${'$'}testHierarchy${'$'}1${'$'}root${'$'}1${'$'}2.invokeSuspend(HierarchyToStringTest.kt:40) - $tab$tab"coroutine#5":StandaloneCoroutine{Active}, continuation is SUSPENDED at line kotlinx.coroutines.debug.HierarchyToStringTest${'$'}testHierarchy${'$'}1${'$'}root${'$'}1${'$'}2${'$'}job$1.invokeSuspend(HierarchyToStringTest.kt:37) - $tab$tab"coroutine#4":DispatchedCoroutine{Active}, continuation is SUSPENDED at line kotlinx.coroutines.debug.HierarchyToStringTest${'$'}testHierarchy${'$'}1${'$'}root${'$'}1${'$'}2${'$'}1.invokeSuspend(HierarchyToStringTest.kt:42) - """.trimIndent() - // DebugProbes.printHierarchy(root) // <- use it for manual validation - assertEquals(expectedString.trimStackTrace(), DebugProbes.hierarchyToString(root).trimEnd().trimStackTrace()) - root.cancel() - root.join() - finish(7) + if (!isCompleting) { + delay(Long.MAX_VALUE) + } + } } private fun wrapperDispatcher(context: CoroutineContext): CoroutineContext { diff --git a/core/kotlinx-coroutines-debug/test/debug/SanitizedProbesTest.kt b/core/kotlinx-coroutines-debug/test/debug/SanitizedProbesTest.kt index 1d44c06993..925f2f7219 100644 --- a/core/kotlinx-coroutines-debug/test/debug/SanitizedProbesTest.kt +++ b/core/kotlinx-coroutines-debug/test/debug/SanitizedProbesTest.kt @@ -9,7 +9,6 @@ import kotlinx.coroutines.* import kotlinx.coroutines.debug.* import org.junit.* import org.junit.Test -import java.io.* import java.util.concurrent.* import kotlin.test.* @@ -17,6 +16,7 @@ class SanitizedProbesTest : TestBase() { @Before fun setUp() { before() + DebugProbes.sanitizeStackTraces = true DebugProbes.install() } @@ -111,4 +111,4 @@ class SanitizedProbesTest : TestBase() { verifyStackTrace(e, traces) } } -} \ No newline at end of file +} diff --git a/core/kotlinx-coroutines-debug/test/debug/ScopedBuildersTest.kt b/core/kotlinx-coroutines-debug/test/debug/ScopedBuildersTest.kt new file mode 100644 index 0000000000..d0657d7a5b --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/debug/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/debug/StracktraceUtils.kt b/core/kotlinx-coroutines-debug/test/debug/StracktraceUtils.kt index c85b3cd7e9..db89fef835 100644 --- a/core/kotlinx-coroutines-debug/test/debug/StracktraceUtils.kt +++ b/core/kotlinx-coroutines-debug/test/debug/StracktraceUtils.kt @@ -8,7 +8,7 @@ import java.io.* import kotlin.test.* public fun String.trimStackTrace(): String { - return trimIndent().replace(Regex(":[0-9]+"), "").applyBackspace() + return trimIndent().replace(Regex(":[0-9]+"), "").replace(Regex("#[0-9]+"), "").applyBackspace() } public fun String.applyBackspace(): String { @@ -62,7 +62,7 @@ public fun verifyDump(vararg traces: String) { 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) @@ -80,6 +80,8 @@ public fun verifyDump(vararg traces: String) { } } +public fun String.trimPackage() = replace("kotlinx.coroutines.debug.", "") + public fun verifyPartialDump(createdCoroutinesCount: Int, vararg frames: String) { val baos = ByteArrayOutputStream() DebugProbes.dumpCoroutines(PrintStream(baos)) @@ -94,7 +96,7 @@ public fun verifyPartialDump(createdCoroutinesCount: Int, vararg frames: String) } private fun String.sanitizeAddresses(): String { - val index = indexOf("coroutine#") + val index = indexOf("coroutine\"") val next = indexOf(',', index) if (index == -1 || next == -1) return this return substring(0, index) + substring(next, length) From 5a80d2a0b0c52121a0788acc036030812fea9f59 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Mon, 10 Dec 2018 20:19:35 +0300 Subject: [PATCH 10/17] Debug agent optimizations * Do not store creation stacktrace of the coroutine, recover it from ArtificialStackFrame * Reduce lock window where possible to avoid contention * Avoid stacktrace copying where possible --- .../kotlinx-coroutines-debug.txt | 5 +- .../src/debug/CoroutineState.kt | 27 ++++++- .../src/debug/DebugProbes.kt | 25 +++--- .../src/debug/internal/DebugProbesImpl.kt | 76 ++++++++++--------- .../test/debug/CoroutinesDumpTest.kt | 39 ++++++++-- 5 files changed, 106 insertions(+), 66 deletions(-) diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt index fa607fbdfa..4f63d7c3e1 100644 --- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt @@ -1,8 +1,7 @@ public final class kotlinx/coroutines/debug/CoroutineState { public final fun component1 ()Lkotlin/coroutines/Continuation; - public final fun component2 ()Ljava/util/List; - public final fun copy (Lkotlin/coroutines/Continuation;Ljava/util/List;J)Lkotlinx/coroutines/debug/CoroutineState; - public static synthetic fun copy$default (Lkotlinx/coroutines/debug/CoroutineState;Lkotlin/coroutines/Continuation;Ljava/util/List;JILjava/lang/Object;)Lkotlinx/coroutines/debug/CoroutineState; + 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; diff --git a/core/kotlinx-coroutines-debug/src/debug/CoroutineState.kt b/core/kotlinx-coroutines-debug/src/debug/CoroutineState.kt index f6f3e96a23..97e1b6d198 100644 --- a/core/kotlinx-coroutines-debug/src/debug/CoroutineState.kt +++ b/core/kotlinx-coroutines-debug/src/debug/CoroutineState.kt @@ -16,7 +16,7 @@ import kotlin.coroutines.* @ExperimentalCoroutinesApi public data class CoroutineState internal constructor( public val continuation: Continuation<*>, - public val creationStackTrace: List, + private val creationStackBottom: CoroutineStackFrame, @JvmField internal val sequenceNumber: Long ) { @@ -32,23 +32,42 @@ public data class CoroutineState internal constructor( */ 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.creationStackTrace, + state.creationStackBottom, state.sequenceNumber) { _state = state.state this.lastObservedFrame = state.lastObservedFrame } - private var _state: State = State.CREATED + private fun creationStackTrace(): List { + // Skip "Coroutine creation stacktrace" frame + return sequence { yieldFrames(creationStackBottom.callerFrame) }.toList() + } - private var lastObservedFrame: CoroutineStackFrame? = null + 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 diff --git a/core/kotlinx-coroutines-debug/src/debug/DebugProbes.kt b/core/kotlinx-coroutines-debug/src/debug/DebugProbes.kt index 0f2c489797..12fb9a7957 100644 --- a/core/kotlinx-coroutines-debug/src/debug/DebugProbes.kt +++ b/core/kotlinx-coroutines-debug/src/debug/DebugProbes.kt @@ -16,8 +16,8 @@ import kotlin.coroutines.* /** * Debug probes support. * - * Debug probes is a dynamic attach mechanism, which installs multiple hooks into [Continuation] machinery. - * It slows down all coroutine-related code, but in return provides a lot of debug information, including + * 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: @@ -30,8 +30,6 @@ import kotlin.coroutines.* * * 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. - * - * **WARNING: DO NOT USE DEBUG PROBES IN PRODUCTION ENVIRONMENT.** */ @ExperimentalCoroutinesApi public object DebugProbes { @@ -39,15 +37,13 @@ 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 user's diagnostic. + * 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. - * - * **WARNING: DO NOT USE DEBUG PROBES IN PRODUCTION ENVIRONMENT** */ public fun install() { DebugProbesImpl.install() @@ -62,8 +58,6 @@ public object DebugProbes { /** * Invokes given block of code with installed debug probes and uninstall probes in the end. - * - * **WARNING: DO NOT USE DEBUG PROBES IN PRODUCTION ENVIRONMENT** */ public inline fun withDebugProbes(block: () -> Unit) { install() @@ -81,22 +75,21 @@ public object DebugProbes { public fun hierarchyToString(job: Job): String = DebugProbesImpl.hierarchyToString(job) /** - * Prints [job] hierarchy representation from [hierarchyToString] to given [out]. + * 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 alive coroutine states. - * Resulting collection represents a consistent snapshot of all alive coroutines at the moment of invocation. + * 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 given output stream. - * Resulting collection represents a consistent snapshot of all alive coroutines at the moment of invocation. - * Output of this method is similar to `jstack` or full thread dump, so this method can and should be used as replacement to - * "Dump threads" action + * 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: * ``` diff --git a/core/kotlinx-coroutines-debug/src/debug/internal/DebugProbesImpl.kt b/core/kotlinx-coroutines-debug/src/debug/internal/DebugProbesImpl.kt index a4484f58de..4d08421880 100644 --- a/core/kotlinx-coroutines-debug/src/debug/internal/DebugProbesImpl.kt +++ b/core/kotlinx-coroutines-debug/src/debug/internal/DebugProbesImpl.kt @@ -25,6 +25,7 @@ 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 @@ -111,37 +112,39 @@ internal object DebugProbesImpl { .toList() } - @Synchronized public fun dumpCoroutines(out: PrintStream) { if (!isInstalled) { error("Debug probes are not installed") } // Avoid inference with other out/err invocations - val resultingString = buildString { - append("Coroutines dump ${dateFormat.format(System.currentTimeMillis())}") + val resultingString = dumpCoroutines() + out.println(resultingString) + } + @Synchronized + private fun dumpCoroutines(): String { + // Synchronization window can be reduce even more, but no need to do it here + return buildString { + 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()) + 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()) + } } - } } - - // Move it out of synchronization? - out.println(resultingString) } private fun StringBuilder.printStackTrace(frames: List) { @@ -150,10 +153,8 @@ internal object DebugProbesImpl { } } - @Synchronized internal fun probeCoroutineResumed(frame: Continuation<*>) = updateState(frame, State.RUNNING) - @Synchronized internal fun probeCoroutineSuspended(frame: Continuation<*>) = updateState(frame, State.SUSPENDED) private fun updateState(frame: Continuation<*>, state: State) { @@ -163,6 +164,11 @@ internal object DebugProbesImpl { // 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] if (coroutineState == null) { warn(frame, state) @@ -176,7 +182,6 @@ internal object DebugProbesImpl { private tailrec fun CoroutineStackFrame.owner(): ArtificialStackFrame<*>? = if (this is ArtificialStackFrame<*>) this else callerFrame?.owner() - @Synchronized internal fun probeCoroutineCreated(completion: Continuation): Continuation { if (!isInstalled) { return completion @@ -197,22 +202,24 @@ internal object DebugProbesImpl { * 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 = sanitizedStackTrace(Exception()) - val frames = ArrayList(stacktrace.size) - for ((index, frame) in stacktrace.reversed().withIndex()) { - frames += object : CoroutineStackFrame { - override val callerFrame: CoroutineStackFrame? - get() = if (index == 0) null else frames[index - 1] - + val stacktrace = sanitizeStackTrace(Exception()) + val frame = stacktrace.foldRight(null) { frame, acc -> + object : CoroutineStackFrame { + override val callerFrame: CoroutineStackFrame? = acc override fun getStackTraceElement(): StackTraceElement = frame } - } + }!! - val result = ArtificialStackFrame(completion, frames.last()!!) - capturedCoroutines[result] = CoroutineState(completion, stacktrace.slice(1 until stacktrace.size), ++sequenceNumber) + val result = ArtificialStackFrame(completion, frame) + storeFrame(result, completion) return result } + @Synchronized + private fun storeFrame(frame: ArtificialStackFrame, completion: Continuation) { + capturedCoroutines[frame] = CoroutineState(completion, frame, ++sequenceNumber) + } + @Synchronized private fun probeCoroutineCompleted(coroutine: ArtificialStackFrame<*>) { capturedCoroutines.remove(coroutine) @@ -230,7 +237,7 @@ internal object DebugProbesImpl { override fun toString(): String = delegate.toString() } - private fun sanitizedStackTrace(throwable: T): Array { + private fun sanitizeStackTrace(throwable: T): List { val stackTrace = throwable.stackTrace val size = stackTrace.size @@ -243,7 +250,7 @@ internal object DebugProbesImpl { } if (!DebugProbes.sanitizeStackTraces) { - return Array(size - probeIndex) { + return List(size - probeIndex) { if (it == 0) artificialFrame(ARTIFICIAL_FRAME_MESSAGE) else stackTrace[it + probeIndex] } } @@ -255,7 +262,6 @@ internal object DebugProbesImpl { */ val result = ArrayList(size - probeIndex + 1) result += artificialFrame(ARTIFICIAL_FRAME_MESSAGE) - Thread.sleep(1) var includeInternalFrame = true for (i in (probeIndex + 1) until size - 1) { val element = stackTrace[i] @@ -278,7 +284,7 @@ internal object DebugProbesImpl { } result += stackTrace[size - 1] - return result.toTypedArray() + return result } private val StackTraceElement.isInternalMethod: Boolean get() = className.startsWith("kotlinx.coroutines") diff --git a/core/kotlinx-coroutines-debug/test/debug/CoroutinesDumpTest.kt b/core/kotlinx-coroutines-debug/test/debug/CoroutinesDumpTest.kt index 870036896e..dea77f1af9 100644 --- a/core/kotlinx-coroutines-debug/test/debug/CoroutinesDumpTest.kt +++ b/core/kotlinx-coroutines-debug/test/debug/CoroutinesDumpTest.kt @@ -51,8 +51,7 @@ class CoroutinesDumpTest : TestBase() { val found = DebugProbes.dumpCoroutinesState().single { it.jobOrNull === deferred } assertSame(deferred, found.job) - deferred.cancel() - runBlocking { deferred.join() } + runBlocking { deferred.cancelAndJoin() } } @Test @@ -75,8 +74,7 @@ class CoroutinesDumpTest : TestBase() { "\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)") - deferred.cancel() - runBlocking { deferred.join() } + runBlocking { deferred.cancelAndJoin() } } @Test @@ -102,8 +100,34 @@ class CoroutinesDumpTest : TestBase() { "\tat kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.testRunningCoroutineWithSuspensionPoint(CoroutinesDumpTest.kt:71)" ) - deferred.cancel() - runBlocking { deferred.join() } + 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 @@ -113,8 +137,7 @@ class CoroutinesDumpTest : TestBase() { } awaitCoroutineStarted() - deferred.cancel() - runBlocking { deferred.join() } + runBlocking { deferred.cancelAndJoin() } verifyDump() } From 12695b763715338149e0bcaa3633a6e0296fc361 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Tue, 11 Dec 2018 19:20:39 +0300 Subject: [PATCH 11/17] Debug agent code style --- .../src/debug/CoroutineState.kt | 4 +- .../src/debug/internal/DebugProbesImpl.kt | 117 +++++++----------- .../test/debug/StracktraceUtils.kt | 10 +- 3 files changed, 51 insertions(+), 80 deletions(-) diff --git a/core/kotlinx-coroutines-debug/src/debug/CoroutineState.kt b/core/kotlinx-coroutines-debug/src/debug/CoroutineState.kt index 97e1b6d198..78f290b8b8 100644 --- a/core/kotlinx-coroutines-debug/src/debug/CoroutineState.kt +++ b/core/kotlinx-coroutines-debug/src/debug/CoroutineState.kt @@ -50,7 +50,8 @@ public data class CoroutineState internal constructor( internal constructor(coroutine: Continuation<*>, state: CoroutineState) : this( coroutine, state.creationStackBottom, - state.sequenceNumber) { + state.sequenceNumber + ) { _state = state.state this.lastObservedFrame = state.lastObservedFrame } @@ -71,7 +72,6 @@ public data class CoroutineState internal constructor( internal fun updateState(state: State, frame: Continuation<*>) { if (_state == state && lastObservedFrame != null) return - _state = state lastObservedFrame = frame as? CoroutineStackFrame } diff --git a/core/kotlinx-coroutines-debug/src/debug/internal/DebugProbesImpl.kt b/core/kotlinx-coroutines-debug/src/debug/internal/DebugProbesImpl.kt index 4d08421880..c40e14ca64 100644 --- a/core/kotlinx-coroutines-debug/src/debug/internal/DebugProbesImpl.kt +++ b/core/kotlinx-coroutines-debug/src/debug/internal/DebugProbesImpl.kt @@ -33,9 +33,7 @@ internal object DebugProbesImpl { @Synchronized public fun install() { - if (++installations > 1) { - return - } + if (++installations > 1) return ByteBuddyAgent.install() val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") @@ -50,7 +48,7 @@ internal object DebugProbesImpl { @Synchronized public fun uninstall() { - if (installations == 0) error("Agent was not installed") + check(isInstalled) { "Agent was not installed" } if (--installations != 0) return capturedCoroutines.clear() @@ -66,17 +64,13 @@ internal object DebugProbesImpl { @Synchronized public fun hierarchyToString(job: Job): String { - if (!isInstalled) { - error("Debug probes are not installed") - } - + check(isInstalled) { "Debug probes are not installed" } val jobToStack = capturedCoroutines .filterKeys { it.delegate.context[Job] != null } .mapKeys { it.key.delegate.context[Job]!! } - - val sb = StringBuilder() - job.build(jobToStack, sb, "") - return sb.toString() + return buildString { + job.build(jobToStack, this, "") + } } private fun Job.build(map: Map, builder: StringBuilder, indent: String) { @@ -94,7 +88,6 @@ internal object DebugProbesImpl { val contState = state.state builder.append("$str, continuation is $contState at line $element\n") } - for (child in children) { child.build(map, builder, indent + "\t") } @@ -102,10 +95,7 @@ internal object DebugProbesImpl { @Synchronized public fun dumpCoroutinesState(): List { - if (!isInstalled) { - error("Debug probes are not installed") - } - + check(isInstalled) { "Debug probes are not installed" } return capturedCoroutines.entries.asSequence() .map { CoroutineState(it.key.delegate, it.value) } .sortedBy { it.sequenceNumber } @@ -113,38 +103,33 @@ internal object DebugProbesImpl { } public fun dumpCoroutines(out: PrintStream) { - if (!isInstalled) { - error("Debug probes are not installed") - } - - // Avoid inference with other out/err invocations - val resultingString = dumpCoroutines() - out.println(resultingString) + 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 { + private fun dumpCoroutines(): String = buildString { // Synchronization window can be reduce even more, but no need to do it here - return buildString { - 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()) - } + 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) { @@ -158,10 +143,7 @@ internal object DebugProbesImpl { internal fun probeCoroutineSuspended(frame: Continuation<*>) = updateState(frame, State.SUSPENDED) private fun updateState(frame: Continuation<*>, state: State) { - if (!isInstalled) { - return - } - + if (!isInstalled) return // Find ArtificialStackFrame of the coroutine val owner = frame.owner() updateState(owner, frame, state) @@ -174,28 +156,23 @@ internal object DebugProbesImpl { warn(frame, state) return } - coroutineState.updateState(state, frame) } - private fun Continuation<*>.owner(): ArtificialStackFrame<*>? = (this as? CoroutineStackFrame)?.owner() + private fun Continuation<*>.owner(): ArtificialStackFrame<*>? = + (this as? CoroutineStackFrame)?.owner() - private tailrec fun CoroutineStackFrame.owner(): ArtificialStackFrame<*>? = if (this is ArtificialStackFrame<*>) this else callerFrame?.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 (!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 - } - + if (owner != null) return completion /* * Here we replace completion with a sequence of CoroutineStackFrame objects * which represents creation stacktrace, thus making stacktrace recovery mechanism @@ -208,11 +185,10 @@ internal object DebugProbesImpl { override val callerFrame: CoroutineStackFrame? = acc override fun getStackTraceElement(): StackTraceElement = frame } - }!! - - val result = ArtificialStackFrame(completion, frame) - storeFrame(result, completion) - return result + } + return ArtificialStackFrame(completion, frame!!).also { + storeFrame(it, completion) + } } @Synchronized @@ -227,8 +203,8 @@ internal object DebugProbesImpl { private class ArtificialStackFrame( @JvmField val delegate: Continuation, - frame: CoroutineStackFrame) : Continuation by delegate, CoroutineStackFrame by frame { - + frame: CoroutineStackFrame + ) : Continuation by delegate, CoroutineStackFrame by frame { override fun resumeWith(result: Result) { probeCoroutineCompleted(this) delegate.resumeWith(result) @@ -240,14 +216,7 @@ internal object DebugProbesImpl { private fun sanitizeStackTrace(throwable: T): List { val stackTrace = throwable.stackTrace val size = stackTrace.size - - var probeIndex = -1 - for (i in 0 until size) { - val name = stackTrace[i].className - if ("kotlin.coroutines.jvm.internal.DebugProbesKt" == name) { - probeIndex = i - } - } + val probeIndex = stackTrace.indexOfLast { it.className == "kotlin.coroutines.jvm.internal.DebugProbesKt" } if (!DebugProbes.sanitizeStackTraces) { return List(size - probeIndex) { diff --git a/core/kotlinx-coroutines-debug/test/debug/StracktraceUtils.kt b/core/kotlinx-coroutines-debug/test/debug/StracktraceUtils.kt index db89fef835..baa48b038d 100644 --- a/core/kotlinx-coroutines-debug/test/debug/StracktraceUtils.kt +++ b/core/kotlinx-coroutines-debug/test/debug/StracktraceUtils.kt @@ -7,9 +7,11 @@ package kotlinx.coroutines.debug import java.io.* import kotlin.test.* -public fun String.trimStackTrace(): String { - return trimIndent().replace(Regex(":[0-9]+"), "").replace(Regex("#[0-9]+"), "").applyBackspace() -} +public fun String.trimStackTrace(): String = + trimIndent() + .replace(Regex(":[0-9]+"), "") + .replace(Regex("#[0-9]+"), "") + .applyBackspace() public fun String.applyBackspace(): String { val array = toCharArray() @@ -46,7 +48,7 @@ public fun verifyStackTrace(e: Throwable, traces: List) { } public fun toStackTrace(t: Throwable): String { - val sw = StringWriter() as Writer + val sw = StringWriter() t.printStackTrace(PrintWriter(sw)) return sw.toString() } From 515ccb10167d2a8703a445089f9d05e053790a96 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 11 Dec 2018 19:40:35 +0300 Subject: [PATCH 12/17] Make StackTraceElement internal --- .../src/internal/StackTraceRecovery.common.kt | 2 +- .../src/internal/StackTraceRecovery.kt | 5 +++-- .../src/internal/StackTraceRecovery.kt | 5 +++-- .../src/internal/StackTraceRecovery.kt | 5 +++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/common/kotlinx-coroutines-core-common/src/internal/StackTraceRecovery.common.kt b/common/kotlinx-coroutines-core-common/src/internal/StackTraceRecovery.common.kt index 46ac8e29ac..8ce0fcd261 100644 --- a/common/kotlinx-coroutines-core-common/src/internal/StackTraceRecovery.common.kt +++ b/common/kotlinx-coroutines-core-common/src/internal/StackTraceRecovery.common.kt @@ -36,7 +36,7 @@ internal expect suspend inline fun recoverAndThrow(exception: Throwable): Nothin */ internal expect fun unwrap(exception: E): E -expect class StackTraceElement +internal expect class StackTraceElement internal expect interface CoroutineStackFrame { public val callerFrame: CoroutineStackFrame? diff --git a/core/kotlinx-coroutines-core/src/internal/StackTraceRecovery.kt b/core/kotlinx-coroutines-core/src/internal/StackTraceRecovery.kt index fdfa69c401..1b1e1b8fce 100644 --- a/core/kotlinx-coroutines-core/src/internal/StackTraceRecovery.kt +++ b/core/kotlinx-coroutines-core/src/internal/StackTraceRecovery.kt @@ -200,6 +200,7 @@ private fun StackTraceElement.elementWiseEquals(e: StackTraceElement): Boolean { } @Suppress("ACTUAL_WITHOUT_EXPECT") -actual typealias CoroutineStackFrame = kotlin.coroutines.jvm.internal.CoroutineStackFrame +internal actual typealias CoroutineStackFrame = kotlin.coroutines.jvm.internal.CoroutineStackFrame -actual typealias StackTraceElement = java.lang.StackTraceElement +@Suppress("ACTUAL_WITHOUT_EXPECT") +internal actual typealias StackTraceElement = java.lang.StackTraceElement diff --git a/js/kotlinx-coroutines-core-js/src/internal/StackTraceRecovery.kt b/js/kotlinx-coroutines-core-js/src/internal/StackTraceRecovery.kt index e0435af28e..57c6247b5e 100644 --- a/js/kotlinx-coroutines-core-js/src/internal/StackTraceRecovery.kt +++ b/js/kotlinx-coroutines-core-js/src/internal/StackTraceRecovery.kt @@ -12,10 +12,11 @@ internal actual suspend inline fun recoverAndThrow(exception: Throwable): Nothin internal actual fun unwrap(exception: E): E = exception -@Suppress("unused") +@Suppress("UNUSED") internal actual interface CoroutineStackFrame { public actual val callerFrame: CoroutineStackFrame? public actual fun getStackTraceElement(): StackTraceElement? } -actual typealias StackTraceElement = Any +@Suppress("ACTUAL_WITHOUT_EXPECT") +internal actual typealias StackTraceElement = Any diff --git a/native/kotlinx-coroutines-core-native/src/internal/StackTraceRecovery.kt b/native/kotlinx-coroutines-core-native/src/internal/StackTraceRecovery.kt index 923a9b1fa4..4faf16ac1d 100644 --- a/native/kotlinx-coroutines-core-native/src/internal/StackTraceRecovery.kt +++ b/native/kotlinx-coroutines-core-native/src/internal/StackTraceRecovery.kt @@ -11,10 +11,11 @@ internal actual fun recoverStackTrace(exception: E): E = exceptio internal actual fun unwrap(exception: E): E = exception internal actual suspend inline fun recoverAndThrow(exception: Throwable): Nothing = throw exception -@Suppress("unused") +@Suppress("UNUSED") internal actual interface CoroutineStackFrame { public actual val callerFrame: CoroutineStackFrame? public actual fun getStackTraceElement(): StackTraceElement? } -actual typealias StackTraceElement = Any +@Suppress("ACTUAL_WITHOUT_EXPECT") +internal actual typealias StackTraceElement = Any From f64bcde03b4c5e46d63f19d5dcb3b1738c59eefa Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 11 Dec 2018 19:44:49 +0300 Subject: [PATCH 13/17] Unreachable code fix --- .../src/channels/AbstractChannel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/kotlinx-coroutines-core-common/src/channels/AbstractChannel.kt b/common/kotlinx-coroutines-core-common/src/channels/AbstractChannel.kt index c133ddff3d..2fa60dbc7e 100644 --- a/common/kotlinx-coroutines-core-common/src/channels/AbstractChannel.kt +++ b/common/kotlinx-coroutines-core-common/src/channels/AbstractChannel.kt @@ -859,7 +859,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel) { - if (result.closeCause != null) recoverStackTrace(throw result.receiveException) + if (result.closeCause != null) throw recoverStackTrace(result.receiveException) return false } return true From 7520c978464e2f3b1cc075dea36a9f11088e2d85 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Tue, 11 Dec 2018 19:49:07 +0300 Subject: [PATCH 14/17] Get rid of top level 'debug' folder --- core/kotlinx-coroutines-debug/src/{debug => }/AgentPremain.kt | 0 core/kotlinx-coroutines-debug/src/{debug => }/CoroutineState.kt | 0 core/kotlinx-coroutines-debug/src/{debug => }/DebugProbes.kt | 0 .../src/{debug => }/internal/DebugProbesImpl.kt | 0 .../src/{debug => }/internal/NoOpProbes.kt | 0 .../test/{debug => }/CoroutinesDumpTest.kt | 0 core/kotlinx-coroutines-debug/test/{debug => }/DebugProbesTest.kt | 0 .../test/{debug => }/HierarchyToStringTest.kt | 0 .../test/{debug => }/SanitizedProbesTest.kt | 0 .../test/{debug => }/ScopedBuildersTest.kt | 0 .../test/{debug => }/StartModeProbesTest.kt | 0 .../kotlinx-coroutines-debug/test/{debug => }/StracktraceUtils.kt | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename core/kotlinx-coroutines-debug/src/{debug => }/AgentPremain.kt (100%) rename core/kotlinx-coroutines-debug/src/{debug => }/CoroutineState.kt (100%) rename core/kotlinx-coroutines-debug/src/{debug => }/DebugProbes.kt (100%) rename core/kotlinx-coroutines-debug/src/{debug => }/internal/DebugProbesImpl.kt (100%) rename core/kotlinx-coroutines-debug/src/{debug => }/internal/NoOpProbes.kt (100%) rename core/kotlinx-coroutines-debug/test/{debug => }/CoroutinesDumpTest.kt (100%) rename core/kotlinx-coroutines-debug/test/{debug => }/DebugProbesTest.kt (100%) rename core/kotlinx-coroutines-debug/test/{debug => }/HierarchyToStringTest.kt (100%) rename core/kotlinx-coroutines-debug/test/{debug => }/SanitizedProbesTest.kt (100%) rename core/kotlinx-coroutines-debug/test/{debug => }/ScopedBuildersTest.kt (100%) rename core/kotlinx-coroutines-debug/test/{debug => }/StartModeProbesTest.kt (100%) rename core/kotlinx-coroutines-debug/test/{debug => }/StracktraceUtils.kt (100%) diff --git a/core/kotlinx-coroutines-debug/src/debug/AgentPremain.kt b/core/kotlinx-coroutines-debug/src/AgentPremain.kt similarity index 100% rename from core/kotlinx-coroutines-debug/src/debug/AgentPremain.kt rename to core/kotlinx-coroutines-debug/src/AgentPremain.kt diff --git a/core/kotlinx-coroutines-debug/src/debug/CoroutineState.kt b/core/kotlinx-coroutines-debug/src/CoroutineState.kt similarity index 100% rename from core/kotlinx-coroutines-debug/src/debug/CoroutineState.kt rename to core/kotlinx-coroutines-debug/src/CoroutineState.kt diff --git a/core/kotlinx-coroutines-debug/src/debug/DebugProbes.kt b/core/kotlinx-coroutines-debug/src/DebugProbes.kt similarity index 100% rename from core/kotlinx-coroutines-debug/src/debug/DebugProbes.kt rename to core/kotlinx-coroutines-debug/src/DebugProbes.kt diff --git a/core/kotlinx-coroutines-debug/src/debug/internal/DebugProbesImpl.kt b/core/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt similarity index 100% rename from core/kotlinx-coroutines-debug/src/debug/internal/DebugProbesImpl.kt rename to core/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt diff --git a/core/kotlinx-coroutines-debug/src/debug/internal/NoOpProbes.kt b/core/kotlinx-coroutines-debug/src/internal/NoOpProbes.kt similarity index 100% rename from core/kotlinx-coroutines-debug/src/debug/internal/NoOpProbes.kt rename to core/kotlinx-coroutines-debug/src/internal/NoOpProbes.kt diff --git a/core/kotlinx-coroutines-debug/test/debug/CoroutinesDumpTest.kt b/core/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt similarity index 100% rename from core/kotlinx-coroutines-debug/test/debug/CoroutinesDumpTest.kt rename to core/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt diff --git a/core/kotlinx-coroutines-debug/test/debug/DebugProbesTest.kt b/core/kotlinx-coroutines-debug/test/DebugProbesTest.kt similarity index 100% rename from core/kotlinx-coroutines-debug/test/debug/DebugProbesTest.kt rename to core/kotlinx-coroutines-debug/test/DebugProbesTest.kt diff --git a/core/kotlinx-coroutines-debug/test/debug/HierarchyToStringTest.kt b/core/kotlinx-coroutines-debug/test/HierarchyToStringTest.kt similarity index 100% rename from core/kotlinx-coroutines-debug/test/debug/HierarchyToStringTest.kt rename to core/kotlinx-coroutines-debug/test/HierarchyToStringTest.kt diff --git a/core/kotlinx-coroutines-debug/test/debug/SanitizedProbesTest.kt b/core/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt similarity index 100% rename from core/kotlinx-coroutines-debug/test/debug/SanitizedProbesTest.kt rename to core/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt diff --git a/core/kotlinx-coroutines-debug/test/debug/ScopedBuildersTest.kt b/core/kotlinx-coroutines-debug/test/ScopedBuildersTest.kt similarity index 100% rename from core/kotlinx-coroutines-debug/test/debug/ScopedBuildersTest.kt rename to core/kotlinx-coroutines-debug/test/ScopedBuildersTest.kt diff --git a/core/kotlinx-coroutines-debug/test/debug/StartModeProbesTest.kt b/core/kotlinx-coroutines-debug/test/StartModeProbesTest.kt similarity index 100% rename from core/kotlinx-coroutines-debug/test/debug/StartModeProbesTest.kt rename to core/kotlinx-coroutines-debug/test/StartModeProbesTest.kt diff --git a/core/kotlinx-coroutines-debug/test/debug/StracktraceUtils.kt b/core/kotlinx-coroutines-debug/test/StracktraceUtils.kt similarity index 100% rename from core/kotlinx-coroutines-debug/test/debug/StracktraceUtils.kt rename to core/kotlinx-coroutines-debug/test/StracktraceUtils.kt From 7a6fd89c04b3ee972136c0648700f8500dfb0f23 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Wed, 12 Dec 2018 19:08:32 +0300 Subject: [PATCH 15/17] Debug agent cleanup * Readme * Do not fail on Windows during signal handler installation * Do not warn about missing artificial stackframe --- .../src/channels/AbstractChannel.kt | 2 +- core/kotlinx-coroutines-debug/README.md | 14 +++++++------- core/kotlinx-coroutines-debug/src/AgentPremain.kt | 8 ++++++-- .../src/internal/DebugProbesImpl.kt | 11 +---------- .../test/DebugProbesTest.kt | 1 + 5 files changed, 16 insertions(+), 20 deletions(-) diff --git a/common/kotlinx-coroutines-core-common/src/channels/AbstractChannel.kt b/common/kotlinx-coroutines-core-common/src/channels/AbstractChannel.kt index 48692f1d88..f0f22ad130 100644 --- a/common/kotlinx-coroutines-core-common/src/channels/AbstractChannel.kt +++ b/common/kotlinx-coroutines-core-common/src/channels/AbstractChannel.kt @@ -859,7 +859,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel) { - if (result.closeCause != null) recoverStackTrace(throw result.receiveException) + if (result.closeCause != null) throw recoverStackTrace(result.receiveException) return false } return true diff --git a/core/kotlinx-coroutines-debug/README.md b/core/kotlinx-coroutines-debug/README.md index f055ffc627..e12c742647 100644 --- a/core/kotlinx-coroutines-debug/README.md +++ b/core/kotlinx-coroutines-debug/README.md @@ -4,19 +4,19 @@ Debugging facilities for `kotlinx.coroutines` on JVM. ### Overview -This module provides a debug JVM agent which allows to track and trace alive coroutines. -Main entry point to debug facilities is [DebugProbes]. +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 +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 list of such coroutines via [DebugProbes.dumpCoroutinesState] or dump isolated parts -of coroutines hierarchies referenced by [Job] instance using [DebugProbes.printHierarchy]. +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 -Additionally, it is possible to use this module as standalone JVM agent to enable debug probes on the application startup. -You can run your application with additional argument: `-javaagent:kotlinx-coroutines-debug-1.1.0.jar`. +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. diff --git a/core/kotlinx-coroutines-debug/src/AgentPremain.kt b/core/kotlinx-coroutines-debug/src/AgentPremain.kt index 5745439e9c..1ff996e5aa 100644 --- a/core/kotlinx-coroutines-debug/src/AgentPremain.kt +++ b/core/kotlinx-coroutines-debug/src/AgentPremain.kt @@ -19,8 +19,12 @@ internal object AgentPremain { } private fun installSignalHandler() { - Signal.handle(Signal("TRAP") ) { // kill -5 - DebugProbes.dumpCoroutines() + 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/internal/DebugProbesImpl.kt b/core/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt index c40e14ca64..b78f4eba9b 100644 --- a/core/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt +++ b/core/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt @@ -151,11 +151,7 @@ internal object DebugProbesImpl { @Synchronized private fun updateState(owner: ArtificialStackFrame<*>?, frame: Continuation<*>, state: State) { - val coroutineState = capturedCoroutines[owner] - if (coroutineState == null) { - warn(frame, state) - return - } + val coroutineState = capturedCoroutines[owner] ?: return coroutineState.updateState(state, frame) } @@ -257,9 +253,4 @@ internal object DebugProbesImpl { } private val StackTraceElement.isInternalMethod: Boolean get() = className.startsWith("kotlinx.coroutines") - - private fun warn(frame: Continuation<*>, state: State) { - // TODO make this warning configurable or not a warning at all - System.err.println("Failed to find an owner of the frame $frame while transferring it to the state $state") - } } diff --git a/core/kotlinx-coroutines-debug/test/DebugProbesTest.kt b/core/kotlinx-coroutines-debug/test/DebugProbesTest.kt index 04c1b05f00..9dd4d7cec0 100644 --- a/core/kotlinx-coroutines-debug/test/DebugProbesTest.kt +++ b/core/kotlinx-coroutines-debug/test/DebugProbesTest.kt @@ -66,6 +66,7 @@ class DebugProbesTest : TestBase() { @Test fun testAsyncWithSanitizedProbes() = DebugProbes.withDebugProbes { + DebugProbes.sanitizeStackTraces = true runTest { val deferred = createDeferred() val traces = listOf( From 3781a821a11a64670b5a3ffa3c276f1533332ad7 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 13 Dec 2018 11:58:29 +0300 Subject: [PATCH 16/17] Repackage byte-buddy along with debug agent (so it shouldn't be a dependency of target process), use shadow plugin for that to avoid clashes with other byte-buddy versions and publish shadow jar instead of regular one for debug module --- build.gradle | 2 +- core/kotlinx-coroutines-debug/build.gradle | 12 ++++++ .../src/CoroutineState.kt | 11 ++++- gradle/publish-bintray.gradle | 42 ++++++++++--------- 4 files changed, 45 insertions(+), 22 deletions(-) diff --git a/build.gradle b/build.gradle index 52ad4ab282..4125af49c2 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/core/kotlinx-coroutines-debug/build.gradle b/core/kotlinx-coroutines-debug/build.gradle index b7a7d3d089..d54b2b67df 100644 --- a/core/kotlinx-coroutines-debug/build.gradle +++ b/core/kotlinx-coroutines-debug/build.gradle @@ -2,6 +2,8 @@ * 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" @@ -13,3 +15,13 @@ jar { 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/CoroutineState.kt b/core/kotlinx-coroutines-debug/src/CoroutineState.kt index 78f290b8b8..31aa10de01 100644 --- a/core/kotlinx-coroutines-debug/src/CoroutineState.kt +++ b/core/kotlinx-coroutines-debug/src/CoroutineState.kt @@ -97,7 +97,16 @@ public data class CoroutineState internal constructor( * Current state of the coroutine. */ public enum class State { - CREATED, // Not yet started + /** + * Created, but not yet started + */ + CREATED, + /** + * Started and running + */ RUNNING, + /** + * Suspended + */ SUSPENDED } diff --git a/gradle/publish-bintray.gradle b/gradle/publish-bintray.gradle index f0018eeeb9..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,9 +65,21 @@ 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') - println "Current branch: $branch" if (branch == "develop") { dependsOn(":artifactoryPublish") } @@ -112,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 From cd162d356e7dbb8a6ed8e02ceae0133af2df95b5 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 13 Dec 2018 12:57:37 +0300 Subject: [PATCH 17/17] After merge fix: resolve clashes between type alias CoroutineStackFrame and jvm.internal.CoroutineStackFrame --- core/kotlinx-coroutines-debug/src/CoroutineState.kt | 3 ++- .../kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/kotlinx-coroutines-debug/src/CoroutineState.kt b/core/kotlinx-coroutines-debug/src/CoroutineState.kt index 31aa10de01..ca8e14691b 100644 --- a/core/kotlinx-coroutines-debug/src/CoroutineState.kt +++ b/core/kotlinx-coroutines-debug/src/CoroutineState.kt @@ -7,8 +7,9 @@ package kotlinx.coroutines.debug import kotlinx.coroutines.* -import kotlinx.coroutines.internal.* +import kotlinx.coroutines.internal.sanitize import kotlin.coroutines.* +import kotlin.coroutines.jvm.internal.* /** * Class describing coroutine state. diff --git a/core/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt b/core/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt index b78f4eba9b..5c1aec1c7b 100644 --- a/core/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt +++ b/core/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt @@ -6,7 +6,7 @@ package kotlinx.coroutines.debug.internal import kotlinx.coroutines.* import kotlinx.coroutines.debug.* -import kotlinx.coroutines.internal.* +import kotlinx.coroutines.internal.artificialFrame import net.bytebuddy.* import net.bytebuddy.agent.* import net.bytebuddy.dynamic.loading.* @@ -15,6 +15,7 @@ import java.text.* import java.util.* import kotlin.collections.ArrayList import kotlin.coroutines.* +import kotlin.coroutines.jvm.internal.* /** * Mirror of [DebugProbes] with actual implementation. @@ -80,7 +81,7 @@ internal object DebugProbesImpl { val str = if (this !is JobSupport) toString() else toDebugString() if (state == null) { @Suppress("INVISIBLE_REFERENCE") - if (this !is ScopeCoroutine<*>) { // Do not print scoped coroutines + if (this !is kotlinx.coroutines.internal.ScopeCoroutine<*>) { // Do not print scoped coroutines builder.append("$str\n") } } else {