Skip to content

Commit

Permalink
Add metadata
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 0ecbf93
Show file tree
Hide file tree
Showing 13 changed files with 176 additions and 14 deletions.
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
}
}
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
Binary file not shown.
2 changes: 2 additions & 0 deletions shark/src/main/java/shark/HeapAnalysis.kt
Expand Up @@ -37,6 +37,7 @@ data class HeapAnalysisFailure(
* An exception wrapping the actual exception that was thrown.
*/
val exception: HeapAnalysisException

) : HeapAnalysis()

/**
Expand All @@ -46,6 +47,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 0ecbf93

Please sign in to comment.