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..4f63d7c3e1 --- /dev/null +++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-debug.txt @@ -0,0 +1,38 @@ +public final class kotlinx/coroutines/debug/CoroutineState { + public final fun component1 ()Lkotlin/coroutines/Continuation; + public final fun copy (Lkotlin/coroutines/Continuation;Lkotlin/coroutines/jvm/internal/CoroutineStackFrame;J)Lkotlinx/coroutines/debug/CoroutineState; + public static synthetic fun copy$default (Lkotlinx/coroutines/debug/CoroutineState;Lkotlin/coroutines/Continuation;Lkotlin/coroutines/jvm/internal/CoroutineStackFrame;JILjava/lang/Object;)Lkotlinx/coroutines/debug/CoroutineState; + public fun equals (Ljava/lang/Object;)Z + public final fun getContinuation ()Lkotlin/coroutines/Continuation; + public final fun getCreationStackTrace ()Ljava/util/List; + public final fun getJob ()Lkotlinx/coroutines/Job; + public final fun getJobOrNull ()Lkotlinx/coroutines/Job; + public final fun getState ()Lkotlinx/coroutines/debug/State; + public fun hashCode ()I + public final fun lastObservedStackTrace ()Ljava/util/List; + public fun toString ()Ljava/lang/String; +} + +public final class kotlinx/coroutines/debug/DebugProbes { + public static final field INSTANCE Lkotlinx/coroutines/debug/DebugProbes; + public final fun dumpCoroutines (Ljava/io/PrintStream;)V + public static synthetic fun dumpCoroutines$default (Lkotlinx/coroutines/debug/DebugProbes;Ljava/io/PrintStream;ILjava/lang/Object;)V + public final fun dumpCoroutinesState ()Ljava/util/List; + public final fun getSanitizeStackTraces ()Z + public final fun hierarchyToString (Lkotlinx/coroutines/Job;)Ljava/lang/String; + public final fun install ()V + public final fun printHierarchy (Lkotlinx/coroutines/Job;Ljava/io/PrintStream;)V + public static synthetic fun printHierarchy$default (Lkotlinx/coroutines/debug/DebugProbes;Lkotlinx/coroutines/Job;Ljava/io/PrintStream;ILjava/lang/Object;)V + public final fun setSanitizeStackTraces (Z)V + public final fun uninstall ()V + public final fun withDebugProbes (Lkotlin/jvm/functions/Function0;)V +} + +public final class kotlinx/coroutines/debug/State : java/lang/Enum { + public static final field CREATED Lkotlinx/coroutines/debug/State; + public static final field RUNNING Lkotlinx/coroutines/debug/State; + public static final field SUSPENDED Lkotlinx/coroutines/debug/State; + public static fun valueOf (Ljava/lang/String;)Lkotlinx/coroutines/debug/State; + public static fun values ()[Lkotlinx/coroutines/debug/State; +} + diff --git a/build.gradle b/build.gradle index 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/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/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/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..4c79dcc94e 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,23 @@ 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..e12c742647 --- /dev/null +++ b/core/kotlinx-coroutines-debug/README.md @@ -0,0 +1,120 @@ +# Module kotlinx-coroutines-debug + +Debugging facilities for `kotlinx.coroutines` on JVM. + +### Overview + +This module provides a debug JVM agent which allows to track and trace existing coroutines. +The main entry point to debug facilities is [DebugProbes] API. +Call to [DebugProbes.install] installs debug agent via ByteBuddy and starts to spy on coroutines when they are created, suspended or resumed. + +After that, you can use [DebugProbes.dumpCoroutines] to print all active (suspended or running) coroutines, including their state, creation and +suspension stacktraces. +Additionally, it is possible to process the list of such coroutines via [DebugProbes.dumpCoroutinesState] or dump isolated parts +of coroutines hierarchy referenced by a [Job] instance using [DebugProbes.printHierarchy]. + +### Using as JVM agent + +It is possible to use this module as a standalone JVM agent to enable debug probes on the application startup. +You can run your application with an additional argument: `-javaagent:kotlinx-coroutines-debug-1.1.0.jar`. +Additionally, on Linux and Mac OS X you can use `kill -5 $pid` command in order to force your application to print all alive coroutines. + + +### Example of usage + +Capabilities of this module can be demonstrated by the following example: +```kotlin +class Computation { + public fun computeValue(): Deferred = GlobalScope.async { + val firstPart = computeFirstPart() + val secondPart = computeSecondPart() + + combineResults(firstPart, secondPart) + } + + private suspend fun combineResults(firstPart: Deferred, secondPart: Deferred): String { + return firstPart.await() + secondPart.await() + } + + + private suspend fun CoroutineScope.computeFirstPart() = async { + delay(5000) + "4" + } + + private suspend fun CoroutineScope.computeSecondPart() = async { + delay(5000) + "2" + } +} + +fun main(args: Array) = runBlocking { + DebugProbes.install() + val computation = Computation() + val deferred = computation.computeValue() + + // Delay for some time + delay(1000) + + DebugProbes.dumpCoroutines() + + println("\nDumping only deferred") + DebugProbes.printHierarchy(deferred) +} +``` + +Printed result will be: +``` +Coroutines dump 2018/11/12 21:44:02 + +Coroutine "coroutine#2":DeferredCoroutine{Active}@1b26f7b2, state: SUSPENDED + at kotlinx.coroutines.DeferredCoroutine.await$suspendImpl(Builders.common.kt:99) + at Computation.combineResults(Example.kt:18) + at Computation$computeValue$1.invokeSuspend(Example.kt:14) + (Coroutine creation stacktrace) + at kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116) + at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23) + at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109) + at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:160) + at kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:88) + at kotlinx.coroutines.BuildersKt.async(Unknown Source) + at kotlinx.coroutines.BuildersKt__Builders_commonKt.async$default(Builders.common.kt:81) + at kotlinx.coroutines.BuildersKt.async$default(Unknown Source) + at Computation.computeValue(Example.kt:10) + at ExampleKt$main$1.invokeSuspend(Example.kt:36) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32) + at kotlinx.coroutines.DispatchedTask$DefaultImpls.run(Dispatched.kt:237) + at kotlinx.coroutines.DispatchedContinuation.run(Dispatched.kt:81) + at kotlinx.coroutines.EventLoopBase.processNextEvent(EventLoop.kt:123) + at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:69) + at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:45) + at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source) + at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:35) + at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source) + at ExampleKt.main(Example.kt:33) + +... More coroutines here ... + +Dumping only deferred +"coroutine#2":DeferredCoroutine{Active}, continuation is SUSPENDED at line kotlinx.coroutines.DeferredCoroutine.await$suspendImpl(Builders.common.kt:99) + "coroutine#3":DeferredCoroutine{Active}, continuation is SUSPENDED at line Computation$computeFirstPart$2.invokeSuspend(Example.kt:23) + "coroutine#4":DeferredCoroutine{Active}, continuation is SUSPENDED at line Computation$computeSecondPart$2.invokeSuspend(Example.kt:28) +``` + + +### Status of the API + +API is purely experimental and it is not guaranteed that it won't be changed (while it is marked as `@ExperimentalCoroutinesApi`). +Do not use this module in production environment and do not rely on the format of the data produced by [DebugProbes]. + + + +[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html + + +[DebugProbes]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/index.html +[DebugProbes.install]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/install.html +[DebugProbes.dumpCoroutines]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/dump-coroutines.html +[DebugProbes.dumpCoroutinesState]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/dump-coroutines-state.html +[DebugProbes.printHierarchy]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-debug/kotlinx.coroutines.debug/-debug-probes/print-hierarchy.html + diff --git a/core/kotlinx-coroutines-debug/build.gradle b/core/kotlinx-coroutines-debug/build.gradle new file mode 100644 index 0000000000..d54b2b67df --- /dev/null +++ b/core/kotlinx-coroutines-debug/build.gradle @@ -0,0 +1,27 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +apply plugin: "com.github.johnrengelman.shadow" + +dependencies { + compile "net.bytebuddy:byte-buddy:$byte_buddy_version" + compile "net.bytebuddy:byte-buddy-agent:$byte_buddy_version" +} + +jar { + manifest { + attributes "Premain-Class": "kotlinx.coroutines.debug.AgentPremain" + attributes "Can-Redefine-Classes": "true" + } +} + +shadowJar { + classifier null + // Shadow only byte buddy, do not package kotlin stdlib + dependencies { + include(dependency("net.bytebuddy:byte-buddy:$byte_buddy_version")) + include(dependency("net.bytebuddy:byte-buddy-agent:$byte_buddy_version")) + } + relocate 'net.bytebuddy', 'kotlinx.coroutines.repackaged.net.bytebuddy' +} diff --git a/core/kotlinx-coroutines-debug/src/AgentPremain.kt b/core/kotlinx-coroutines-debug/src/AgentPremain.kt new file mode 100644 index 0000000000..1ff996e5aa --- /dev/null +++ b/core/kotlinx-coroutines-debug/src/AgentPremain.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug + +import net.bytebuddy.agent.* +import sun.misc.* +import java.lang.instrument.* + +@Suppress("unused") +internal object AgentPremain { + + @JvmStatic + public fun premain(args: String?, instrumentation: Instrumentation) { + Installer.premain(args, instrumentation) + DebugProbes.install() + installSignalHandler() + } + + private fun installSignalHandler() { + try { + Signal.handle(Signal("TRAP")) { // kill -5 + DebugProbes.dumpCoroutines() + } + } catch (t: Throwable) { + System.err.println("Failed to install signal handler: $t") + } + } +} diff --git a/core/kotlinx-coroutines-debug/src/CoroutineState.kt b/core/kotlinx-coroutines-debug/src/CoroutineState.kt new file mode 100644 index 0000000000..31aa10de01 --- /dev/null +++ b/core/kotlinx-coroutines-debug/src/CoroutineState.kt @@ -0,0 +1,112 @@ +/* + * 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<*>, + private val creationStackBottom: CoroutineStackFrame, + @JvmField internal val sequenceNumber: Long +) { + + /** + * [Job] associated with a current coroutine or [IllegalStateException] otherwise. + * May be later used in [DebugProbes.printHierarchy] + */ + public val job: Job get() = continuation.context[Job] ?: error("Continuation $continuation does not have a job") + + /** + * [Job] associated with a current coroutine or null. + * May be later used in [DebugProbes.printHierarchy] + */ + public val jobOrNull: Job? get() = continuation.context[Job] + + /** + * Creation stacktrace of coroutine + */ + public val creationStackTrace: List get() = creationStackTrace() + + /** + * Last observed [state][State] of the coroutine. + */ + public val state: State get() = _state + + private var _state: State = State.CREATED + + private var lastObservedFrame: CoroutineStackFrame? = null + + // Copy constructor + internal constructor(coroutine: Continuation<*>, state: CoroutineState) : this( + coroutine, + state.creationStackBottom, + state.sequenceNumber + ) { + _state = state.state + this.lastObservedFrame = state.lastObservedFrame + } + + private fun creationStackTrace(): List { + // Skip "Coroutine creation stacktrace" frame + return sequence { yieldFrames(creationStackBottom.callerFrame) }.toList() + } + + private tailrec suspend fun SequenceScope.yieldFrames(frame: CoroutineStackFrame?) { + if (frame == null) return + frame.getStackTraceElement()?.let { yield(it) } + val caller = frame.callerFrame + if (caller != null) { + yieldFrames(caller) + } + } + + internal fun updateState(state: State, frame: Continuation<*>) { + if (_state == state && lastObservedFrame != null) return + _state = state + lastObservedFrame = frame as? CoroutineStackFrame + } + + /** + * Last observed stacktrace of the coroutine captured on its suspension or resumption point. + * It means that for [running][State.RUNNING] coroutines resulting stacktrace is inaccurate and + * reflects stacktrace of the resumption point, not the actual current stacktrace + */ + public fun lastObservedStackTrace(): List { + var frame: CoroutineStackFrame? = lastObservedFrame ?: return emptyList() + val result = ArrayList() + while (frame != null) { + frame.getStackTraceElement()?.let { result.add(sanitize(it)) } + frame = frame.callerFrame + } + + return result + } +} + +/** + * Current state of the coroutine. + */ +public enum class State { + /** + * Created, but not yet started + */ + CREATED, + /** + * Started and running + */ + RUNNING, + /** + * Suspended + */ + SUSPENDED +} diff --git a/core/kotlinx-coroutines-debug/src/DebugProbes.kt b/core/kotlinx-coroutines-debug/src/DebugProbes.kt new file mode 100644 index 0000000000..12fb9a7957 --- /dev/null +++ b/core/kotlinx-coroutines-debug/src/DebugProbes.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("unused") + +package kotlinx.coroutines.debug + +import kotlinx.coroutines.* +import kotlinx.coroutines.debug.internal.* +import java.io.* +import java.lang.management.* +import java.util.* +import kotlin.coroutines.* + +/** + * Debug probes support. + * + * Debug probes is a dynamic attach mechanism which installs multiple hooks into coroutines machinery. + * It slows down all coroutine-related code, but in return provides a lot of diagnostic information, including + * asynchronous stack-traces and coroutine dumps (similar to [ThreadMXBean.dumpAllThreads] and `jstack` via [DebugProbes.dumpCoroutines]. + * + * Installed hooks: + * + * * `probeCoroutineResumed` is invoked on every [Continuation.resume]. + * * `probeCoroutineSuspended` is invoked on every continuation suspension. + * * `probeCoroutineCreated` is invoked on every coroutine creation using stdlib intrinsics. + * + * Overhead: + * * Every created coroutine is stored in a weak hash map, thus adding additional GC pressure. + * * On every created coroutine, stacktrace of the current thread is dumped. + * * On every `resume` and `suspend`, [WeakHashMap] is updated under a global lock. + */ +@ExperimentalCoroutinesApi +public object DebugProbes { + + /** + * Whether coroutine creation stacktraces should be sanitized. + * Sanitization removes all frames from `kotlinx.coroutines` package except + * the first one and the last one to simplify diagnostic. + */ + public var sanitizeStackTraces: Boolean = true + + /** + * Installs a [DebugProbes] instead of no-op stdlib probes by redefining + * debug probes class using the same class loader as one loaded [DebugProbes] class. + */ + public fun install() { + DebugProbesImpl.install() + } + + /** + * Uninstall debug probes. + */ + public fun uninstall() { + DebugProbesImpl.uninstall() + } + + /** + * Invokes given block of code with installed debug probes and uninstall probes in the end. + */ + public inline fun withDebugProbes(block: () -> Unit) { + install() + try { + block() + } finally { + uninstall() + } + } + + /** + * Returns string representation of the coroutines [job] hierarchy with additional debug information. + * Hierarchy is printed from the [job] as a root transitively to all children. + */ + public fun hierarchyToString(job: Job): String = DebugProbesImpl.hierarchyToString(job) + + /** + * Prints [job] hierarchy representation from [hierarchyToString] to the given [out]. + */ + public fun printHierarchy(job: Job, out: PrintStream = System.out) = + out.println(DebugProbesImpl.hierarchyToString(job)) + + /** + * Returns all existing coroutine states. + * The resulting collection represents a consistent snapshot of all existing coroutines at the moment of invocation. + */ + public fun dumpCoroutinesState(): List = DebugProbesImpl.dumpCoroutinesState() + + /** + * Dumps all active coroutines into the given output stream, providing a consistent snapshot of all existing coroutines at the moment of invocation. + * The output of this method is similar to `jstack` or a full thread dump. It can be used as the replacement to + * "Dump threads" action. + * + * Example of the output: + * ``` + * Coroutines dump 2018/11/12 19:45:14 + * + * Coroutine "coroutine#42":StandaloneCoroutine{Active}@58fdd99, state: SUSPENDED + * at MyClass$awaitData.invokeSuspend(MyClass.kt:37) + * (Coroutine creation stacktrace) + * at MyClass.createIoRequest(MyClass.kt:142) + * at MyClass.fetchData(MyClass.kt:154) + * at MyClass.showData(MyClass.kt:31) + * + * ... + * ``` + */ + public fun dumpCoroutines(out: PrintStream = System.out) = DebugProbesImpl.dumpCoroutines(out) +} + +// Stubs which are injected as coroutine probes. Require direct match of signatures +internal fun probeCoroutineResumed(frame: Continuation<*>) = DebugProbesImpl.probeCoroutineResumed(frame) + +internal fun probeCoroutineSuspended(frame: Continuation<*>) = DebugProbesImpl.probeCoroutineSuspended(frame) +internal fun probeCoroutineCreated(completion: kotlin.coroutines.Continuation): kotlin.coroutines.Continuation = + DebugProbesImpl.probeCoroutineCreated(completion) diff --git a/core/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt b/core/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt new file mode 100644 index 0000000000..b78f4eba9b --- /dev/null +++ b/core/kotlinx-coroutines-debug/src/internal/DebugProbesImpl.kt @@ -0,0 +1,256 @@ +/* + * 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>() + @Volatile + private var installations = 0 + private val isInstalled: Boolean get() = installations > 0 + // To sort coroutines by creation order, used as unique id + private var sequenceNumber: Long = 0 + + @Synchronized + public fun install() { + if (++installations > 1) return + + ByteBuddyAgent.install() + val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") + val cl2 = Class.forName("kotlinx.coroutines.debug.DebugProbesKt") + + ByteBuddy() + .redefine(cl2) + .name(cl.name) + .make() + .load(cl.classLoader, ClassReloadingStrategy.fromInstalledAgent()) + } + + @Synchronized + public fun uninstall() { + check(isInstalled) { "Agent was not installed" } + if (--installations != 0) return + + capturedCoroutines.clear() + val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") + val cl2 = Class.forName("kotlinx.coroutines.debug.internal.NoOpProbesKt") + + ByteBuddy() + .redefine(cl2) + .name(cl.name) + .make() + .load(cl.classLoader, ClassReloadingStrategy.fromInstalledAgent()) + } + + @Synchronized + public fun hierarchyToString(job: Job): String { + check(isInstalled) { "Debug probes are not installed" } + val jobToStack = capturedCoroutines + .filterKeys { it.delegate.context[Job] != null } + .mapKeys { it.key.delegate.context[Job]!! } + return buildString { + job.build(jobToStack, this, "") + } + } + + private fun Job.build(map: Map, builder: StringBuilder, indent: String) { + val state = map[this] + builder.append(indent) + @Suppress("DEPRECATION_ERROR") + val str = if (this !is JobSupport) toString() else toDebugString() + if (state == null) { + @Suppress("INVISIBLE_REFERENCE") + if (this !is ScopeCoroutine<*>) { // Do not print scoped coroutines + builder.append("$str\n") + } + } else { + val element = state.lastObservedStackTrace().firstOrNull() + val contState = state.state + builder.append("$str, continuation is $contState at line $element\n") + } + for (child in children) { + child.build(map, builder, indent + "\t") + } + } + + @Synchronized + public fun dumpCoroutinesState(): List { + check(isInstalled) { "Debug probes are not installed" } + return capturedCoroutines.entries.asSequence() + .map { CoroutineState(it.key.delegate, it.value) } + .sortedBy { it.sequenceNumber } + .toList() + } + + public fun dumpCoroutines(out: PrintStream) { + check(isInstalled) { "Debug probes are not installed" } + // Avoid inference with other out/err invocations by creating a string first + dumpCoroutines().let { out.println(it) } + } + + @Synchronized + private fun dumpCoroutines(): String = buildString { + // Synchronization window can be reduce even more, but no need to do it here + append("Coroutines dump ${dateFormat.format(System.currentTimeMillis())}") + capturedCoroutines + .asSequence() + .sortedBy { it.value.sequenceNumber } + .forEach { (key, value) -> + val state = if (value.state == State.RUNNING) + "${value.state} (Last suspension stacktrace, not an actual stacktrace)" + else + value.state.toString() + + append("\n\nCoroutine $key, state: $state") + val observedStackTrace = value.lastObservedStackTrace() + if (observedStackTrace.isEmpty()) { + append("\n\tat ${artificialFrame(ARTIFICIAL_FRAME_MESSAGE)}") + printStackTrace(value.creationStackTrace) + } else { + printStackTrace(value.lastObservedStackTrace()) + } + } + } + + private fun StringBuilder.printStackTrace(frames: List) { + frames.forEach { frame -> + append("\n\tat $frame") + } + } + + internal fun probeCoroutineResumed(frame: Continuation<*>) = updateState(frame, State.RUNNING) + + internal fun probeCoroutineSuspended(frame: Continuation<*>) = updateState(frame, State.SUSPENDED) + + private fun updateState(frame: Continuation<*>, state: State) { + if (!isInstalled) return + // Find ArtificialStackFrame of the coroutine + val owner = frame.owner() + updateState(owner, frame, state) + } + + @Synchronized + private fun updateState(owner: ArtificialStackFrame<*>?, frame: Continuation<*>, state: State) { + val coroutineState = capturedCoroutines[owner] ?: return + coroutineState.updateState(state, frame) + } + + private fun Continuation<*>.owner(): ArtificialStackFrame<*>? = + (this as? CoroutineStackFrame)?.owner() + + private tailrec fun CoroutineStackFrame.owner(): ArtificialStackFrame<*>? = + if (this is ArtificialStackFrame<*>) this else callerFrame?.owner() + + internal fun probeCoroutineCreated(completion: Continuation): Continuation { + if (!isInstalled) return completion + /* + * If completion already has an owner, it means that we are in scoped coroutine (coroutineScope, withContext etc.), + * then piggyback on its already existing owner and do not replace completion + */ + val owner = completion.owner() + if (owner != null) return completion + /* + * Here we replace completion with a sequence of CoroutineStackFrame objects + * which represents creation stacktrace, thus making stacktrace recovery mechanism + * even more verbose (it will attach coroutine creation stacktrace to all exceptions), + * and then using this artificial frame as an identifier of coroutineSuspended/resumed calls. + */ + val stacktrace = sanitizeStackTrace(Exception()) + val frame = stacktrace.foldRight(null) { frame, acc -> + object : CoroutineStackFrame { + override val callerFrame: CoroutineStackFrame? = acc + override fun getStackTraceElement(): StackTraceElement = frame + } + } + return ArtificialStackFrame(completion, frame!!).also { + storeFrame(it, completion) + } + } + + @Synchronized + private fun storeFrame(frame: ArtificialStackFrame, completion: Continuation) { + capturedCoroutines[frame] = CoroutineState(completion, frame, ++sequenceNumber) + } + + @Synchronized + private fun probeCoroutineCompleted(coroutine: ArtificialStackFrame<*>) { + capturedCoroutines.remove(coroutine) + } + + private class ArtificialStackFrame( + @JvmField val delegate: Continuation, + frame: CoroutineStackFrame + ) : Continuation by delegate, CoroutineStackFrame by frame { + override fun resumeWith(result: Result) { + probeCoroutineCompleted(this) + delegate.resumeWith(result) + } + + override fun toString(): String = delegate.toString() + } + + private fun sanitizeStackTrace(throwable: T): List { + val stackTrace = throwable.stackTrace + val size = stackTrace.size + val probeIndex = stackTrace.indexOfLast { it.className == "kotlin.coroutines.jvm.internal.DebugProbesKt" } + + if (!DebugProbes.sanitizeStackTraces) { + return List(size - probeIndex) { + if (it == 0) artificialFrame(ARTIFICIAL_FRAME_MESSAGE) else stackTrace[it + probeIndex] + } + } + + /* + * Trim intervals of internal methods from the stacktrace (bounds are excluded from trimming) + * E.g. for sequence [e, i1, i2, i3, e, i4, e, i5, i6, e7] + * output will be [e, i1, i3, e, i4, e, i5, i7] + */ + val result = ArrayList(size - probeIndex + 1) + result += artificialFrame(ARTIFICIAL_FRAME_MESSAGE) + var includeInternalFrame = true + for (i in (probeIndex + 1) until size - 1) { + val element = stackTrace[i] + if (!element.isInternalMethod) { + includeInternalFrame = true + result += element + continue + } + + if (includeInternalFrame) { + result += element + includeInternalFrame = false + } else if (stackTrace[i + 1].isInternalMethod) { + continue + } else { + result += element + includeInternalFrame = true + } + + } + + result += stackTrace[size - 1] + return result + } + + private val StackTraceElement.isInternalMethod: Boolean get() = className.startsWith("kotlinx.coroutines") +} diff --git a/core/kotlinx-coroutines-debug/src/internal/NoOpProbes.kt b/core/kotlinx-coroutines-debug/src/internal/NoOpProbes.kt new file mode 100644 index 0000000000..d32eeb674b --- /dev/null +++ b/core/kotlinx-coroutines-debug/src/internal/NoOpProbes.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("unused", "UNUSED_PARAMETER") + +package kotlinx.coroutines.debug.internal + +import kotlin.coroutines.* + +/* + * Empty class used to replace installed agent in the end of debug session + */ +@JvmName("probeCoroutineResumed") +internal fun probeCoroutineResumedNoOp(frame: Continuation<*>) = Unit +@JvmName("probeCoroutineSuspended") +internal fun probeCoroutineSuspendedNoOp(frame: Continuation<*>) = Unit +@JvmName("probeCoroutineCreated") +internal fun probeCoroutineCreatedNoOp(completion: kotlin.coroutines.Continuation): kotlin.coroutines.Continuation = completion diff --git a/core/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt b/core/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt new file mode 100644 index 0000000000..dea77f1af9 --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt @@ -0,0 +1,177 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug + +import kotlinx.coroutines.* +import org.junit.* +import org.junit.Test +import kotlin.coroutines.* +import kotlin.test.* + +@Suppress("SUSPENSION_POINT_INSIDE_MONITOR") // bug in 1.3.0 FE +class CoroutinesDumpTest : TestBase() { + + private val monitor = Any() + + @Before + fun setUp() { + before() + DebugProbes.sanitizeStackTraces = false + DebugProbes.install() + } + + @After + fun tearDown() { + try { + DebugProbes.uninstall() + } finally { + onCompletion() + } + } + + @Test + fun testSuspendedCoroutine() = synchronized(monitor) { + val deferred = GlobalScope.async { + sleepingOuterMethod() + } + + awaitCoroutineStarted() + Thread.sleep(100) // Let delay be invoked + verifyDump( + "Coroutine \"coroutine#1\":DeferredCoroutine{Active}@1e4a7dd4, state: SUSPENDED\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.sleepingNestedMethod(CoroutinesDumpTest.kt:95)\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.sleepingOuterMethod(CoroutinesDumpTest.kt:88)\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest\$testSuspendedCoroutine\$1\$deferred\$1.invokeSuspend(CoroutinesDumpTest.kt:29)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + + "\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n") + + val found = DebugProbes.dumpCoroutinesState().single { it.jobOrNull === deferred } + assertSame(deferred, found.job) + runBlocking { deferred.cancelAndJoin() } + } + + @Test + fun testRunningCoroutine() = synchronized(monitor) { + val deferred = GlobalScope.async { + activeMethod(shouldSuspend = false) + } + + awaitCoroutineStarted() + verifyDump( + "Coroutine \"coroutine#1\":DeferredCoroutine{Active}@1e4a7dd4, state: RUNNING (Last suspension stacktrace, not an actual stacktrace)\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest\$testRunningCoroutine\$1\$deferred\$1.invokeSuspend(CoroutinesDumpTest.kt:49)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + + "\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n" + + "\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:148)\n" + + "\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt)\n" + + "\tat kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" + + "\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt)\n" + + "\tat kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.testRunningCoroutine(CoroutinesDumpTest.kt:49)") + runBlocking { deferred.cancelAndJoin() } + } + + @Test + fun testRunningCoroutineWithSuspensionPoint() = synchronized(monitor) { + val deferred = GlobalScope.async { + activeMethod(shouldSuspend = true) + } + + awaitCoroutineStarted() + verifyDump( + "Coroutine \"coroutine#1\":DeferredCoroutine{Active}@1e4a7dd4, state: RUNNING (Last suspension stacktrace, not an actual stacktrace)\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.nestedActiveMethod(CoroutinesDumpTest.kt:111)\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.activeMethod(CoroutinesDumpTest.kt:106)\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest\$testRunningCoroutineWithSuspensionPoint\$1\$deferred\$1.invokeSuspend(CoroutinesDumpTest.kt:71)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + + "\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n" + + "\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:148)\n" + + "\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt)\n" + + "\tat kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" + + "\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt)\n" + + "\tat kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.testRunningCoroutineWithSuspensionPoint(CoroutinesDumpTest.kt:71)" + ) + runBlocking { deferred.cancelAndJoin() } + } + + @Test + fun testCreationStackTrace() = synchronized(monitor) { + val deferred = GlobalScope.async { + activeMethod(shouldSuspend = true) + } + + awaitCoroutineStarted() + val coroutine = DebugProbes.dumpCoroutinesState().first() + val result = coroutine.creationStackTrace.fold(StringBuilder()) { acc, element -> + acc.append(element.toString()) + acc.append('\n') + }.toString().trimStackTrace() + + runBlocking { deferred.cancelAndJoin() } + + val expected = ("kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + + "kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + + "kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109)\n" + + "kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:160)\n" + + "kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:88)\n" + + "kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" + + "kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt:81)\n" + + "kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" + + "kotlinx.coroutines.debug.CoroutinesDumpTest.testCreationStackTrace(CoroutinesDumpTest.kt:109)").trimStackTrace() + assertTrue(result.startsWith(expected)) + } + + @Test + fun testFinishedCoroutineRemoved() = synchronized(monitor) { + val deferred = GlobalScope.async { + activeMethod(shouldSuspend = true) + } + + awaitCoroutineStarted() + runBlocking { deferred.cancelAndJoin() } + verifyDump() + } + + private suspend fun activeMethod(shouldSuspend: Boolean) { + nestedActiveMethod(shouldSuspend) + delay(1) + } + + private suspend fun nestedActiveMethod(shouldSuspend: Boolean) { + if (shouldSuspend) delay(1) + notifyTest() + while (coroutineContext[Job]!!.isActive) { + Thread.sleep(100) + } + } + + private suspend fun sleepingOuterMethod() { + sleepingNestedMethod() + delay(1) + } + + private suspend fun sleepingNestedMethod() { + delay(1) + notifyTest() + delay(Long.MAX_VALUE) + } + + private fun awaitCoroutineStarted() { + (monitor as Object).wait() + } + + private fun notifyTest() { + synchronized(monitor) { + (monitor as Object).notify() + } + } +} diff --git a/core/kotlinx-coroutines-debug/test/DebugProbesTest.kt b/core/kotlinx-coroutines-debug/test/DebugProbesTest.kt new file mode 100644 index 0000000000..9dd4d7cec0 --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/DebugProbesTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.coroutines.debug + +import kotlinx.coroutines.* +import org.junit.Test +import java.util.concurrent.* +import kotlin.test.* + +class DebugProbesTest : TestBase() { + + private fun CoroutineScope.createDeferred(): Deferred<*> = async(NonCancellable) { + throw ExecutionException(null) + } + + @Test + fun testAsync() = runTest { + val deferred = createDeferred() + val traces = listOf( + "java.util.concurrent.ExecutionException\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:14)\n" + + "\t(Coroutine boundary)\n" + + "\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.oneMoreNestedMethod(DebugProbesTest.kt:49)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.nestedMethod(DebugProbesTest.kt:44)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$testAsync\$1.invokeSuspend(DebugProbesTest.kt:17)\n", + "Caused by: java.util.concurrent.ExecutionException\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:14)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)" + ) + nestedMethod(deferred, traces) + deferred.join() + } + + @Test + fun testAsyncWithProbes() = DebugProbes.withDebugProbes { + DebugProbes.sanitizeStackTraces = false + runTest { + val deferred = createDeferred() + val traces = listOf( + "java.util.concurrent.ExecutionException\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:16)\n" + + "\t(Coroutine boundary)\n" + + "\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.oneMoreNestedMethod(DebugProbesTest.kt:71)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.nestedMethod(DebugProbesTest.kt:66)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$testAsyncWithProbes\$1\$1.invokeSuspend(DebugProbesTest.kt:43)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + + "\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n" + + "\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:148)\n" + + "\tat kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:45)\n" + + "\tat kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)\n" + + "\tat kotlinx.coroutines.TestBase.runTest(TestBase.kt:138)\n" + + "\tat kotlinx.coroutines.TestBase.runTest\$default(TestBase.kt:19)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.testAsyncWithProbes(DebugProbesTest.kt:38)", + "Caused by: java.util.concurrent.ExecutionException\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:16)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n") + nestedMethod(deferred, traces) + deferred.join() + } + } + + @Test + fun testAsyncWithSanitizedProbes() = DebugProbes.withDebugProbes { + DebugProbes.sanitizeStackTraces = true + runTest { + val deferred = createDeferred() + val traces = listOf( + "java.util.concurrent.ExecutionException\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:16)\n" + + "\t(Coroutine boundary)\n" + + "\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.oneMoreNestedMethod(DebugProbesTest.kt:71)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.nestedMethod(DebugProbesTest.kt:66)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$testAsyncWithSanitizedProbes\$1\$1.invokeSuspend(DebugProbesTest.kt:43)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.testAsyncWithSanitizedProbes(DebugProbesTest.kt:38)", + "Caused by: java.util.concurrent.ExecutionException\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:16)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n") + nestedMethod(deferred, traces) + deferred.join() + } + } + + private suspend fun nestedMethod(deferred: Deferred<*>, traces: List) { + oneMoreNestedMethod(deferred, traces) + assertTrue(true) // Prevent tail-call optimization + } + + private suspend fun oneMoreNestedMethod(deferred: Deferred<*>, traces: List) { + try { + deferred.await() + expectUnreached() + } catch (e: ExecutionException) { + verifyStackTrace(e, traces) + } + } +} diff --git a/core/kotlinx-coroutines-debug/test/HierarchyToStringTest.kt b/core/kotlinx-coroutines-debug/test/HierarchyToStringTest.kt new file mode 100644 index 0000000000..6a6b4feda5 --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/HierarchyToStringTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import org.junit.* +import org.junit.Test +import kotlin.coroutines.* +import kotlin.test.* + +class HierarchyToStringTest : TestBase() { + + @Before + fun setUp() { + before() + DebugProbes.sanitizeStackTraces = false + DebugProbes.install() + } + + @After + fun tearDown() { + try { + DebugProbes.uninstall() + } finally { + onCompletion() + } + } + + @Test + fun testCompletingHierarchy() = runBlocking { + val tab = '\t' + val expectedString = """ + "coroutine#2":StandaloneCoroutine{Completing} + $tab"foo#3":DeferredCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1${'$'}1.invokeSuspend(HierarchyToStringTest.kt:30) + $tab"coroutine#4":ActorCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1${'$'}2${'$'}1.invokeSuspend(HierarchyToStringTest.kt:40) + $tab$tab"coroutine#5":StandaloneCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1${'$'}2${'$'}job$1.invokeSuspend(HierarchyToStringTest.kt:37) + """.trimIndent() + + checkHierarchy(isCompleting = true, expectedString = expectedString) + } + + @Test + fun testActiveHierarchy() = runBlocking { + val tab = '\t' + val expectedString = """ + "coroutine#2":StandaloneCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1.invokeSuspend(HierarchyToStringTest.kt:94) + $tab"foo#3":DeferredCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1${'$'}1.invokeSuspend(HierarchyToStringTest.kt:30) + $tab"coroutine#4":ActorCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1${'$'}2${'$'}1.invokeSuspend(HierarchyToStringTest.kt:40) + $tab$tab"coroutine#5":StandaloneCoroutine{Active}, continuation is SUSPENDED at line HierarchyToStringTest${'$'}launchHierarchy${'$'}1${'$'}2${'$'}job$1.invokeSuspend(HierarchyToStringTest.kt:37) + """.trimIndent() + checkHierarchy(isCompleting = false, expectedString = expectedString) + } + + private suspend fun CoroutineScope.checkHierarchy(isCompleting: Boolean, expectedString: String) { + val root = launchHierarchy(isCompleting) + repeat(4) { yield() } + expect(6) + assertEquals( + expectedString.trimStackTrace().trimPackage(), + DebugProbes.hierarchyToString(root).trimEnd().trimStackTrace().trimPackage() + ) + root.cancel() + root.join() + finish(7) + } + + private fun CoroutineScope.launchHierarchy(isCompleting: Boolean): Job { + return launch { + expect(1) + async(CoroutineName("foo")) { + expect(2) + delay(Long.MAX_VALUE) + } + + actor { + expect(3) + val job = launch { + expect(4) + delay(Long.MAX_VALUE) + } + + withContext(wrapperDispatcher(coroutineContext)) { + expect(5) + job.join() + } + } + + if (!isCompleting) { + delay(Long.MAX_VALUE) + } + } + } + + private fun wrapperDispatcher(context: CoroutineContext): CoroutineContext { + val dispatcher = context[ContinuationInterceptor] as CoroutineDispatcher + return object : CoroutineDispatcher() { + override fun dispatch(context: CoroutineContext, block: Runnable) { + dispatcher.dispatch(context, block) + } + } + } +} diff --git a/core/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt b/core/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt new file mode 100644 index 0000000000..925f2f7219 --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/SanitizedProbesTest.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("PackageDirectoryMismatch") +package definitely.not.kotlinx.coroutines + +import kotlinx.coroutines.* +import kotlinx.coroutines.debug.* +import org.junit.* +import org.junit.Test +import java.util.concurrent.* +import kotlin.test.* + +class SanitizedProbesTest : TestBase() { + @Before + fun setUp() { + before() + DebugProbes.sanitizeStackTraces = true + DebugProbes.install() + } + + @After + fun tearDown() { + try { + DebugProbes.uninstall() + } finally { + onCompletion() + } + } + + @Test + fun testRecoveredStackTrace() = runTest { + val deferred = createDeferred() + val traces = listOf( + "java.util.concurrent.ExecutionException\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest\$createDeferredNested\$1.invokeSuspend(SanitizedProbesTest.kt:97)\n" + + "\t(Coroutine boundary)\n" + + "\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt:99)\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest.oneMoreNestedMethod(SanitizedProbesTest.kt:67)\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest.nestedMethod(SanitizedProbesTest.kt:61)\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest\$testRecoveredStackTrace\$1.invokeSuspend(SanitizedProbesTest.kt:50)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + + "\tat kotlinx.coroutines.TestBase.runTest\$default(TestBase.kt:141)\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest.testRecoveredStackTrace(SanitizedProbesTest.kt:33)", + "Caused by: java.util.concurrent.ExecutionException\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest\$createDeferredNested\$1.invokeSuspend(SanitizedProbesTest.kt:57)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n" + ) + nestedMethod(deferred, traces) + deferred.join() + } + + @Test + fun testCoroutinesDump() = runTest { + val deferred = createActiveDeferred() + yield() + verifyDump( + "Coroutine \"coroutine#3\":BlockingCoroutine{Active}@7d68ef40, state: RUNNING (Last suspension stacktrace, not an actual stacktrace)\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest\$testCoroutinesDump\$1.invokeSuspend(SanitizedProbesTest.kt:58)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + + "\tat kotlinx.coroutines.TestBase.runTest\$default(TestBase.kt:141)\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest.testCoroutinesDump(SanitizedProbesTest.kt:56)", + + "Coroutine \"coroutine#4\":DeferredCoroutine{Active}@75c072cb, state: SUSPENDED\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest\$createActiveDeferred\$1.invokeSuspend(SanitizedProbesTest.kt:63)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + + "\tat kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest.createActiveDeferred(SanitizedProbesTest.kt:62)\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest.access\$createActiveDeferred(SanitizedProbesTest.kt:16)\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest\$testCoroutinesDump\$1.invokeSuspend(SanitizedProbesTest.kt:57)\n" + + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n" + + "\tat kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:237)\n" + + "\tat kotlinx.coroutines.TestBase.runTest\$default(TestBase.kt:141)\n" + + "\tat definitely.not.kotlinx.coroutines.SanitizedProbesTest.testCoroutinesDump(SanitizedProbesTest.kt:56)" + ) + deferred.cancelAndJoin() + } + + private fun CoroutineScope.createActiveDeferred(): Deferred<*> = async { + suspendingMethod() + assertTrue(true) + } + + private suspend fun suspendingMethod() { + delay(Long.MAX_VALUE) + } + + private fun CoroutineScope.createDeferred(): Deferred<*> = createDeferredNested() + + private fun CoroutineScope.createDeferredNested(): Deferred<*> = async(NonCancellable) { + throw ExecutionException(null) + } + + private suspend fun nestedMethod(deferred: Deferred<*>, traces: List) { + oneMoreNestedMethod(deferred, traces) + assertTrue(true) // Prevent tail-call optimization + } + + private suspend fun oneMoreNestedMethod(deferred: Deferred<*>, traces: List) { + try { + deferred.await() + expectUnreached() + } catch (e: ExecutionException) { + verifyStackTrace(e, traces) + } + } +} diff --git a/core/kotlinx-coroutines-debug/test/ScopedBuildersTest.kt b/core/kotlinx-coroutines-debug/test/ScopedBuildersTest.kt new file mode 100644 index 0000000000..d0657d7a5b --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/ScopedBuildersTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug + +import kotlinx.coroutines.* +import org.junit.* +import kotlin.coroutines.* + +class ScopedBuildersTest : TestBase() { + @Before + fun setUp() { + before() + DebugProbes.sanitizeStackTraces = false + DebugProbes.install() + } + + @After + fun tearDown() { + try { + DebugProbes.uninstall() + } finally { + onCompletion() + } + } + + @Test + fun testNestedScopes() = runBlocking { + val job = launch { doInScope() } + yield() + yield() + verifyDump( + "Coroutine \"coroutine#1\":BlockingCoroutine{Active}@16612a51, state: RUNNING (Last suspension stacktrace, not an actual stacktrace)\n" + + "\tat kotlinx.coroutines.debug.ScopedBuildersTest\$testNestedScopes\$1.invokeSuspend(ScopedBuildersTest.kt:32)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n", + + "Coroutine \"coroutine#2\":StandaloneCoroutine{Active}@6b53e23f, state: SUSPENDED\n" + + "\tat kotlinx.coroutines.debug.ScopedBuildersTest\$doWithContext\$2.invokeSuspend(ScopedBuildersTest.kt:49)\n" + + "\tat kotlinx.coroutines.debug.ScopedBuildersTest.doWithContext(ScopedBuildersTest.kt:47)\n" + + "\tat kotlinx.coroutines.debug.ScopedBuildersTest\$doInScope\$2.invokeSuspend(ScopedBuildersTest.kt:41)\n" + + "\tat kotlinx.coroutines.debug.ScopedBuildersTest\$testNestedScopes\$1\$job\$1.invokeSuspend(ScopedBuildersTest.kt:30)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)") + job.cancelAndJoin() + finish(4) + } + + private suspend fun doInScope() = coroutineScope { + expect(1) + doWithContext() + expectUnreached() + } + + private suspend fun doWithContext() { + expect(2) + withContext(wrapperDispatcher(coroutineContext)) { + expect(3) + delay(Long.MAX_VALUE) + } + expectUnreached() + } +} \ No newline at end of file diff --git a/core/kotlinx-coroutines-debug/test/StartModeProbesTest.kt b/core/kotlinx-coroutines-debug/test/StartModeProbesTest.kt new file mode 100644 index 0000000000..bd33c5c9d2 --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/StartModeProbesTest.kt @@ -0,0 +1,157 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug + +import kotlinx.coroutines.* +import org.junit.* +import org.junit.Test +import kotlin.test.* + +class StartModeProbesTest : TestBase() { + + @Before + fun setUp() { + before() + DebugProbes.sanitizeStackTraces = false + DebugProbes.install() + } + + @After + fun tearDown() { + try { + DebugProbes.uninstall() + } finally { + onCompletion() + } + } + + @Test + fun testUndispatched() = runTest { + expect(1) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + undispatchedSleeping() + assertTrue(true) + } + + yield() + expect(3) + verifyPartialDump(2, "StartModeProbesTest.undispatchedSleeping") + job.cancelAndJoin() + verifyPartialDump(1, "StartModeProbesTest\$testUndispatched") + finish(4) + } + + private suspend fun undispatchedSleeping() { + delay(Long.MAX_VALUE) + assertTrue(true) + } + + @Test + fun testWithTimeoutWithUndispatched() = runTest { + expect(1) + val job = launchUndispatched() + + yield() + expect(3) + verifyPartialDump( + 2, + "StartModeProbesTest\$launchUndispatched\$1.invokeSuspend", + "StartModeProbesTest.withTimeoutHelper", + "StartModeProbesTest\$withTimeoutHelper\$2.invokeSuspend" + ) + job.cancelAndJoin() + verifyPartialDump(1, "StartModeProbesTest\$testWithTimeoutWithUndispatched") + finish(4) + } + + private fun CoroutineScope.launchUndispatched(): Job { + return launch(start = CoroutineStart.UNDISPATCHED) { + withTimeoutHelper() + assertTrue(true) + } + } + + private suspend fun withTimeoutHelper() { + withTimeout(Long.MAX_VALUE) { + expect(2) + delay(Long.MAX_VALUE) + } + + assertTrue(true) + } + + @Test + fun testWithTimeout() = runTest { + withTimeout(Long.MAX_VALUE) { + testActiveDump( + false, + "StartModeProbesTest\$testWithTimeout\$1.invokeSuspend", + "state: RUNNING" + ) + } + } + + @Test + fun testWithTimeoutAfterYield() = runTest { + withTimeout(Long.MAX_VALUE) { + testActiveDump( + true, + "StartModeProbesTest\$testWithTimeoutAfterYield\$1.invokeSuspend", + "StartModeProbesTest\$testWithTimeoutAfterYield\$1\$1.invokeSuspend", + "StartModeProbesTest.testActiveDump", + "state: RUNNING" + ) + } + } + + private suspend fun testActiveDump(shouldYield: Boolean, vararg expectedFrames: String) { + if (shouldYield) yield() + verifyPartialDump(1, *expectedFrames) + assertTrue(true) + } + + @Test + fun testWithTailCall() = runTest { + expect(1) + val job = tailCallMethod() + yield() + expect(3) + verifyPartialDump(2, "StartModeProbesTest\$launchFromTailCall\$2") + job.cancelAndJoin() + verifyPartialDump(1, "StartModeProbesTest\$testWithTailCall") + finish(4) + } + + private suspend fun CoroutineScope.tailCallMethod(): Job = launchFromTailCall() + private suspend fun CoroutineScope.launchFromTailCall(): Job = launch { + expect(2) + delay(Long.MAX_VALUE) + } + + @Test + fun testCoroutineScope() = runTest { + expect(1) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + runScope() + } + + yield() + expect(3) + verifyPartialDump( + 2, + "StartModeProbesTest\$runScope\$2.invokeSuspend", + "StartModeProbesTest\$testCoroutineScope\$1\$job\$1.invokeSuspend") + job.cancelAndJoin() + finish(4) + } + + private suspend fun runScope() { + coroutineScope { + expect(2) + delay(Long.MAX_VALUE) + } + } +} diff --git a/core/kotlinx-coroutines-debug/test/StracktraceUtils.kt b/core/kotlinx-coroutines-debug/test/StracktraceUtils.kt new file mode 100644 index 0000000000..baa48b038d --- /dev/null +++ b/core/kotlinx-coroutines-debug/test/StracktraceUtils.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.debug + +import java.io.* +import kotlin.test.* + +public fun String.trimStackTrace(): String = + trimIndent() + .replace(Regex(":[0-9]+"), "") + .replace(Regex("#[0-9]+"), "") + .applyBackspace() + +public fun String.applyBackspace(): String { + val array = toCharArray() + val stack = CharArray(array.size) + var stackSize = -1 + for (c in array) { + if (c != '\b') { + stack[++stackSize] = c + } else { + --stackSize + } + } + + return String(stack, 0, stackSize + 1) +} + +public fun verifyStackTrace(e: Throwable, traces: List) { + val stacktrace = toStackTrace(e) + traces.forEach { + val expectedLines = it.trimStackTrace().split("\n") + for (i in 0 until expectedLines.size) { + traces.forEach { + assertTrue( + stacktrace.trimStackTrace().contains(it.trimStackTrace()), + "\nExpected trace element:\n$it\n\nActual stacktrace:\n$stacktrace" + ) + } + } + } + + val causes = stacktrace.count("Caused by") + assertNotEquals(0, causes) + assertEquals(causes, traces.map { it.count("Caused by") }.sum()) +} + +public fun toStackTrace(t: Throwable): String { + val sw = StringWriter() + t.printStackTrace(PrintWriter(sw)) + return sw.toString() +} + +public fun String.count(substring: String): Int = split(substring).size - 1 + +public fun verifyDump(vararg traces: String) { + val baos = ByteArrayOutputStream() + DebugProbes.dumpCoroutines(PrintStream(baos)) + val trace = baos.toString().split("\n\n") + if (traces.isEmpty()) { + assertEquals(1, trace.size) + assertTrue(trace[0].startsWith("Coroutines dump")) + return + } + // Drop "Coroutine dump" line + trace.withIndex().drop(1).forEach { (index, value) -> + val expected = traces[index - 1].applyBackspace().split("\n\t(Coroutine creation stacktrace)\n", limit = 2) + val actual = value.applyBackspace().split("\n\t(Coroutine creation stacktrace)\n", limit = 2) + assertEquals(expected.size, actual.size) + + expected.withIndex().forEach { (index, trace) -> + val actualTrace = actual[index].trimStackTrace().sanitizeAddresses() + val expectedTrace = trace.trimStackTrace().sanitizeAddresses() + val actualLines = actualTrace.split("\n") + val expectedLines = expectedTrace.split("\n") + for (i in 0 until expectedLines.size) { + assertEquals(expectedLines[i], actualLines[i]) + } + } + } +} + +public fun String.trimPackage() = replace("kotlinx.coroutines.debug.", "") + +public fun verifyPartialDump(createdCoroutinesCount: Int, vararg frames: String) { + val baos = ByteArrayOutputStream() + DebugProbes.dumpCoroutines(PrintStream(baos)) + val dump = baos.toString() + val trace = dump.split("\n\n") + val matches = frames.all { frame -> + trace.any { tr -> tr.contains(frame) } + } + + assertEquals(createdCoroutinesCount, DebugProbes.dumpCoroutinesState().size) + assertTrue(matches) +} + +private fun String.sanitizeAddresses(): String { + val index = indexOf("coroutine\"") + val next = indexOf(',', index) + if (index == -1 || next == -1) return this + return substring(0, index) + substring(next, length) +} diff --git a/gradle.properties b/gradle.properties index df587f53ea..51834fa113 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,6 +11,7 @@ html_version=0.6.8 lincheck_version=1.9 dokka_version=0.9.16-rdev-2-mpp-hacks bintray_version=1.8.2-SNAPSHOT +byte_buddy_version=1.9.3 artifactory_plugin_version=4.7.3 # JS diff --git a/gradle/publish-bintray.gradle b/gradle/publish-bintray.gradle index 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 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')