Created
March 27, 2024 20:57
-
-
Save pyricau/e51e75a334882fc8e52e66c72c41ba47 to your computer and use it in GitHub Desktop.
A standalone Kotlin script that shows how to do custom heap analysis work with Shark, the heap parsing library that powers LeakCanary (see https://square.github.io/leakcanary/shark)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env kotlin | |
// Before running this script, install Kotlin with `brew install kotlin` | |
// Then run this with `kotlin shark-custom-script.main.kts` | |
// Edit this script in the latest Android Studio or IntelliJ IDEA releases to get dependency import and auto completion. | |
@file:Repository("https://repo.maven.apache.org/maven2/") | |
@file:DependsOn("com.squareup.leakcanary:shark-android:2.13") | |
import java.io.File | |
import shark.AndroidMetadataExtractor | |
import shark.AndroidObjectInspectors | |
import shark.AndroidReferenceMatchers | |
import shark.FilteringLeakingObjectFinder | |
import shark.HeapAnalyzer | |
import shark.HeapObject.HeapInstance | |
import shark.HprofHeapGraph.Companion.openHeapGraph | |
val heapDumpFile: File = TODO("Replace this with hprof file") // = File("PATH_TO_HPROF") | |
val heapAnalyzer = HeapAnalyzer(listener = { step -> | |
println("Analysis in progress, working on: ${step.name}") | |
}) | |
heapDumpFile.openHeapGraph( | |
proguardMapping = null | |
).use { graph -> | |
println("Looking for known leaks $heapDumpFile") | |
val heapAnalysis = heapAnalyzer.analyze( | |
heapDumpFile, | |
graph = graph, | |
leakingObjectFinder = FilteringLeakingObjectFinder( | |
AndroidObjectInspectors.appLeakingObjectFilters | |
), | |
referenceMatchers = AndroidReferenceMatchers.appDefaults | |
computeRetainedHeapSize = true, | |
objectInspectors = AndroidObjectInspectors.appDefaults, | |
metadataExtractor = AndroidMetadataExtractor, | |
) | |
println(heapAnalysis) | |
println("Let's do some custom heap analysis just for fun.") | |
println("#####") | |
println("String instance count: ${graph.findClassByName("java.lang.String")!!.instances.count()}") | |
println("#####") | |
val dexPaths = | |
graph.findClassByName("dalvik.system.BaseDexClassLoader")!!.instances.flatMap { classLoader -> | |
val pathList = classLoader["dalvik.system.BaseDexClassLoader", "pathList"]!! | |
.valueAsInstance!! | |
pathList["dalvik.system.DexPathList", "dexElements"]!! | |
.valueAsObjectArray!!.readElements() | |
.mapNotNull { | |
it.asObject?.asInstance | |
}.map { dexElement -> | |
val pathFile = dexElement["dalvik.system.DexPathList\$Element", "path"]!! | |
.valueAsInstance!! | |
pathFile["java.io.File", "path"]!!.valueAsInstance!!.readAsJavaString() | |
} | |
} | |
println("All dex paths:\n${dexPaths.joinToString("\n")}") | |
println("#####") | |
val generatedComponentClasses = graph.classes.filter { heapClass -> | |
val classSimpleName = heapClass.simpleName | |
classSimpleName.startsWith("Dagger") && | |
classSimpleName.endsWith("Impl") | |
} | |
val componentInstances = generatedComponentClasses.flatMap { it.instances } | |
val allSingletons = componentInstances.flatMap { it.componentSingletons } | |
println( | |
"Found ${componentInstances.count()} Dagger component instances " + | |
"and ${allSingletons.count()} Dagger singleton instances in heap." | |
) | |
} | |
private val HeapInstance.componentSingletons: Sequence<Long> | |
get() = readFields().mapNotNull { componentField -> | |
val componentFieldInstance = componentField.valueAsInstance ?: return@mapNotNull null | |
val provider = if (componentFieldInstance instanceOf "dagger.internal.DelegateFactory") { | |
componentFieldInstance["dagger.internal.DelegateFactory", "delegate"]?.valueAsInstance | |
} else { | |
componentFieldInstance | |
} | |
if (provider == null || !(provider instanceOf "dagger.internal.DoubleCheck")) { | |
return@mapNotNull null | |
} | |
val singletonObjectId = | |
provider["dagger.internal.DoubleCheck", "instance"]!!.value.asNonNullObjectId!! | |
val doubleCheckUninitializedObjectId = graph.context.getOrPut("DoubleCheck.UNINITIALIZED") { | |
checkNotNull(graph.findClassByName("dagger.internal.DoubleCheck")) { | |
"The Dagger DoubleCheck class should always be in the classpath." | |
}["UNINITIALIZED"]!!.valueAsInstance!!.objectId | |
} | |
if (singletonObjectId != doubleCheckUninitializedObjectId) { | |
singletonObjectId | |
} else { | |
null | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Line 36 is missing a comma. See https://gist.github.com/jush/87ac548fe2c3379938b57cc3e961b72c/7dcbe96505d6dc4cf527869e3ee16149f2371139#file-shark-custom-script-main-kts-L36
If you're interested I also added Xmx java option https://gist.github.com/jush/87ac548fe2c3379938b57cc3e961b72c/b1299d2db33652f8de091e0dcde84bb078e7fced#file-shark-custom-script-main-kts-L1 which is useful when parsing big heap files