<lambda>null1 package shark
2
3 import shark.GcRoot.ThreadObject
4 import shark.HeapObject.HeapClass
5 import shark.HeapObject.HeapInstance
6 import shark.HeapObject.HeapObjectArray
7 import shark.HeapObject.HeapPrimitiveArray
8 import shark.internal.friendly.mapNativeSizes
9
10 object AndroidMetadataExtractor : MetadataExtractor {
11 override fun extractMetadata(graph: HeapGraph): Map<String, String> {
12 val metadata = mutableMapOf<String, String>()
13
14 val build = AndroidBuildMirror.fromHeapGraph(graph)
15 metadata["Build.VERSION.SDK_INT"] = build.sdkInt.toString()
16 metadata["Build.MANUFACTURER"] = build.manufacturer
17 metadata["LeakCanary version"] = readLeakCanaryVersion(graph)
18 metadata["App process name"] = readProcessName(graph)
19 metadata["Class count"] = graph.classCount.toString()
20 metadata["Instance count"] = graph.instanceCount.toString()
21 metadata["Primitive array count"] = graph.primitiveArrayCount.toString()
22 metadata["Object array count"] = graph.objectArrayCount.toString()
23 metadata["Thread count"] = readThreadCount(graph).toString()
24 metadata["Heap total bytes"] = readHeapTotalBytes(graph).toString()
25 metadata.putBitmaps(graph)
26 metadata.putDbLabels(graph)
27
28 return metadata
29 }
30
31 private fun readHeapTotalBytes(graph: HeapGraph): Int {
32 return graph.objects.sumBy { heapObject ->
33 when(heapObject) {
34 is HeapInstance -> {
35 heapObject.byteSize
36 }
37 // This is probably way off but is a cheap approximation.
38 is HeapClass -> heapObject.recordSize
39 is HeapObjectArray -> heapObject.byteSize
40 is HeapPrimitiveArray -> heapObject.byteSize
41 }
42 }
43 }
44
45 private fun MutableMap<String, String>.putBitmaps(
46 graph: HeapGraph,
47 ) {
48
49 val bitmapClass = graph.findClassByName("android.graphics.Bitmap") ?: return
50
51 val maxDisplayPixels =
52 graph.findClassByName("android.util.DisplayMetrics")?.directInstances?.map { instance ->
53 val width = instance["android.util.DisplayMetrics", "widthPixels"]?.value?.asInt ?: 0
54 val height = instance["android.util.DisplayMetrics", "heightPixels"]?.value?.asInt ?: 0
55 width * height
56 }?.max() ?: 0
57
58 val maxDisplayPixelsWithThreshold = (maxDisplayPixels * 1.1).toInt()
59
60 val sizeMap = graph.mapNativeSizes()
61
62 var sizeSum = 0
63 var count = 0
64 var largeBitmapCount = 0
65 var largeBitmapSizeSum = 0
66 bitmapClass.instances.forEach { bitmap ->
67 val width = bitmap["android.graphics.Bitmap", "mWidth"]?.value?.asInt ?: 0
68 val height = bitmap["android.graphics.Bitmap", "mHeight"]?.value?.asInt ?: 0
69 val size = sizeMap[bitmap.objectId] ?: 0
70
71 count++
72 sizeSum += size
73 if (maxDisplayPixelsWithThreshold > 0 && width * height > maxDisplayPixelsWithThreshold) {
74 largeBitmapCount++
75 largeBitmapSizeSum += size
76 }
77 }
78 this["Bitmap count"] = count.toString()
79 this["Bitmap total bytes"] = sizeSum.toString()
80 this["Large bitmap count"] = largeBitmapCount.toString()
81 this["Large bitmap total bytes"] = largeBitmapSizeSum.toString()
82 }
83
84 private fun readThreadCount(graph: HeapGraph): Int {
85 return graph.gcRoots.filterIsInstance<ThreadObject>().map { it.id }.toSet().size
86 }
87
88 private fun readLeakCanaryVersion(graph: HeapGraph): String {
89 val versionHolderClass = graph.findClassByName("leakcanary.internal.InternalLeakCanary")
90 return versionHolderClass?.get("version")?.value?.readAsJavaString() ?: "Unknown"
91 }
92
93 private fun readProcessName(graph: HeapGraph): String {
94 val activityThread = graph.findClassByName("android.app.ActivityThread")
95 ?.get("sCurrentActivityThread")
96 ?.valueAsInstance
97 val appBindData = activityThread?.get("android.app.ActivityThread", "mBoundApplication")
98 ?.valueAsInstance
99 val appInfo = appBindData?.get("android.app.ActivityThread\$AppBindData", "appInfo")
100 ?.valueAsInstance
101
102 return appInfo?.get(
103 "android.content.pm.ApplicationInfo", "processName"
104 )?.valueAsInstance?.readAsJavaString() ?: "Unknown"
105 }
106
107 private fun MutableMap<String, String>.putDbLabels(graph: HeapGraph) {
108 val dbClass = graph.findClassByName("android.database.sqlite.SQLiteDatabase") ?: return
109
110 val openDbLabels = dbClass.instances.mapNotNull { instance ->
111 val config =
112 instance["android.database.sqlite.SQLiteDatabase", "mConfigurationLocked"]?.valueAsInstance
113 ?: return@mapNotNull null
114 val label =
115 config["android.database.sqlite.SQLiteDatabaseConfiguration", "label"]?.value?.readAsJavaString()
116 ?: return@mapNotNull null
117 val open =
118 instance["android.database.sqlite.SQLiteDatabase", "mConnectionPoolLocked"]?.value?.isNonNullReference
119 ?: return@mapNotNull null
120 label to open
121 }
122
123 openDbLabels.forEachIndexed { index, (label, open) ->
124 this["Db ${index + 1}"] = (if (open) "open " else "closed ") + label
125 }
126 }
127 }
128