xref: /aosp_15_r20/external/leakcanary2/shark-android/src/main/java/shark/AndroidMetadataExtractor.kt (revision d9e8da70d8c9df9a41d7848ae506fb3115cae6e6)

<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