|
| 1 | +/* |
| 2 | + * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. |
| 3 | + */ |
| 4 | + |
| 5 | +@file:Suppress("unused") |
| 6 | + |
| 7 | +package kotlinx.coroutines |
| 8 | + |
| 9 | +import kotlinx.coroutines.internal.* |
| 10 | +import kotlinx.coroutines.internal.CoroutineStackFrame |
| 11 | +import net.bytebuddy.* |
| 12 | +import net.bytebuddy.agent.* |
| 13 | +import net.bytebuddy.dynamic.loading.* |
| 14 | +import java.lang.management.* |
| 15 | +import java.util.* |
| 16 | +import kotlin.collections.ArrayList |
| 17 | +import kotlin.coroutines.* |
| 18 | + |
| 19 | +/** |
| 20 | + * Debug probes support. |
| 21 | + * |
| 22 | + * Debug probes is a dynamic attach mechanism, which installs multiple hooks into [Continuation] machinery. |
| 23 | + * It significantly slows down all coroutine-related code, but in return provides a lot of debug information, including |
| 24 | + * asynchronous stack-traces and coroutine dumps (similar to [ThreadMXBean.dumpAllThreads] and `jstack`. |
| 25 | + * |
| 26 | + * Installed hooks: |
| 27 | + * |
| 28 | + * * `probeCoroutineResumed` is invoked on every [Continuation.resume]. |
| 29 | + * * `probeCoroutineSuspended` is invoked on every continuation suspension. |
| 30 | + * * `probeCoroutineCreated` is invoked on every coroutine creation using stdlib intrinsics. |
| 31 | + * |
| 32 | + * Overhead: |
| 33 | + * * Every created continuation is stored in a weak hash map, thus adding additional GC pressure |
| 34 | + * * On every created continuation, stacktrace of the current thread is dumped |
| 35 | + * * On every `resume` and `suspend`, [WeakHashMap] is updated under a global lock |
| 36 | + * |
| 37 | + * **WARNING: DO NOT USE DEBUG PROBES IN PRODUCTION ENVIRONMENT** |
| 38 | + */ |
| 39 | +public object DebugProbes { |
| 40 | + private val traces = WeakHashMap<Continuation<*>, Array<StackTraceElement>>() |
| 41 | + private var installed: Boolean = false |
| 42 | + |
| 43 | + /** |
| 44 | + * Installs a [DebugProbes] instead of no-op stdlib probes by redefining |
| 45 | + * debug probes class using the same class loader. |
| 46 | + * |
| 47 | + * **WARNING: DO NOT USE DEBUG PROBES IN PRODUCTION ENVIRONMENT** |
| 48 | + * Subsequent invocations of this method have no effect. |
| 49 | + */ |
| 50 | + @Synchronized |
| 51 | + public fun install() { |
| 52 | + if (installed) { |
| 53 | + return |
| 54 | + } |
| 55 | + |
| 56 | + installed = true |
| 57 | + ByteBuddyAgent.install() |
| 58 | + val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") |
| 59 | + val cl2 = Class.forName("kotlinx.coroutines.DebugProbesKt") |
| 60 | + |
| 61 | + ByteBuddy() |
| 62 | + .redefine(cl2) |
| 63 | + .name(cl.name) |
| 64 | + .make() |
| 65 | + .load(cl.classLoader, ClassReloadingStrategy.fromInstalledAgent()) |
| 66 | + } |
| 67 | + |
| 68 | + /** |
| 69 | + * Uninstall debug probes |
| 70 | + */ |
| 71 | + @Synchronized |
| 72 | + public fun uninstall() { |
| 73 | + if (!installed) { |
| 74 | + return |
| 75 | + } |
| 76 | + |
| 77 | + traces.clear() |
| 78 | + ByteBuddyAgent.install() |
| 79 | + val cl = Class.forName("kotlin.coroutines.jvm.internal.DebugProbesKt") |
| 80 | + val cl2 = Class.forName("kotlinx.coroutines.NoOpProbesKt") |
| 81 | + |
| 82 | + ByteBuddy() |
| 83 | + .redefine(cl2) |
| 84 | + .name(cl.name) |
| 85 | + .make() |
| 86 | + .load(cl.classLoader, ClassReloadingStrategy.fromInstalledAgent()) |
| 87 | + } |
| 88 | + |
| 89 | + /** |
| 90 | + * Invokes given block of code with installed debug probes and unistall probes in the end. |
| 91 | + * **NOTE:** this method is not composable, it will uninstall debug probes even if they were installed |
| 92 | + * prior to method invocation |
| 93 | + */ |
| 94 | + public inline fun withDebugProbes(block: () -> Unit) { |
| 95 | + install() |
| 96 | + try { |
| 97 | + block() |
| 98 | + } finally { |
| 99 | + uninstall() |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + |
| 104 | + public fun dumpCoroutines(): String = TODO("Not implemented") |
| 105 | + |
| 106 | + @Synchronized |
| 107 | + internal fun probeCoroutineResumed(frame: Continuation<*>) { |
| 108 | + } |
| 109 | + |
| 110 | + @Synchronized |
| 111 | + internal fun probeCoroutineSuspended(frame: Continuation<*>) { |
| 112 | + } |
| 113 | + |
| 114 | + @Synchronized |
| 115 | + internal fun <T> probeCoroutineCreated(completion: kotlin.coroutines.Continuation<T>): kotlin.coroutines.Continuation<T> { |
| 116 | + val stacktrace = sanitizedStackTrace(Exception()) |
| 117 | + traces[completion] = stacktrace |
| 118 | + |
| 119 | + // TODO create lazy proxy and reverse stacktrace on demand |
| 120 | + val frames = ArrayList<CoroutineStackFrame?>(stacktrace.size) |
| 121 | + for ((index, frame) in stacktrace.reversed().withIndex()) { |
| 122 | + frames += object : CoroutineStackFrame { |
| 123 | + override val callerFrame: CoroutineStackFrame? |
| 124 | + get() = if (index == 0) null else frames[index - 1] |
| 125 | + |
| 126 | + override fun getStackTraceElement(): StackTraceElement = frame |
| 127 | + } |
| 128 | + } |
| 129 | + |
| 130 | + return object : Continuation<T> by completion, CoroutineStackFrame by frames.last()!! {} |
| 131 | + } |
| 132 | + |
| 133 | + private fun <T : Throwable> sanitizedStackTrace(throwable: T): Array<StackTraceElement> { |
| 134 | + val stackTrace = throwable.stackTrace |
| 135 | + val size = stackTrace.size |
| 136 | + |
| 137 | + var lastIntrinsic = -1 |
| 138 | + for (i in 0 until size) { |
| 139 | + val name = stackTrace[i].className |
| 140 | + if ("kotlin.coroutines.jvm.internal.DebugProbesKt" == name) { |
| 141 | + lastIntrinsic = i |
| 142 | + } |
| 143 | + } |
| 144 | + |
| 145 | + val startIndex = lastIntrinsic + 1 |
| 146 | + return Array(size - lastIntrinsic) { |
| 147 | + if (it == 0) { |
| 148 | + artificialFrame("Coroutine creation callsite") |
| 149 | + } else { |
| 150 | + stackTrace[startIndex + it - 1] |
| 151 | + } |
| 152 | + } |
| 153 | + } |
| 154 | +} |
| 155 | + |
| 156 | +// Stubs which are injected as coroutine probes. Require direct match of signatures |
| 157 | +internal fun probeCoroutineResumed(frame: Continuation<*>) = DebugProbes.probeCoroutineResumed(frame) |
| 158 | +internal fun probeCoroutineSuspended(frame: Continuation<*>) = DebugProbes.probeCoroutineSuspended(frame) |
| 159 | +internal fun <T> probeCoroutineCreated(completion: kotlin.coroutines.Continuation<T>): kotlin.coroutines.Continuation<T> = |
| 160 | + DebugProbes.probeCoroutineCreated(completion) |
0 commit comments