Skip to content

Commit

Permalink
New experimental Hprof explorer
Browse files Browse the repository at this point in the history
  • Loading branch information
pyricau committed Jul 11, 2019
1 parent eded1c9 commit c8e9fd4
Show file tree
Hide file tree
Showing 5 changed files with 382 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ internal class HeapAnalysisSuccessScreen(
goTo(RenderHeapDumpScreen(heapAnalysis.heapDumpFile))
true
}
menu.add(R.string.leak_canary_options_menu_explore_heap_dump)
.setOnMenuItemClickListener {
goTo(HprofExplorerScreen(heapAnalysis.heapDumpFile))
true
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
package leakcanary.internal.activity.screen

import android.app.AlertDialog
import android.view.View
import android.view.View.OnAttachStateChangeListener
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ListView
import android.widget.TextView
import android.widget.Toast
import com.squareup.leakcanary.core.R
import leakcanary.GraphField
import leakcanary.GraphHeapValue
import leakcanary.GraphObjectRecord.GraphClassRecord
import leakcanary.GraphObjectRecord.GraphInstanceRecord
import leakcanary.GraphObjectRecord.GraphObjectArrayRecord
import leakcanary.GraphObjectRecord.GraphPrimitiveArrayRecord
import leakcanary.HeapValue.BooleanValue
import leakcanary.HeapValue.ByteValue
import leakcanary.HeapValue.CharValue
import leakcanary.HeapValue.DoubleValue
import leakcanary.HeapValue.FloatValue
import leakcanary.HeapValue.IntValue
import leakcanary.HeapValue.LongValue
import leakcanary.HeapValue.ObjectReference
import leakcanary.HeapValue.ShortValue
import leakcanary.HprofGraph
import leakcanary.PrimitiveType.BOOLEAN
import leakcanary.PrimitiveType.BYTE
import leakcanary.PrimitiveType.CHAR
import leakcanary.PrimitiveType.DOUBLE
import leakcanary.PrimitiveType.FLOAT
import leakcanary.PrimitiveType.INT
import leakcanary.PrimitiveType.LONG
import leakcanary.PrimitiveType.SHORT
import leakcanary.Record.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.BooleanArrayDump
import leakcanary.Record.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ByteArrayDump
import leakcanary.Record.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.CharArrayDump
import leakcanary.Record.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.DoubleArrayDump
import leakcanary.Record.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.FloatArrayDump
import leakcanary.Record.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.IntArrayDump
import leakcanary.Record.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.LongArrayDump
import leakcanary.Record.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ShortArrayDump
import leakcanary.internal.activity.db.Io
import leakcanary.internal.activity.db.executeOnIo
import leakcanary.internal.activity.ui.SimpleListAdapter
import leakcanary.internal.navigation.Screen
import leakcanary.internal.navigation.activity
import leakcanary.internal.navigation.inflate
import java.io.Closeable
import java.io.File

internal class HprofExplorerScreen(
private val heapDumpFile: File
) : Screen() {
override fun createView(container: ViewGroup) =
container.inflate(R.layout.leak_canary_hprof_explorer).apply {
container.activity.title = resources.getString(R.string.leak_canary_loading_title)

lateinit var closeable: Closeable

addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
override fun onViewAttachedToWindow(view: View) {
}

override fun onViewDetachedFromWindow(view: View) {
Io.execute {
closeable.close()
}
}
})

executeOnIo {
container.activity.title =
resources.getString(R.string.leak_canary_options_menu_explore_heap_dump)
val pair = HprofGraph.readHprof(heapDumpFile)
val graph = pair.first
closeable = pair.second
updateUi {
val titleView = findViewById<TextView>(R.id.leak_canary_explorer_title)
val searchView = findViewById<View>(R.id.leak_canary_search_button)
val listView = findViewById<ListView>(R.id.leak_canary_explorer_list)
titleView.visibility = VISIBLE
searchView.visibility = VISIBLE
listView.visibility = VISIBLE
searchView.setOnClickListener {
val input = EditText(context)
AlertDialog.Builder(context)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle("Type a fully qualified class name")
.setView(input)
.setPositiveButton(android.R.string.ok) { _, _ ->
executeOnIo {
val partialClassName = input.text.toString()
val matchingClasses = graph.classSequence()
.filter { partialClassName in it.name }
.toList()

if (matchingClasses.isEmpty()) {
updateUi {
Toast.makeText(
context, "No class matching [$partialClassName]", Toast.LENGTH_LONG
)
.show()
}
} else {
updateUi {
titleView.text =
"${matchingClasses.size} classes matching [$partialClassName]"
listView.adapter = SimpleListAdapter(
R.layout.leak_canary_leak_row, matchingClasses
) { view, position ->
val itemTitleView = view.findViewById<TextView>(R.id.leak_canary_row_text)
itemTitleView.text = matchingClasses[position].name
}
listView.setOnItemClickListener { _, _, position, _ ->
val selectedClass = matchingClasses[position]
showClass(titleView, listView, selectedClass)
}
}
}
}
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
}
}
}

private fun View.showClass(
titleView: TextView,
listView: ListView,
selectedClass: GraphClassRecord
) {
executeOnIo {
val className = selectedClass.name
val instances = selectedClass.directInstances.toList()
val staticFields = selectedClass.readStaticFields()
.fieldsAsString()
updateUi {
titleView.text =
"Class $className (${instances.size} instances)"
listView.adapter = SimpleListAdapter(
R.layout.leak_canary_leak_row, staticFields + instances
) { view, position ->
val itemTitleView =
view.findViewById<TextView>(R.id.leak_canary_row_text)
if (position < staticFields.size) {
itemTitleView.text = staticFields[position].second
} else {
itemTitleView.text = "@${instances[position - staticFields.size].objectId}"
}
}
listView.setOnItemClickListener { _, _, position, _ ->
if (position < staticFields.size) {
val staticField = staticFields[position].first
onHeapValueClicked(titleView, listView, staticField.value)
} else {
val instance = instances[position - staticFields.size]
showInstance(titleView, listView, instance)
}
}
}
}
}

private fun View.showInstance(
titleView: TextView,
listView: ListView,
instance: GraphInstanceRecord
) {
executeOnIo {
val fields = instance.readFields()
.fieldsAsString()
val className = instance.className
updateUi {
titleView.text = "Instance @${instance.objectId} of class $className"
listView.adapter = SimpleListAdapter(
R.layout.leak_canary_leak_row, fields
) { view, position ->
val itemTitleView =
view.findViewById<TextView>(R.id.leak_canary_row_text)
itemTitleView.text = fields[position].second
}
listView.setOnItemClickListener { _, _, position, _ ->
val field = fields[position].first
onHeapValueClicked(titleView, listView, field.value)
}
}
}
}

private fun View.showObjectArray(
titleView: TextView,
listView: ListView,
instance: GraphObjectArrayRecord
) {
executeOnIo {
val elements = instance.readElements()
.mapIndexed { index: Int, element: GraphHeapValue ->
element to "[$index] = ${element.heapValueAsString()}"
}
.toList()
val arrayClassName = instance.arrayClassName
val className = arrayClassName.substring(0, arrayClassName.length - 2)
updateUi {
titleView.text = "Array $className[${elements.size}]"
listView.adapter = SimpleListAdapter(
R.layout.leak_canary_leak_row, elements
) { view, position ->
val itemTitleView =
view.findViewById<TextView>(R.id.leak_canary_row_text)
itemTitleView.text = elements[position].second
}
listView.setOnItemClickListener { _, _, position, _ ->
val element = elements[position].first
onHeapValueClicked(titleView, listView, element)
}
}
}
}

private fun View.showPrimitiveArray(
titleView: TextView,
listView: ListView,
instance: GraphPrimitiveArrayRecord
) {
executeOnIo {
val (type, values) = when (val record = instance.readRecord()) {
is BooleanArrayDump -> "boolean" to record.array.map { it.toString() }
is CharArrayDump -> "char" to record.array.map { "'$it'" }
is FloatArrayDump -> "float" to record.array.map { it.toString() }
is DoubleArrayDump -> "double" to record.array.map { it.toString() }
is ByteArrayDump -> "byte" to record.array.map { it.toString() }
is ShortArrayDump -> "short" to record.array.map { it.toString() }
is IntArrayDump -> "int" to record.array.map { it.toString() }
is LongArrayDump -> "long" to record.array.map { it.toString() }
}
updateUi {
titleView.text = "Array $type[${values.size}]"
listView.adapter = SimpleListAdapter(
R.layout.leak_canary_leak_row, values
) { view, position ->
val itemTitleView =
view.findViewById<TextView>(R.id.leak_canary_row_text)
itemTitleView.text = "$type ${values[position]}"
}
listView.setOnItemClickListener { _, _, _, _ ->
}
}
}
}

private fun View.onHeapValueClicked(
titleView: TextView,
listView: ListView,
graphHeapValue: GraphHeapValue
) {
if (graphHeapValue.isNonNullReference) {
when (val objectRecord = graphHeapValue.asObject!!) {
is GraphInstanceRecord -> {
showInstance(titleView, listView, objectRecord)
}
is GraphClassRecord -> {
showClass(titleView, listView, objectRecord)
}
is GraphObjectArrayRecord -> {
showObjectArray(titleView, listView, objectRecord)
}
is GraphPrimitiveArrayRecord -> {
showPrimitiveArray(titleView, listView, objectRecord)
}
}
}
}

private fun Sequence<GraphField>.fieldsAsString(): List<Pair<GraphField, String>> {
return map { field ->
field to "${field.classRecord.simpleName}.${field.name} = ${field.value.heapValueAsString()}"
}
.toList()
}

private fun GraphHeapValue.heapValueAsString(): String {
return when (val heapValue = actual) {
is ObjectReference -> {
if (isNullReference) {
"null"
} else {
when (val objectRecord = asObject!!) {
is GraphInstanceRecord -> {
if (objectRecord instanceOf "java.lang.String") {
"${objectRecord.className}@${heapValue.value} \"${objectRecord.readAsJavaString()!!}\""
} else {
"${objectRecord.className}@${heapValue.value}"
}
}
is GraphClassRecord -> {
"Class ${objectRecord.name}"
}
is GraphObjectArrayRecord -> {
objectRecord.arrayClassName
}
is GraphPrimitiveArrayRecord -> when (objectRecord.primitiveType) {
BOOLEAN -> "boolean[]"
CHAR -> "char[]"
FLOAT -> "float[]"
DOUBLE -> "double[]"
BYTE -> "byte[]"
SHORT -> "short[]"
INT -> "int[]"
LONG -> "long[]"
}
}
}
}
is BooleanValue -> "boolean ${heapValue.value}"
is CharValue -> "char ${heapValue.value}"
is FloatValue -> "float ${heapValue.value}"
is DoubleValue -> "double ${heapValue.value}"
is ByteValue -> "byte ${heapValue.value}"
is ShortValue -> "short ${heapValue.value}"
is IntValue -> "int ${heapValue.value}"
is LongValue -> "long ${heapValue.value}"
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/leak_canary_background_color"
android:orientation="vertical"
>
<Button
android:id="@+id/leak_canary_search_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/leak_canary_explorer_search_classes"
android:visibility="invisible"
/>
<TextView
android:id="@+id/leak_canary_explorer_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="invisible"
/>
<ListView
android:id="@+id/leak_canary_explorer_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@null"
android:dividerHeight="0dp"
android:visibility="invisible"
/>
</LinearLayout>
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<string name="leak_canary_analysis_success_notification">Analysis done: %1$d leaks (%2$d new, %3$d known, %4$d won\'t fix)</string>
<string name="leak_canary_class_has_leaked">%1$s Leaked</string>
<string name="leak_canary_download_dump">You can download the heap dump via \"Menu > Share Heap Dump\" or \"adb pull %1$s\"</string>
<string name="leak_canary_explorer_search_classes">Search classes</string>
<string name="leak_canary_loading_title">Loading…</string>
<string name="leak_canary_notification_analysing">Analyzing Heap Dump</string>
<string name="leak_canary_notification_channel_low">LeakCanary Low Priority</string>
Expand Down Expand Up @@ -82,6 +83,7 @@
<string name="leak_canary_options_menu_import_hprof_file">Import &amp; Analyze Hprof File</string>
<string name="leak_canary_options_menu_see_analysis_list">See Analysis List</string>
<string name="leak_canary_options_menu_render_heap_dump">Render Heap Dump</string>
<string name="leak_canary_options_menu_explore_heap_dump">Explore Heap Dump</string>
<string name="leak_canary_heap_analysis_success_screen_no_path_to_instance_count">%d non leaking retained instances</string>
<string name="leak_canary_help_title">Tap here to learn more</string>
</resources>
Loading

0 comments on commit c8e9fd4

Please sign in to comment.