Skip to content

Commit

Permalink
HeapAnalysis string rendering (#1617)
Browse files Browse the repository at this point in the history
Fixes #1333
  • Loading branch information
pyricau committed Nov 14, 2019
1 parent 652a27b commit 129b6a5
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 89 deletions.
100 changes: 99 additions & 1 deletion shark/src/main/java/shark/HeapAnalysis.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,29 @@ data class HeapAnalysisFailure(
* An exception wrapping the actual exception that was thrown.
*/
val exception: HeapAnalysisException
) : HeapAnalysis()
) : HeapAnalysis() {

override fun toString(): String {
return """====================================
HEAP ANALYSIS FAILED
You can report this failure at https://github.com/square/leakcanary/issues
Please provide the stacktrace, metadata and the heap dump file.
====================================
STACKTRACE
$exception====================================
METADATA
Build.VERSION.SDK_INT: ${androidSdkInt()}
Build.MANUFACTURER: ${androidManufacturer()}
LeakCanary version: ${leakCanaryVersion()}
Analysis duration: $analysisDurationMillis ms
Heap dump file path: ${heapDumpFile.absolutePath}
Heap dump timestamp: $createdAtTimeMillis
===================================="""
}
}

/**
* The result of a successful heap analysis performed by [HeapAnalyzer].
Expand All @@ -62,6 +84,35 @@ data class HeapAnalysisSuccess(
*/
val allLeaks: List<Leak>
get() = applicationLeaks + libraryLeaks

override fun toString(): String {
return """====================================
HEAP ANALYSIS RESULT
====================================
${applicationLeaks.size} APPLICATION LEAKS
References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.
${if (applicationLeaks.isNotEmpty()) "\n" + applicationLeaks.joinToString(
"\n\n"
) + "\n" else ""}====================================
${libraryLeaks.size} LIBRARY LEAKS
Leaks coming from the Android Framework or Google libraries.
${if (libraryLeaks.isNotEmpty()) "\n" + libraryLeaks.joinToString(
"\n\n"
) + "\n" else ""}====================================
METADATA
Please include this in bug reports and Stack Overflow questions.
${if (metadata.isNotEmpty()) "\n" + metadata.map { "${it.key}: ${it.value}" }.joinToString(
"\n"
) else ""}
Analysis duration: $analysisDurationMillis ms
Heap dump file path: ${heapDumpFile.absolutePath}
Heap dump timestamp: $createdAtTimeMillis
===================================="""
}
}

