diff --git a/runtime/src/main/kotlin/kotlin/Throwable.kt b/runtime/src/main/kotlin/kotlin/Throwable.kt index 832cbe30cb2..ce9ec8d9c9f 100644 --- a/runtime/src/main/kotlin/kotlin/Throwable.kt +++ b/runtime/src/main/kotlin/kotlin/Throwable.kt @@ -27,7 +27,7 @@ public open class Throwable(open val message: String?, open val cause: Throwable constructor() : this(null, null) @get:ExportForCppRuntime("Kotlin_Throwable_getStackTrace") - private val stackTrace = getCurrentStackTrace() + private val stackTrace: NativePtrArray = getCurrentStackTrace() private val stackTraceStrings: Array by lazy { getStackTraceStrings(stackTrace).freeze() @@ -45,40 +45,83 @@ public open class Throwable(open val message: String?, open val cause: Throwable /** * Prints the [detailed description][Throwable.stackTraceToString] of this throwable to the standard output. */ - public fun printStackTrace(): Unit = dumpStackTrace("", "", { println(it) }, mutableSetOf()) + public fun printStackTrace(): Unit = ExceptionTraceBuilder(this).print() - internal fun dumpStackTrace(): String = buildString { - dumpStackTrace("", "", { appendln(it) }, mutableSetOf()) - } + internal fun dumpStackTrace(): String = ExceptionTraceBuilder(this).build() - private fun Throwable.dumpStackTrace(indent: String, qualifier: String, writeln: (String) -> Unit, visited: MutableSet) { - this.dumpSelfTrace(indent, qualifier, writeln, visited) || return + private class ExceptionTraceBuilder(private val top: Throwable) { + private val target = StringBuilder() + private var printOut = false + private val visited = mutableSetOf() - var cause = this.cause - while (cause != null) { - // TODO: should skip common stack frames - cause.dumpSelfTrace(indent, "Caused by: ", writeln, visited) - cause = cause.cause + fun build(): String { + top.dumpFullTrace("", "") + return target.toString() } - } - private fun Throwable.dumpSelfTrace(indent: String, qualifier: String, writeln: (String) -> Unit, visited: MutableSet): Boolean { - if (!visited.add(this)) { - writeln(indent + qualifier + "[CIRCULAR REFERENCE, SEE ABOVE: $this]") - return false + fun print() { + printOut = true + top.dumpFullTrace("", "") } - writeln(indent + qualifier + this.toString()) - for (element in stackTraceStrings) { - writeln("$indent\tat $element") - } - val suppressed = suppressedExceptionsList - if (!suppressed.isNullOrEmpty()) { - val suppressedIndent = indent + '\t' - for (s in suppressed) { - s.dumpStackTrace(suppressedIndent, "Suppressed: ", writeln, visited) + + private fun StringBuilder.endln() { + if (printOut) { + println(this) + clear() + } else { + appendln() } } - return true + + private fun Throwable.dumpFullTrace(indent: String, qualifier: String) { + this.dumpSelfTrace(indent, qualifier) || return + + var cause = this.cause + while (cause != null) { + cause.dumpSelfTrace(indent, "Caused by: ") + cause = cause.cause + } + } + + private fun Throwable.dumpSelfTrace(indent: String, qualifier: String): Boolean { + if (!visited.add(this)) { + target.append(indent).append(qualifier).append("[CIRCULAR REFERENCE, SEE ABOVE: ").append(this).append("]").endln() + return false + } + target.append(indent).append(qualifier).append(this).endln() + // leave 1 common frame to ease matching with the top exception stack + val commonFrames = (commonStackFrames() - 1).coerceAtLeast(0) + for (frameIndex in 0 until stackTraceStrings.size - commonFrames) { + val element = stackTraceStrings[frameIndex] + target.append(indent).append(" at ").append(element).endln() + } + if (commonFrames > 0) { + target.append(indent).append(" ... and ").append(commonFrames).append(" more common stack frames skipped").endln() + } + val suppressed = suppressedExceptionsList + if (!suppressed.isNullOrEmpty()) { + val suppressedIndent = indent + " " + for (s in suppressed) { + s.dumpFullTrace(suppressedIndent, "Suppressed: ") + } + } + return true + } + + private fun Throwable.commonStackFrames(): Int { + if (top === this) return 0 + val topStack = top.stackTrace + val topSize = topStack.size + val thisStack = this.stackTrace + val thisSize = thisStack.size + val maxSize = minOf(topSize, thisSize) + var frame = 0 + while (frame < maxSize) { + if (thisStack[thisSize - 1 - frame] != topStack[topSize - 1 - frame]) break + frame++ + } + return frame + } }