Skip to content

Commit

Permalink
Extract metadata from heap dump
Browse files Browse the repository at this point in the history
Fixes #1519
  • Loading branch information
pyricau committed Nov 14, 2019
1 parent 4afcda2 commit 0c40611
Show file tree
Hide file tree
Showing 15 changed files with 215 additions and 15 deletions.
3 changes: 2 additions & 1 deletion docs/changelog.md
Expand Up @@ -2,7 +2,8 @@

## Next release

* Adding support for deobfuscation using Proguard mapping files in Shark [#1499](https://github.com/square/leakcanary/issues/1499)
* Added support for deobfuscation using Proguard mapping files in Shark [#1499](https://github.com/square/leakcanary/issues/1499)
* Added support for extracting metadata from the heap dump (see the[recipe](recipes.md#extracting-additional-metadata-from-the-heap-dump)) [#1519](https://github.com/square/leakcanary/issues/1519)
* Several performance improvements when parsing heap dumps
* Fixed several bugs and crashes
* Added new known leak patterns
Expand Down
36 changes: 36 additions & 0 deletions docs/recipes.md
Expand Up @@ -298,3 +298,39 @@ dependencies {
devDebugImplementation "com.squareup.leakcanary:leakcanary-android:${version}"
}
```

## Extracting additional metadata from the heap dump

[LeakCanary.Config.metatadaExtractor](/leakcanary/api/leakcanary-android-core/leakcanary/-leak-canary/-config/metatadaExtractor/) extracts metadata from a hprof. The metadata is then available in `HeapAnalysisSuccess.metadata`. If defaults to `AndroidMetadataExtractor` but you can override it to extract additional metadata from the hprof.

For example, if you want to include the app version name in your heap analysis reports, you need to first store it in memory (e.g. in a static field) and then you can retrieve it in `MetadataExtractor`.

```kotlin
class DebugExampleApplication : ExampleApplication() {

companion object {
@JvmStatic
lateinit var savedVersionName: String
}

override fun onCreate() {
super.onCreate()

val packageInfo = packageManager.getPackageInfo(packageName, 0)
savedVersionName = packageInfo.versionName

LeakCanary.config = LeakCanary.config.copy(
metatadaExtractor = MetadataExtractor { graph ->
val companionClass =
graph.findClassByName("com.example.DebugExampleApplication")!!

val versionNameField = companionClass["savedVersionName"]!!
val versionName = versionNameField.valueAsInstance!!.readAsJavaString()!!

val defaultMetadata = AndroidMetadataExtractor.extractMetadata(graph)

mapOf("App Version Name" to versionName) + defaultMetadata
})
}
}
```
11 changes: 11 additions & 0 deletions leakcanary-android-core/src/main/java/leakcanary/LeakCanary.kt
Expand Up @@ -3,10 +3,13 @@ package leakcanary
import android.content.Intent
import leakcanary.LeakCanary.config
import leakcanary.internal.InternalLeakCanary
import shark.AndroidMetadataExtractor
import shark.AndroidObjectInspectors
import shark.AndroidReferenceMatchers
import shark.HeapAnalysisSuccess
import shark.IgnoredReferenceMatcher
import shark.LibraryLeakReferenceMatcher
import shark.MetadataExtractor
import shark.ObjectInspector
import shark.ReferenceMatcher
import shark.SharkLog
Expand Down Expand Up @@ -92,6 +95,14 @@ object LeakCanary {
*/
val onHeapAnalyzedListener: OnHeapAnalyzedListener = DefaultOnHeapAnalyzedListener.create(),

/**
* Extracts metadata from a hprof to be reported in [HeapAnalysisSuccess.metadata].
* Called on a background thread during heap analysis.
*
* Defaults to [AndroidMetadataExtractor]
*/
val metatadaExtractor: MetadataExtractor = AndroidMetadataExtractor,

/**
* Whether to compute the retained heap size, which is the total number of bytes in memory that
* would be reclaimed if the detected leaks didn't happen. This includes native memory
Expand Down
Expand Up @@ -60,10 +60,14 @@ internal class HeapAnalyzerService : ForegroundService(

val heapAnalysis =
heapAnalyzer.analyze(
heapDumpFile, config.referenceMatchers, config.computeRetainedHeapSize, config.objectInspectors,
heapDumpFile,
config.referenceMatchers,
config.computeRetainedHeapSize,
config.objectInspectors,
if (config.useExperimentalLeakFinders) config.objectInspectors else listOf(
ObjectInspectors.KEYED_WEAK_REFERENCE
)
),
config.metatadaExtractor
)

config.onHeapAnalyzedListener.onHeapAnalyzed(heapAnalysis)
Expand Down
Expand Up @@ -14,6 +14,7 @@ import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Handler
import android.os.HandlerThread
import com.squareup.leakcanary.core.BuildConfig
import com.squareup.leakcanary.core.R
import leakcanary.GcTrigger
import leakcanary.LeakCanary
Expand All @@ -34,6 +35,13 @@ internal object InternalLeakCanary : (Application) -> Unit, OnObjectRetainedList
private lateinit var heapDumpTrigger: HeapDumpTrigger

lateinit var application: Application

// BuildConfig.LIBRARY_VERSION is stripped so this static var is how we keep it around to find
// it later when parsing the heap dump.
@Suppress("unused")
@JvmStatic
private var version = BuildConfig.LIBRARY_VERSION

@Volatile
var applicationVisible = false
private set
Expand Down
Expand Up @@ -25,7 +25,6 @@ internal class LeaksDbHelper(context: Context) : SQLiteOpenHelper(
}

companion object {
// Last updated for next after 2.0-alpha-3
private const val VERSION = 16
private const val VERSION = 17
}
}
Expand Up @@ -123,6 +123,8 @@ internal class HeapAnalysisSuccessScreen(
titleText to timeText
})

rowList.addAll(heapAnalysis.metadata.map { it.key to it.value })

listView.adapter =
SimpleListAdapter(R.layout.leak_canary_leak_row, rowList) { view, position ->
val titleView = view.findViewById<TextView>(R.id.leak_canary_row_text)
Expand Down
36 changes: 36 additions & 0 deletions shark-android/src/main/java/shark/AndroidMetadataExtractor.kt
@@ -0,0 +1,36 @@
package shark

object AndroidMetadataExtractor : MetadataExtractor {
override fun extractMetadata(graph: HeapGraph): Map<String, String> {
val build = AndroidBuildMirror.fromHeapGraph(graph)

val leakCanaryVersion = readLeakCanaryVersion(graph)
val processName = readProcessName(graph)

return mapOf(
"Build.VERSION.SDK_INT" to build.sdkInt.toString(),
"Build.MANUFACTURER" to build.manufacturer,
"LeakCanary version" to leakCanaryVersion,
"App process name" to processName
)
}

private fun readLeakCanaryVersion(graph: HeapGraph): String {
val versionHolderClass = graph.findClassByName("leakcanary.internal.InternalLeakCanary")
return versionHolderClass?.get("version")?.value?.readAsJavaString() ?: "Unknown"
}

private fun readProcessName(graph: HeapGraph): String {
val activityThread = graph.findClassByName("android.app.ActivityThread")
?.get("sCurrentActivityThread")
?.valueAsInstance
val appBindData = activityThread?.get("android.app.ActivityThread", "mBoundApplication")
?.valueAsInstance
val appInfo = appBindData?.get("android.app.ActivityThread\$AppBindData", "appInfo")
?.valueAsInstance

return appInfo?.get(
"android.content.pm.ApplicationInfo", "processName"
)?.valueAsInstance?.readAsJavaString() ?: "Unknown"
}
}
14 changes: 13 additions & 1 deletion shark-android/src/test/java/shark/LegacyHprofTest.kt
Expand Up @@ -16,6 +16,14 @@ class LegacyHprofTest {
val leak2 = analysis.applicationLeaks[1]
assertThat(leak1.className).isEqualTo("android.graphics.Bitmap")
assertThat(leak2.className).isEqualTo("com.example.leakcanary.MainActivity")
assertThat(analysis.metadata).isEqualTo(
mapOf(
"App process name" to "com.example.leakcanary",
"Build.MANUFACTURER" to "Genymotion",
"Build.VERSION.SDK_INT" to "19",
"LeakCanary version" to "Unknown"
)
)
}

@Test fun androidM() {
Expand Down Expand Up @@ -119,7 +127,11 @@ class LegacyHprofTest {
private fun analyzeHprof(hprofFile: File): HeapAnalysisSuccess {
val heapAnalyzer = HeapAnalyzer(OnAnalysisProgressListener.NO_OP)
val analysis = heapAnalyzer.analyze(
hprofFile, AndroidReferenceMatchers.appDefaults, false, AndroidObjectInspectors.appDefaults
heapDumpFile = hprofFile,
referenceMatchers = AndroidReferenceMatchers.appDefaults,
computeRetainedHeapSize = false,
objectInspectors = AndroidObjectInspectors.appDefaults,
metadataExtractor = AndroidMetadataExtractor
)
println(analysis)
return analysis as HeapAnalysisSuccess
Expand Down
1 change: 1 addition & 0 deletions shark/src/main/java/shark/HeapAnalysis.kt
Expand Up @@ -46,6 +46,7 @@ data class HeapAnalysisSuccess(
override val heapDumpFile: File,
override val createdAtTimeMillis: Long,
override val analysisDurationMillis: Long,
val metadata: Map<String, String>,
/**
* The list of [ApplicationLeak] found in the heap dump by [HeapAnalyzer].
*/
Expand Down
7 changes: 6 additions & 1 deletion shark/src/main/java/shark/HeapAnalyzer.kt
Expand Up @@ -41,6 +41,7 @@ import shark.LeakTraceElement.Holder.THREAD
import shark.OnAnalysisProgressListener.Step.BUILDING_LEAK_TRACES
import shark.OnAnalysisProgressListener.Step.COMPUTING_NATIVE_RETAINED_SIZE
import shark.OnAnalysisProgressListener.Step.COMPUTING_RETAINED_SIZE
import shark.OnAnalysisProgressListener.Step.EXTRACTING_METADATA
import shark.OnAnalysisProgressListener.Step.FINDING_LEAKING_INSTANCES
import shark.OnAnalysisProgressListener.Step.PARSING_HEAP_DUMP
import shark.OnAnalysisProgressListener.Step.REPORTING_HEAP_ANALYSIS
Expand Down Expand Up @@ -82,6 +83,7 @@ class HeapAnalyzer constructor(
computeRetainedHeapSize: Boolean = false,
objectInspectors: List<ObjectInspector> = emptyList(),
leakFinders: List<ObjectInspector> = objectInspectors,
metadataExtractor: MetadataExtractor = MetadataExtractor.NO_OP,
proguardMapping: ProguardMapping? = null
): HeapAnalysis {
val analysisStartNanoTime = System.nanoTime()
Expand All @@ -101,13 +103,16 @@ class HeapAnalyzer constructor(
.use { hprof ->
val graph = HprofHeapGraph.indexHprof(hprof, proguardMapping)

listener.onAnalysisProgress(EXTRACTING_METADATA)
val metadata = metadataExtractor.extractMetadata(graph)

val findLeakInput = FindLeakInput(
graph, leakFinders, referenceMatchers, computeRetainedHeapSize, objectInspectors
)
val (applicationLeaks, libraryLeaks) = findLeakInput.findLeaks()
listener.onAnalysisProgress(REPORTING_HEAP_ANALYSIS)
return HeapAnalysisSuccess(
heapDumpFile, System.currentTimeMillis(), since(analysisStartNanoTime),
heapDumpFile, System.currentTimeMillis(), since(analysisStartNanoTime), metadata,
applicationLeaks, libraryLeaks
)
}
Expand Down
39 changes: 39 additions & 0 deletions shark/src/main/java/shark/MetadataExtractor.kt
@@ -0,0 +1,39 @@
package shark

import shark.MetadataExtractor.Companion.invoke
import shark.ObjectInspector.Companion.invoke

/**
* Extracts metadata from a hprof to be reported in [HeapAnalysisSuccess.metadata].
*
* You can create a [MetadataExtractor] from a lambda by calling [invoke].
*/
interface MetadataExtractor {
fun extractMetadata(graph: HeapGraph): Map<String, String>

companion object {

/**
* A no-op [MetadataExtractor]
*/
val NO_OP = MetadataExtractor { emptyMap() }

/**
* Utility function to create a [MetadataExtractor] from the passed in [block] lambda instead of
* using the anonymous `object : MetadataExtractor` syntax.
*
* Usage:
*
* ```kotlin
* val inspector = MetadataExtractor { graph ->
*
* }
* ```
*/
inline operator fun invoke(crossinline block: (HeapGraph) -> Map<String, String>): MetadataExtractor =
object : MetadataExtractor {
override fun extractMetadata(graph: HeapGraph): Map<String, String> = block(graph)
}
}

}
6 changes: 2 additions & 4 deletions shark/src/main/java/shark/OnAnalysisProgressListener.kt
Expand Up @@ -8,6 +8,7 @@ interface OnAnalysisProgressListener {
// These steps are defined in the order in which they occur.
enum class Step {
PARSING_HEAP_DUMP,
EXTRACTING_METADATA,
FINDING_LEAKING_INSTANCES,
FINDING_PATHS_TO_LEAKING_OBJECTS,
FINDING_DOMINATORS,
Expand All @@ -24,10 +25,7 @@ interface OnAnalysisProgressListener {
/**
* A no-op [OnAnalysisProgressListener]
*/
val NO_OP = object : OnAnalysisProgressListener {
override fun onAnalysisProgress(step: Step) {
}
}
val NO_OP = OnAnalysisProgressListener {}

/**
* Utility function to create a [OnAnalysisProgressListener] from the passed in [block] lambda
Expand Down
46 changes: 46 additions & 0 deletions shark/src/test/java/shark/MetadataExtractorTest.kt
@@ -0,0 +1,46 @@
package shark

import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File

class MetadataExtractorTest {

@get:Rule
var testFolder = TemporaryFolder()
private lateinit var hprofFile: File

@Before
fun setUp() {
hprofFile = testFolder.newFile("temp.hprof")
}

@Test fun extractStaticStringField() {
HprofWriter.open(hprofFile)
.helper {
val helloString = string("Hello")
clazz(
"World", staticFields = listOf(
"message" to helloString
)
)
}

val extractor = object : MetadataExtractor {
override fun extractMetadata(graph: HeapGraph): Map<String, String> {
val message =
graph.findClassByName("World")!!["message"]!!.valueAsInstance!!.readAsJavaString()!!
return mapOf("World message" to message)
}
}

val analysis = hprofFile.checkForLeaks<HeapAnalysisSuccess>(metadataExtractor = extractor)

val metadata = analysis.metadata

assertThat(metadata).isEqualTo(mapOf("World message" to "Hello"))
}
}
10 changes: 6 additions & 4 deletions shark/src/test/java/shark/TestUtil.kt
Expand Up @@ -12,6 +12,7 @@ fun <T : HeapAnalysis> File.checkForLeaks(
objectInspectors: List<ObjectInspector> = emptyList(),
computeRetainedHeapSize: Boolean = false,
referenceMatchers: List<ReferenceMatcher> = defaultReferenceMatchers,
metadataExtractor: MetadataExtractor = MetadataExtractor.NO_OP,
proguardMapping: ProguardMapping? = null
): T {
val inspectors = if (ObjectInspectors.KEYED_WEAK_REFERENCE !in objectInspectors) {
Expand All @@ -21,10 +22,11 @@ fun <T : HeapAnalysis> File.checkForLeaks(
}
val heapAnalyzer = HeapAnalyzer(OnAnalysisProgressListener.NO_OP)
val result = heapAnalyzer.analyze(
this,
referenceMatchers,
computeRetainedHeapSize,
inspectors,
heapDumpFile = this,
referenceMatchers = referenceMatchers,
computeRetainedHeapSize = computeRetainedHeapSize,
objectInspectors = inspectors,
metadataExtractor = metadataExtractor,
proguardMapping = proguardMapping
)
if (result is HeapAnalysisFailure) {
Expand Down

0 comments on commit 0c40611

Please sign in to comment.