Shark 🦈¶
Shark: Smart Heap Analysis Reports for Kotlin
Shark is the heap analyzer that powers LeakCanary 2. It’s a Kotlin standalone heap analysis library that runs at high speed with a low memory footprint.
Shark is released in layers:
- Shark Hprof: Read and write records in hprof files.
- Shark Graph: Navigate the heap object graph.
- Shark: Generate heap analysis reports.
- Shark Android: Android heuristics to generate tailored heap analysis reports.
- Shark CLI: Analyze the heap of debuggable apps installed on an Android device connected to your desktop. The output is similar to the output of LeakCanary, except you don’t have to add the LeakCanary dependency to your app.
- LeakCanary: Builds on top. It automatically watches destroyed activities and fragments, triggers a heap dump, runs Shark Android and then displays the result.
A few more things:
- Shark is built on top of Okio. Okio makes it easy to parse heap dumps efficiently.
- Shark is a 100% Kotlin library, and Kotlin is essential to its design, because Shark relies heavily on sealed classes and sequences to save memory.
- Shark has the unique ability to help narrow down the cause of memory leaks through platform specific heuristics.
- Shark is heavily tested (80% test coverage).
- Shark can run in both Java and Android VMs, with no other dependency than Okio and Kotlin.
- Shark can analyze both Java and Android VM hprof files.
- Shark can deobfuscate hprof records if it has access to obfuscation mapping file.
Shark CLI¶
The Shark Command Line Interface (CLI) enables you to analyze heaps directly from your computer. It can dump the heap of an app installed on a connected Android device, analyze it, and even strip a heap dump of any sensitive data (e.g. PII, passwords or encryption keys) which is useful when sharing a heap dump.
Install it via Homebrew:
brew install leakcanary-shark
You can also download it here.
You can then look for leaks in apps on any connected device, for example:
$ shark-cli --device emulator-5554 --process com.example.app.debug analyze
Info
shark-cli
works with all debuggable apps, even if they don’t include the leakcanary-android
dependency.
Run shark-cli
to see usage instructions:
$ shark-cli
Usage: shark-cli [OPTIONS] COMMAND [ARGS]...
^`. .=""=.
^_ \ \ / _ _ \
\ \ { \ | d b |
{ \ / `~~~--__ \ /\ /
{ \___----~~' `~~-_/'-=\/=-'\,
\ /// a `~. \ \
/ /~~~~-, ,__. , /// __,,,,) \ |
\/ \/ `~~~; ,---~~-_`/ \ / \/
/ / '. .'
'._.' _|`~~`|_
/|\ /|\
Options:
-p, --process TEXT Full or partial name of a process, e.g.
"example" would match "com.example.app"
-d, --device ID device/emulator id
-m, --obfuscation-mapping PATH path to obfuscation mapping file
--verbose / --no-verbose provide additional details as to what
shark-cli is doing
-h, --hprof FILE path to a .hprof file
--help Show this message and exit
Commands:
interactive Explore a heap dump.
analyze Analyze a heap dump.
dump-process Dump the heap and pull the hprof file.
strip-hprof Replace all primitive arrays from the provided heap dump with
arrays of zeroes and generate a new "-stripped.hprof" file.
Shark code examples¶
Reading records in a hprof file¶
// build.gradle
dependencies {
implementation 'com.squareup.leakcanary:shark-hprof:$sharkVersion'
}
import java.io.File
import shark.Hprof
import shark.HprofRecord.StringRecord
import shark.OnHprofRecordListener
fun main(args: Array<String>) {
val heapDumpFile = File(args[0])
// Prints all class and field names
Hprof.open(heapDumpFile).use { hprof ->
hprof.reader.readHprofRecords(
recordTypes = setOf(StringRecord::class),
listener =
OnHprofRecordListener { position, record -> println((record as StringRecord).string) },
)
}
}
Navigating the heap object graph¶
// build.gradle
dependencies {
implementation 'com.squareup.leakcanary:shark-graph:$sharkVersion'
}
import java.io.File
import shark.Hprof
import shark.HprofHeapGraph
fun main(args: Array<String>) {
val heapDumpFile = File(args[0])
// Prints all thread names
Hprof.open(heapDumpFile).use { hprof ->
val heapGraph = HprofHeapGraph.indexHprof(hprof)
val threadClass = heapGraph.findClassByName("java.lang.Thread")!!
val threadNames: Sequence<String> =
threadClass.instances.map { instance ->
val nameField = instance["java.lang.Thread", "name"]!!
nameField.value.readAsJavaString()!!
}
threadNames.forEach { println(it) }
}
}
Generating a heap analysis report¶
// build.gradle
dependencies {
implementation 'com.squareup.leakcanary:shark:$sharkVersion'
}
import java.io.File
import shark.FilteringLeakingObjectFinder
import shark.FilteringLeakingObjectFinder.LeakingObjectFilter
import shark.HeapAnalyzer
import shark.HeapObject
import shark.HeapObject.HeapInstance
import shark.Hprof
import shark.HprofHeapGraph
// Marks any instance of com.example.ThingWithLifecycle with
// ThingWithLifecycle.destroyed=true as leaking
val leakingObjectFilter =
object : LeakingObjectFilter {
override fun isLeakingObject(heapObject: HeapObject): Boolean {
return if (
heapObject is HeapInstance && heapObject instanceOf "com.example.ThingWithLifecycle"
) {
val destroyedField = heapObject["com.example.ThingWithLifecycle", "destroyed"]!!
destroyedField.value.asBoolean!!
} else false
}
}
val leakingObjectFinder = FilteringLeakingObjectFinder(listOf(leakingObjectFilter))
fun main(args: Array<String>) {
val heapDumpFile = File(args[0])
val heapAnalysis =
Hprof.open(heapDumpFile).use { hprof ->
val heapGraph = HprofHeapGraph.indexHprof(hprof)
val heapAnalyzer = HeapAnalyzer({})
heapAnalyzer.analyze(
heapDumpFile = heapDumpFile,
graph = heapGraph,
leakingObjectFinder = leakingObjectFinder,
)
}
println(heapAnalysis)
}
Generating an Android heap analysis report¶
// build.gradle
dependencies {
implementation 'com.squareup.leakcanary:shark-android:$sharkVersion'
}
import java.io.File
import shark.AndroidObjectInspectors
import shark.AndroidReferenceMatchers
import shark.FilteringLeakingObjectFinder
import shark.FilteringLeakingObjectFinder.LeakingObjectFilter
import shark.HeapAnalyzer
import shark.HeapObject
import shark.HeapObject.HeapInstance
import shark.Hprof
import shark.HprofHeapGraph
// Marks any instance of com.example.ThingWithLifecycle with
// ThingWithLifecycle.destroyed=true as leaking
val leakingObjectFilter =
object : LeakingObjectFilter {
override fun isLeakingObject(heapObject: HeapObject): Boolean {
return if (
heapObject is HeapInstance && heapObject instanceOf "com.example.ThingWithLifecycle"
) {
val instance = heapObject as HeapInstance
val destroyedField = instance["com.example.ThingWithLifecycle", "destroyed"]!!
destroyedField.value.asBoolean!!
} else false
}
}
val leakingObjectFinder = FilteringLeakingObjectFinder(listOf(leakingObjectFilter))
fun main(args: Array<String>) {
val heapDumpFile = File(args[0])
val heapAnalysis =
Hprof.open(heapDumpFile).use { hprof ->
val heapGraph = HprofHeapGraph.indexHprof(hprof)
val heapAnalyzer = HeapAnalyzer({})
heapAnalyzer.analyze(
heapDumpFile = heapDumpFile,
graph = heapGraph,
leakingObjectFinder = leakingObjectFinder,
referenceMatchers = AndroidReferenceMatchers.appDefaults,
objectInspectors = AndroidObjectInspectors.appDefaults,
)
}
println(heapAnalysis)
}