main.shark.AndroidMetadataExtractor.kt Maven / Gradle / Ivy
package shark
import shark.GcRoot.ThreadObject
import shark.HeapObject.HeapClass
import shark.HeapObject.HeapInstance
import shark.HeapObject.HeapObjectArray
import shark.HeapObject.HeapPrimitiveArray
import shark.internal.friendly.mapNativeSizes
object AndroidMetadataExtractor : MetadataExtractor {
override fun extractMetadata(graph: HeapGraph): Map {
val metadata = mutableMapOf()
val build = AndroidBuildMirror.fromHeapGraph(graph)
metadata["Build.VERSION.SDK_INT"] = build.sdkInt.toString()
metadata["Build.MANUFACTURER"] = build.manufacturer
metadata["LeakCanary version"] = readLeakCanaryVersion(graph)
metadata["App process name"] = readProcessName(graph)
metadata["Class count"] = graph.classCount.toString()
metadata["Instance count"] = graph.instanceCount.toString()
metadata["Primitive array count"] = graph.primitiveArrayCount.toString()
metadata["Object array count"] = graph.objectArrayCount.toString()
metadata["Thread count"] = readThreadCount(graph).toString()
metadata["Heap total bytes"] = readHeapTotalBytes(graph).toString()
metadata.putBitmaps(graph)
metadata.putDbLabels(graph)
return metadata
}
private fun readHeapTotalBytes(graph: HeapGraph): Int {
return graph.objects.sumBy { heapObject ->
when(heapObject) {
is HeapInstance -> {
heapObject.byteSize
}
// This is probably way off but is a cheap approximation.
is HeapClass -> heapObject.recordSize
is HeapObjectArray -> heapObject.byteSize
is HeapPrimitiveArray -> heapObject.byteSize
}
}
}
private fun MutableMap.putBitmaps(
graph: HeapGraph,
) {
val bitmapClass = graph.findClassByName("android.graphics.Bitmap") ?: return
val maxDisplayPixels =
graph.findClassByName("android.util.DisplayMetrics")?.directInstances?.map { instance ->
val width = instance["android.util.DisplayMetrics", "widthPixels"]?.value?.asInt ?: 0
val height = instance["android.util.DisplayMetrics", "heightPixels"]?.value?.asInt ?: 0
width * height
}?.max() ?: 0
val maxDisplayPixelsWithThreshold = (maxDisplayPixels * 1.1).toInt()
val sizeMap = graph.mapNativeSizes()
var sizeSum = 0
var count = 0
var largeBitmapCount = 0
var largeBitmapSizeSum = 0
bitmapClass.instances.forEach { bitmap ->
val width = bitmap["android.graphics.Bitmap", "mWidth"]?.value?.asInt ?: 0
val height = bitmap["android.graphics.Bitmap", "mHeight"]?.value?.asInt ?: 0
val size = sizeMap[bitmap.objectId] ?: 0
count++
sizeSum += size
if (maxDisplayPixelsWithThreshold > 0 && width * height > maxDisplayPixelsWithThreshold) {
largeBitmapCount++
largeBitmapSizeSum += size
}
}
this["Bitmap count"] = count.toString()
this["Bitmap total bytes"] = sizeSum.toString()
this["Large bitmap count"] = largeBitmapCount.toString()
this["Large bitmap total bytes"] = largeBitmapSizeSum.toString()
}
private fun readThreadCount(graph: HeapGraph): Int {
return graph.gcRoots.filterIsInstance().map { it.id }.toSet().size
}
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"
}
private fun MutableMap.putDbLabels(graph: HeapGraph) {
val dbClass = graph.findClassByName("android.database.sqlite.SQLiteDatabase") ?: return
val openDbLabels = dbClass.instances.mapNotNull { instance ->
val config =
instance["android.database.sqlite.SQLiteDatabase", "mConfigurationLocked"]?.valueAsInstance
?: return@mapNotNull null
val label =
config["android.database.sqlite.SQLiteDatabaseConfiguration", "label"]?.value?.readAsJavaString()
?: return@mapNotNull null
val open =
instance["android.database.sqlite.SQLiteDatabase", "mConnectionPoolLocked"]?.value?.isNonNullReference
?: return@mapNotNull null
label to open
}
openDbLabels.forEachIndexed { index, (label, open) ->
this["Db ${index + 1}"] = (if (open) "open " else "closed ") + label
}
}
}