diff --git a/java/debugger/impl/resources/META-INF/java-debugger.xml b/java/debugger/impl/resources/META-INF/java-debugger.xml index 5a46251d3f6b..c40e2c4cf693 100644 --- a/java/debugger/impl/resources/META-INF/java-debugger.xml +++ b/java/debugger/impl/resources/META-INF/java-debugger.xml @@ -205,6 +205,8 @@ description="Allow to compile the code before evaluation if needed"/> + diff --git a/java/debugger/impl/src/com/intellij/debugger/engine/AsyncStacksUtils.java b/java/debugger/impl/src/com/intellij/debugger/engine/AsyncStacksUtils.java index 665620482aa9..4a227b92dde8 100644 --- a/java/debugger/impl/src/com/intellij/debugger/engine/AsyncStacksUtils.java +++ b/java/debugger/impl/src/com/intellij/debugger/engine/AsyncStacksUtils.java @@ -141,25 +141,30 @@ public final class AsyncStacksUtils { result -> result instanceof StringReference ? ((StringReference)result).value() : null, evaluationContext); if (value != null) { - List res = new ArrayList<>(); - try (DataInputStream dis = new DataInputStream(new ByteArrayInputStream(value.getBytes(StandardCharsets.ISO_8859_1)))) { - while (dis.available() > 0) { - StackFrameItem item = null; - if (dis.readBoolean()) { - String className = dis.readUTF(); - String methodName = dis.readUTF(); - int line = dis.readInt(); - Location location = - DebuggerUtilsEx.findOrCreateLocation(virtualMachineProxy.getVirtualMachine(), className, methodName, line); - item = new StackFrameItem(location, null); - } - res.add(item); + return parseAgentAsyncStackTrace(value, virtualMachineProxy); + } + return null; + } + + @ApiStatus.Internal + public static List parseAgentAsyncStackTrace(String value, VirtualMachineProxyImpl vm) { + List res = new ArrayList<>(); + try (DataInputStream dis = new DataInputStream(new ByteArrayInputStream(value.getBytes(StandardCharsets.ISO_8859_1)))) { + while (dis.available() > 0) { + StackFrameItem item = null; + if (dis.readBoolean()) { + String className = dis.readUTF(); + String methodName = dis.readUTF(); + int line = dis.readInt(); + Location location = DebuggerUtilsEx.findOrCreateLocation(vm.getVirtualMachine(), className, methodName, line); + item = new StackFrameItem(location, null); } - return res; - } - catch (Exception e) { - DebuggerUtilsImpl.logError(e); + res.add(item); } + return res; + } + catch (Exception e) { + DebuggerUtilsImpl.logError(e); } return null; } @@ -363,6 +368,9 @@ public final class AsyncStacksUtils { if (!Registry.is("debugger.async.stack.trace.for.exceptions.printing", false)) { parametersList.addProperty("debugger.agent.support.throwable", "false"); } + if (Registry.is("debugger.async.stack.trace.for.all.threads")) { + parametersList.addProperty("debugger.async.stack.trace.for.all.threads", "true"); + } } } else { diff --git a/java/java-runtime/src/com/intellij/rt/debugger/coroutines/CoroutinesDebugHelper.java b/java/java-runtime/src/com/intellij/rt/debugger/coroutines/CoroutinesDebugHelper.java index d985c4118e05..97ba8dc77604 100644 --- a/java/java-runtime/src/com/intellij/rt/debugger/coroutines/CoroutinesDebugHelper.java +++ b/java/java-runtime/src/com/intellij/rt/debugger/coroutines/CoroutinesDebugHelper.java @@ -17,10 +17,12 @@ public final class CoroutinesDebugHelper { private static final String COROUTINE_CONTEXT_FQN = "kotlin.coroutines.CoroutineContext"; private static final String COROUTINE_JOB_FQN = "kotlinx.coroutines.Job"; private static final String COROUTINE_CONTEXT_KEY_FQN = "kotlin.coroutines.CoroutineContext$Key"; + private static final String DEBUGGER_AGENT_CAPTURE_STORAGE_FQN = "com.intellij.rt.debugger.agent.CaptureStorage"; - public static long[] getCoroutinesRunningOnCurrentThread(Object debugProbes, Thread currentThread) throws ReflectiveOperationException { + public static long[] getCoroutinesRunningOnCurrentThread(Class debugProbesImplClass, Thread currentThread) throws ReflectiveOperationException { + Object debugProbesImplInstance = debugProbesImplClass.getField("INSTANCE").get(null); List coroutinesIds = new ArrayList<>(); - List infos = (List)invoke(debugProbes, "dumpCoroutinesInfo"); + List infos = (List)invoke(debugProbesImplInstance, "dumpCoroutinesInfo"); for (Object info : infos) { if (invoke(info, "getLastObservedThread") == currentThread) { coroutinesIds.add((Long)invoke(info, "getSequenceNumber")); @@ -154,10 +156,8 @@ public final class CoroutinesDebugHelper { return current.getClass().getSimpleName().contains(COROUTINE_OWNER_CLASS); } - public static Object[] dumpCoroutinesInfoAsJsonAndReferences() throws ReflectiveOperationException { - ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + public static Object[] dumpCoroutinesInfoAsJsonAndReferences(Class debugProbesImplClass) { try { - Class debugProbesImplClass = classLoader.loadClass("kotlinx.coroutines.debug.internal.DebugProbesImpl"); Object debugProbesImplInstance = debugProbesImplClass.getField("INSTANCE").get(null); Object[] infos = (Object[])invoke(debugProbesImplInstance, "dumpCoroutinesInfoAsJsonAndReferences"); return infos; @@ -166,10 +166,8 @@ public final class CoroutinesDebugHelper { } } - public static Object[] dumpCoroutinesWithStacktracesAsJson() throws ReflectiveOperationException { - ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + public static Object[] dumpCoroutinesWithStacktracesAsJson(Class debugProbesImplClass) { try { - Class debugProbesImplClass = classLoader.loadClass("kotlinx.coroutines.debug.internal.DebugProbesImpl"); Object debugProbesImplInstance = debugProbesImplClass.getField("INSTANCE").get(null); Object[] dump = (Object[])invoke(debugProbesImplInstance, "dumpCoroutinesInfoAsJsonAndReferences"); Object[] coroutineInfos = (Object[])dump[3]; @@ -177,19 +175,50 @@ public final class CoroutinesDebugHelper { for (int i = 0; i < coroutineInfos.length; i++) { lastObservedStackTraces[i] = lastObservedStackTrace(coroutineInfos[i]); } - dump = Arrays.copyOf(dump, dump.length + 1); + dump = Arrays.copyOf(dump, dump.length + 2); dump[4] = lastObservedStackTraces; + + Object[] lastObservedThreads = (Object[])dump[1]; + dump[5] = getAsyncStackTracesForThreads(lastObservedThreads); return dump; } catch (Throwable e) { return null; } } - public static String lastObservedStackTrace(Object debugCoroutineInfo) throws ReflectiveOperationException { + private static String lastObservedStackTrace(Object debugCoroutineInfo) throws ReflectiveOperationException { List stackTrace = (List)invoke(debugCoroutineInfo, "lastObservedStackTrace"); return JsonUtils.dumpStackTraceElements(stackTrace); } + /** + * Invokes com.intellij.rt.debugger.agent.CaptureStorage#getAllCapturedStacks method + * which returns a map of threads to their captured async stack traces. + * If `debugger.async.stack.trace.for.all.threads` is false, only the current thread's stack trace is returned. + * + * If debugger-agent is not available, e.g. in attach, returns null + */ + private static String[] getAsyncStackTracesForThreads(Object[] threads) { + try { + Class captureStorageClass = Class.forName(DEBUGGER_AGENT_CAPTURE_STORAGE_FQN, false, null); + Method getAllCapturedStacks = captureStorageClass.getMethod("getAllCapturedStacks", int.class); + + Map threadToStackTrace = (Map)invoke(null, getAllCapturedStacks, 500); + + String[] asyncStackTraces = new String[threads.length]; + + for (int i = 0; i < threads.length; i++) { + Object thread = threads[i]; + if (thread != null) { + asyncStackTraces[i] = threadToStackTrace.get(thread); + } + } + return asyncStackTraces; + } catch (Throwable e) { + return null; + } + } + /** * This method takes the array of {@link kotlinx.coroutines.debug.internal.DebugCoroutineInfo} instances * and for each coroutine finds it's job and the first parent, which corresponds to some coroutine, captured in the dump. diff --git a/java/testFramework/src/com/intellij/debugger/impl/OutputChecker.java b/java/testFramework/src/com/intellij/debugger/impl/OutputChecker.java index 981161089258..956eca9b1222 100644 --- a/java/testFramework/src/com/intellij/debugger/impl/OutputChecker.java +++ b/java/testFramework/src/com/intellij/debugger/impl/OutputChecker.java @@ -262,6 +262,7 @@ public class OutputChecker { result = result.replace("-Ddebugger.agent.enable.coroutines=true ", ""); result = result.replace("-Dkotlinx.coroutines.debug.enable.flows.stack.trace=true ", ""); result = result.replace("-Dkotlinx.coroutines.debug.enable.mutable.state.flows.stack.trace=true ", ""); + result = result.replace("-Ddebugger.async.stack.trace.for.all.threads=true ", ""); result = result.replace("-Ddebugger.agent.support.throwable=false ", ""); result = result.replaceAll("\\((.*):\\d+\\)", "($1:!LINE_NUMBER!)"); diff --git a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineStackFrameInterceptor.kt b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineStackFrameInterceptor.kt index 9747234ff313..911cbe271fc0 100644 --- a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineStackFrameInterceptor.kt +++ b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/CoroutineStackFrameInterceptor.kt @@ -84,17 +84,18 @@ private class CoroutineStackFrameInterceptor : StackFrameInterceptor { if (continuationFilter != null) return continuationFilter // If continuation could not be extracted or the root continuation was not an instance of BaseContinuationImpl, // dump coroutines running on the current thread and compute [CoroutineIdFilter]. - val debugProbesImpl = DebugProbesImpl.instance(defaultExecutionContext) - return if (debugProbesImpl != null && debugProbesImpl.isInstalled) { - // first try the helper, it is the fastest way, then try the mirror - val currentCoroutines = getCoroutinesRunningOnCurrentThreadFromHelper(defaultExecutionContext, debugProbesImpl) - ?: debugProbesImpl.getCoroutinesRunningOnCurrentThread(defaultExecutionContext) - - if (currentCoroutines.isNotEmpty()) CoroutineIdFilter(currentCoroutines) - else null + val currentCoroutines = getCoroutinesRunningOnCurrentThreadFromHelper(defaultExecutionContext) + ?: run { + val debugProbesImpl = DebugProbesImpl.instance(defaultExecutionContext) + if (debugProbesImpl != null && debugProbesImpl.isInstalled) { + debugProbesImpl.getCoroutinesRunningOnCurrentThread(defaultExecutionContext) + } else null + } + return if (currentCoroutines != null) { + if (currentCoroutines.isNotEmpty()) CoroutineIdFilter(currentCoroutines) else null } else { //TODO: IDEA-341142 show nice notification about this - thisLogger().warn("[coroutine filter]: kotlinx-coroutines debug agent was not enabled, DebugProbesImpl class is not found.") + thisLogger().warn("[coroutine filter]: kotlinx-coroutines debug agent was not enabled or DebugProbesImpl class is not found.") null } } @@ -181,11 +182,11 @@ private class CoroutineStackFrameInterceptor : StackFrameInterceptor { } private fun getCoroutinesRunningOnCurrentThreadFromHelper( - context: DefaultExecutionContext, - debugProbesImpl: DebugProbesImpl + context: DefaultExecutionContext ): Set? { val threadReferenceProxyImpl = context.suspendContext.thread ?: return null - val args = listOf(debugProbesImpl.getObject(), threadReferenceProxyImpl.threadReference) + val debugProbesImplClass = context.vm.findDebugProbesImplClass() ?: return null + val args = listOf(debugProbesImplClass, threadReferenceProxyImpl.threadReference) val result = callMethodFromHelper(CoroutinesDebugHelper::class.java, context, "getCoroutinesRunningOnCurrentThread", args) result ?: return null return (result as ArrayReference).values.asSequence().map { (it as LongValue).value() }.toHashSet() diff --git a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/coroutineInfoDatas.kt b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/coroutineInfoDatas.kt index 927890a4fd77..8f7954f63015 100644 --- a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/coroutineInfoDatas.kt +++ b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/data/coroutineInfoDatas.kt @@ -29,7 +29,8 @@ open class CoroutineInfoData( val lastObservedThread: ThreadReference?, val debugCoroutineInfoRef: ObjectReference?, private val stackFrameProvider: CoroutineStackFramesProvider?, - val lastObservedStackTrace: List = emptyList() + val lastObservedStackTrace: List = emptyList(), + val asyncStackTrace: List = emptyList() ) { val name: String = name ?: DEFAULT_COROUTINE_NAME diff --git a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineInfoProvider.kt b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineInfoProvider.kt index 5e3affeed9b8..f97a62de1ff2 100644 --- a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineInfoProvider.kt +++ b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/proxy/CoroutineInfoProvider.kt @@ -3,6 +3,8 @@ package org.jetbrains.kotlin.idea.debugger.coroutine.proxy import com.google.gson.Gson +import com.intellij.debugger.engine.AsyncStacksUtils +import com.intellij.openapi.util.registry.Registry import com.intellij.rt.debugger.JsonUtils import com.intellij.rt.debugger.coroutines.CoroutinesDebugHelper import com.sun.jdi.ArrayReference @@ -15,6 +17,7 @@ import org.jetbrains.kotlin.idea.debugger.base.util.evaluate.DefaultExecutionCon import org.jetbrains.kotlin.idea.debugger.coroutine.callMethodFromHelper import org.jetbrains.kotlin.idea.debugger.coroutine.data.* import org.jetbrains.kotlin.idea.debugger.coroutine.proxy.mirror.* +import org.jetbrains.kotlin.idea.debugger.coroutine.util.findDebugProbesImplClass import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger import org.jetbrains.kotlin.utils.addToStdlib.safeAs import kotlin.reflect.typeOf @@ -29,8 +32,8 @@ internal class CoroutinesInfoFromJsonAndReferencesProvider( private val stackFramesProvider = CoroutineStackFramesProvider(executionContext) override fun dumpCoroutinesInfo(): List? { - - val array = callMethodFromHelper(CoroutinesDebugHelper::class.java, executionContext, "dumpCoroutinesInfoAsJsonAndReferences", emptyList()) + val debugProbesImplClass = executionContext.vm.findDebugProbesImplClass() ?: return null + val array = callMethodFromHelper(CoroutinesDebugHelper::class.java, executionContext, "dumpCoroutinesInfoAsJsonAndReferences", listOf(debugProbesImplClass)) ?: fallbackToOldMirrorDump(executionContext) val arrayValues = (array as? ArrayReference)?.values ?: return null @@ -55,16 +58,21 @@ internal class CoroutinesInfoFromJsonAndReferencesProvider( error("Arrays must have equal sizes") } - return calculateCoroutineInfoData(coroutinesInfo, coroutineInfoRefs, lastObservedThreadRefs, lastObservedFrameRefs, null) + return calculateCoroutineInfoData(coroutinesInfo, coroutineInfoRefs, lastObservedThreadRefs, lastObservedFrameRefs, null, null) } fun dumpCoroutinesWithStacktraces(): List? { - val array = callMethodFromHelper(CoroutinesDebugHelper::class.java, executionContext, "dumpCoroutinesWithStacktracesAsJson", emptyList(), JsonUtils::class.java.name) - + val debugProbesImplClass = executionContext.vm.findDebugProbesImplClass() ?: return null + val array = callMethodFromHelper( + CoroutinesDebugHelper::class.java, executionContext, + "dumpCoroutinesWithStacktracesAsJson", + listOf(debugProbesImplClass), + JsonUtils::class.java.name + ) val arrayValues = (array as? ArrayReference)?.values ?: return null - if (arrayValues.size != 5) { - error("The result array of 'dumpCoroutinesWithStacktracesAsJson' should be of size 5") + if (arrayValues.size != 6) { + error("The result array of 'dumpCoroutinesWithStacktracesAsJson' should be of size 6") } val coroutinesInfoAsJsonString = arrayValues[0].safeAs()?.value() @@ -77,22 +85,45 @@ internal class CoroutinesInfoFromJsonAndReferencesProvider( ?: error("The 4th element of the result array must be an array") val lastObservedStackTraceJsons = arrayValues[4].safeAs()?.toTypedList() ?: error("The 5th element of the result array must be an array") + val asyncStackTraceJsons = arrayValues[5].safeAs()?.toTypedList() val coroutinesInfo = Gson().fromJson(coroutinesInfoAsJsonString, Array::class.java) val lastObservedStackTraces: List> = lastObservedStackTraceJsons.map { - Gson().fromJson(it.value(), Array::class.java).map { ste -> - findOrCreateLocation(executionContext, ste.stackTraceElement()) + Gson().fromJson(it.value(), Array::class.java) + .map { ste -> + findOrCreateLocation(executionContext, ste.stackTraceElement()) + } + } + + val asyncStackTraces: List>? = asyncStackTraceJsons?.map { stackTrace -> + if (stackTrace == null) emptyList() + else { + AsyncStacksUtils.parseAgentAsyncStackTrace(stackTrace.value(), executionContext.vm) + .mapNotNull { it?.location() } } } + if (Registry.`is`("debugger.async.stack.trace.for.all.threads") && asyncStackTraces == null) { + log.error("Could not obtain async stack traces, suspendContext = ${executionContext.suspendContext}") + } + if (lastObservedStackTraces.size != lastObservedFrameRefs.size || lastObservedFrameRefs.size != coroutinesInfo.size || coroutineInfoRefs.size != coroutinesInfo.size || - coroutinesInfo.size != lastObservedThreadRefs.size) { + coroutinesInfo.size != lastObservedThreadRefs.size || + (asyncStackTraces != null && asyncStackTraces.size != coroutinesInfo.size) + ) { error("Arrays must have equal sizes") } - return calculateCoroutineInfoData(coroutinesInfo, coroutineInfoRefs, lastObservedThreadRefs, lastObservedFrameRefs, lastObservedStackTraces) + return calculateCoroutineInfoData( + coroutinesInfo, + coroutineInfoRefs, + lastObservedThreadRefs, + lastObservedFrameRefs, + lastObservedStackTraces, + asyncStackTraces + ) } private fun fallbackToOldMirrorDump(executionContext: DefaultExecutionContext): ArrayReference? { @@ -107,7 +138,8 @@ internal class CoroutinesInfoFromJsonAndReferencesProvider( coroutineInfoRefs: List, lastObservedThreadRefs: List, lastObservedFrameRefs: List, - lastObservedStackTraces: List>? + lastObservedStackTraces: List>?, + asyncStackTraces: List>? ): List { return coroutineInfoRefs.mapIndexed { i, ref -> val info = coroutineInfos[i] @@ -120,7 +152,8 @@ internal class CoroutinesInfoFromJsonAndReferencesProvider( lastObservedThread = lastObservedThreadRefs[i], debugCoroutineInfoRef = ref, stackFrameProvider = stackFramesProvider, - lastObservedStackTrace = lastObservedStackTraces?.get(i) ?: emptyList() + lastObservedStackTrace = lastObservedStackTraces?.get(i) ?: emptyList(), + asyncStackTrace = asyncStackTraces?.get(i) ?: emptyList() ) } } diff --git a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/util/CoroutineUtils.kt b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/util/CoroutineUtils.kt index 66365e056b75..25f7df9fb50f 100644 --- a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/util/CoroutineUtils.kt +++ b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/util/CoroutineUtils.kt @@ -9,6 +9,7 @@ import com.intellij.debugger.engine.SuspendContextImpl import com.intellij.debugger.impl.DebuggerUtilsEx import com.intellij.debugger.jdi.StackFrameProxyImpl import com.intellij.debugger.jdi.ThreadReferenceProxyImpl +import com.intellij.debugger.jdi.VirtualMachineProxyImpl import com.intellij.openapi.application.ReadAction import com.intellij.openapi.diagnostic.thisLogger import com.intellij.xdebugger.XSourcePosition @@ -20,6 +21,7 @@ import org.jetbrains.kotlin.idea.util.application.isUnitTestMode const val CREATION_STACK_TRACE_SEPARATOR = "\b\b\b" // the "\b\b\b" is used as creation stacktrace separator in kotlinx.coroutines const val CREATION_CLASS_NAME = "_COROUTINE._CREATION" +private const val DEBUG_PROBES_IMPL_CLASS_FQNAME = "kotlinx.coroutines.debug.internal.DebugProbesImpl" fun Method.isInvokeSuspend(): Boolean = name() == KotlinDebuggerConstants.INVOKE_SUSPEND_METHOD_NAME && signature() == "(Ljava/lang/Object;)Ljava/lang/Object;" @@ -145,3 +147,6 @@ fun Location.isFilterFromTop(location: Location?): Boolean = fun Location.isFilterFromBottom(location: Location?): Boolean = sameLineAndMethod(location) + +internal fun VirtualMachineProxyImpl.findDebugProbesImplClass(): ClassObjectReference? = + classesByName(DEBUG_PROBES_IMPL_CLASS_FQNAME).firstOrNull()?.classObject() \ No newline at end of file diff --git a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutinesDumpAsyncProvider.kt b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutinesDumpAsyncProvider.kt index f576079ecc92..d008d569f432 100644 --- a/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutinesDumpAsyncProvider.kt +++ b/plugins/kotlin/jvm-debugger/coroutines/src/org/jetbrains/kotlin/idea/debugger/coroutine/view/CoroutinesDumpAsyncProvider.kt @@ -61,12 +61,22 @@ private class CoroutineDumpItem(info: CoroutineInfoData) : MergeableDumpItem { private val dispatcher = info.dispatcher + private val lastObservedStackTrace: String = info.lastObservedStackTrace.joinToString(prefix = "\t", separator = "\n\t") { + ThreadDumpAction.renderLocation(it) + } + override val stackTrace: String = - info.coroutineDescriptor + "\n" + - info.lastObservedStackTrace.joinToString(prefix = "\t", separator = "\n\t") { ThreadDumpAction.renderLocation(it) } + buildString { + appendLine(info.coroutineDescriptor) + appendLine(lastObservedStackTrace) + if (info.asyncStackTrace.isNotEmpty()) { + appendLine("\t--------- Async Stack Trace ---------") + appendLine(info.asyncStackTrace.joinToString(prefix = "\t", separator = "\n\t") { ThreadDumpAction.renderLocation(it) }) + } + } override val interestLevel: Int = when { - info.lastObservedStackTrace.isEmpty() -> -10 + stackTrace.isEmpty() -> -10 else -> stackTrace.count { it == '\n' } }