/**
Expand Down Expand Up @@ -103,6 +154,10 @@ sealed class Leak : Serializable {
return if (separator == -1) className else className.substring(separator + 1)
}

override fun toString(): String {
return if (retainedHeapByteSize != null) "$retainedHeapByteSize bytes retained" else "" + leakTrace
}

protected abstract fun createGroupHash(): String
}

Expand All @@ -126,6 +181,13 @@ data class LibraryLeak(
val description: String
) : Leak() {
override fun createGroupHash() = pattern.toString().createSHA1Hash()

override fun toString(): String {
return """Known leak pattern: $pattern
Description: $description
${super.toString()}
"""
}
}

/**
Expand All @@ -144,4 +206,40 @@ data class ApplicationLeak(
}
.createSHA1Hash()
}

// Required to avoid the default toString() from data classes
override fun toString(): String {
return super.toString()
}
}

private fun androidSdkInt(): Int {
return try {
val versionClass = Class.forName("android.os.Build\$VERSION")
val sdkIntField = versionClass.getDeclaredField("SDK_INT")
sdkIntField.get(null) as Int
} catch (e: Exception) {
-1
}
}

private fun androidManufacturer(): String {
return try {
val buildClass = Class.forName("android.os.Build")
val manufacturerField = buildClass.getDeclaredField("MANUFACTURER")
manufacturerField.get(null) as String
} catch (e: Exception) {
"Unknown"
}
}

private fun leakCanaryVersion(): String {
return try {
val versionHolderClass = Class.forName("leakcanary.internal.InternalLeakCanary")
val versionField = versionHolderClass.getDeclaredField("version")
versionField.isAccessible = true
versionField.get(null) as String
} catch (e: Exception) {
"Unknown"
}
}
2 changes: 1 addition & 1 deletion shark/src/main/java/shark/HeapAnalysisException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ class HeapAnalysisException(cause: Throwable) : RuntimeException(cause) {
override fun toString(): String {
val stringWriter = StringWriter()
cause!!.printStackTrace(PrintWriter(stringWriter))
return "\n$stringWriter\n"
return stringWriter.toString()
}
}
79 changes: 74 additions & 5 deletions shark/src/main/java/shark/LeakTrace.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package shark

import shark.LeakNodeStatus.LEAKING
import shark.LeakNodeStatus.NOT_LEAKING
import shark.LeakNodeStatus.UNKNOWN
import shark.internal.renderToString
import shark.LeakTraceElement.Holder.ARRAY
import shark.LeakTraceElement.Holder.THREAD
import shark.LeakTraceElement.Type.STATIC_FIELD
import java.io.Serializable
import java.util.Locale

/**
* A chain of references that constitute the shortest strong reference path from a GC root to the
Expand All @@ -16,10 +20,6 @@ data class LeakTrace(
elementMayBeLeakCause(index)
}

override fun toString(): String {
return "\n${renderToString()}\n"
}

fun elementMayBeLeakCause(index: Int): Boolean {
return when (elements[index].leakStatus) {
UNKNOWN -> true
Expand All @@ -31,4 +31,73 @@ data class LeakTrace(
else -> false
}
}

override fun toString(): String {
var result = ""

elements.forEachIndexed { index, element ->
val isLast = index == elements.lastIndex
val nodePrefix = if (!isLast) {
"├─ "
} else {
"╰→ "
}
result += "\n" + nodePrefix + element.className

val contentPrefix = if (!isLast) {
""
} else {
"$ZERO_WIDTH_SPACE "
}

result += "\n" + contentPrefix + "Leaking: " + when (elements[index].leakStatus) {
UNKNOWN -> "UNKNOWN"
NOT_LEAKING -> "NO (${elements[index].leakStatusReason})"
LEAKING -> "YES (${elements[index].leakStatusReason})"
}

for (label in element.labels) {
result += "\n" + contentPrefix + label
}

if (!isLast) {
result += "\n$contentPrefix" + getNextElementString(this, element, index)
}

}
return result
}

companion object {
private fun getNextElementString(
leakTrace: LeakTrace,
element: LeakTraceElement,
index: Int
): String {
val maybeLeakCause = leakTrace.elementMayBeLeakCause(index)

val staticString =
if (element.reference != null && element.reference.type == STATIC_FIELD) "static " else ""
val holderString =
if (element.holder == ARRAY || element.holder == THREAD) {
"${element.holder.name.toLowerCase(Locale.US)} "
} else ""
val simpleClassName = element.classSimpleName
val referenceName = if (element.reference != null) ".${element.reference.displayName}" else ""
val requiredSpaces =
staticString.length + holderString.length + simpleClassName.length + "├─".length
val leakString = if (maybeLeakCause) {
"\n$ELEMENT_DEFAULT_NEW_LINE_SPACE" + " ".repeat(
requiredSpaces
) + "~".repeat(referenceName.length - 1)
} else {
""
}

return staticString + holderString + simpleClassName + referenceName + leakString
}

private const val ZERO_WIDTH_SPACE = '\u200b'
private const val ELEMENT_DEFAULT_NEW_LINE_SPACE = " "
}
}
78 changes: 0 additions & 78 deletions shark/src/main/java/shark/internal/LeakTraceRenderer.kt

This file was deleted.

0 comments on commit 129b6a5

Please sign in to comment.