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

<lambda>null1 package shark
2 
3 import java.io.File
4 import org.assertj.core.api.Assertions.assertThat
5 import org.junit.Test
6 import shark.HeapObject.HeapInstance
7 import shark.HprofHeapGraph.Companion.openHeapGraph
8 import shark.HprofRecordTag.LOAD_CLASS
9 import shark.HprofRecordTag.ROOT_STICKY_CLASS
10 import shark.HprofRecordTag.STRING_IN_UTF8
11 import shark.LeakTrace.GcRootType
12 import shark.LegacyHprofTest.WRAPS_ACTIVITY.DESTROYED
13 import shark.LegacyHprofTest.WRAPS_ACTIVITY.NOT_ACTIVITY
14 import shark.LegacyHprofTest.WRAPS_ACTIVITY.NOT_DESTROYED
15 import shark.SharkLog.Logger
16 
17 class LegacyHprofTest {
18 
19   @Test fun preM() {
20     val analysis = analyzeHprof("leak_asynctask_pre_m.hprof")
21     assertThat(analysis.applicationLeaks).hasSize(2)
22     val leak1 = analysis.applicationLeaks[0].leakTraces.first()
23     val leak2 = analysis.applicationLeaks[1].leakTraces.first()
24     assertThat(leak1.leakingObject.className).isEqualTo("android.graphics.Bitmap")
25     assertThat(leak2.leakingObject.className).isEqualTo("com.example.leakcanary.MainActivity")
26     assertThat(analysis.metadata).containsAllEntriesOf(
27       mapOf(
28         "App process name" to "com.example.leakcanary",
29         "Build.MANUFACTURER" to "Genymotion",
30         "Build.VERSION.SDK_INT" to "19",
31         "LeakCanary version" to "Unknown"
32       )
33     )
34     assertThat(analysis.allLeaks.sumBy { it.totalRetainedHeapByteSize!! }).isEqualTo(193431)
35   }
36 
37   @Test fun androidM() {
38     val analysis = analyzeHprof("leak_asynctask_m.hprof")
39 
40     assertThat(analysis.applicationLeaks).hasSize(1)
41     val leak = analysis.applicationLeaks[0].leakTraces.first()
42     assertThat(leak.leakingObject.className).isEqualTo("com.example.leakcanary.MainActivity")
43     assertThat(leak.gcRootType).isEqualTo(GcRootType.STICKY_CLASS)
44     assertThat(analysis.allLeaks.sumBy { it.totalRetainedHeapByteSize!! }).isEqualTo(49584)
45   }
46 
47   @Test fun gcRootReferencesUnknownObject() {
48     val analysis = analyzeHprof("gcroot_unknown_object.hprof")
49 
50     assertThat(analysis.applicationLeaks).hasSize(2)
51     assertThat(analysis.allLeaks.sumBy { it.totalRetainedHeapByteSize!! }).isEqualTo(5306218)
52   }
53 
54   @Test fun androidMStripped() {
55     val stripper = HprofPrimitiveArrayStripper()
56     val sourceHprof = "leak_asynctask_m.hprof".classpathFile()
57     val strippedHprof = stripper.stripPrimitiveArrays(sourceHprof)
58 
59     assertThat(readThreadNames(sourceHprof)).contains("AsyncTask #1")
60     assertThat(readThreadNames(strippedHprof)).allMatch { threadName ->
61       threadName.all { character -> character == '?' }
62     }
63   }
64 
65   private fun readThreadNames(hprofFile: File): List<String> {
66     return hprofFile.openHeapGraph().use { graph ->
67       graph.findClassByName("java.lang.Thread")!!.instances.map { instance ->
68         instance["java.lang.Thread", "name"]!!.value.readAsJavaString()!!
69       }
70         .toList()
71     }
72   }
73 
74   @Test fun androidO() {
75     val analysis = analyzeHprof("leak_asynctask_o.hprof")
76 
77     assertThat(analysis.applicationLeaks).hasSize(1)
78     val leak = analysis.applicationLeaks[0].leakTraces.first()
79     assertThat(leak.leakingObject.className).isEqualTo("com.example.leakcanary.MainActivity")
80     assertThat(analysis.allLeaks.sumBy { it.totalRetainedHeapByteSize!! }).isEqualTo(211038)
81   }
82 
83   private enum class WRAPS_ACTIVITY {
84     DESTROYED,
85     NOT_DESTROYED,
86     NOT_ACTIVITY
87   }
88 
89   @Test fun `AndroidObjectInspectors#CONTEXT_FIELD labels Context fields`() {
90     val toastLabels = "leak_asynctask_o.hprof".classpathFile().openHeapGraph().use { graph ->
91       graph.instances.filter { it.instanceClassName == "android.widget.Toast" }
92         .map { instance ->
93           ObjectReporter(instance).apply {
94             AndroidObjectInspectors.CONTEXT_FIELD.inspect(this)
95           }.labels.joinToString(",")
96         }.toList()
97     }
98     assertThat(toastLabels).containsExactly(
99       "mContext instance of com.example.leakcanary.ExampleApplication"
100     )
101   }
102 
103   @Test fun androidOCountActivityWrappingContexts() {
104     val contextWrapperStatuses = Hprof.open("leak_asynctask_o.hprof".classpathFile())
105       .use { hprof ->
106         val graph = HprofHeapGraph.indexHprof(hprof)
107         graph.instances.filter {
108           it instanceOf "android.content.ContextWrapper"
109             && !(it instanceOf "android.app.Activity")
110             && !(it instanceOf "android.app.Application")
111             && !(it instanceOf "android.app.Service")
112         }
113           .map { instance ->
114             val reporter = ObjectReporter(instance)
115             AndroidObjectInspectors.CONTEXT_WRAPPER.inspect(reporter)
116             if (reporter.leakingReasons.size == 1) {
117               DESTROYED
118             } else if (reporter.labels.size == 1) {
119               if ("Activity.mDestroyed false" in reporter.labels.first()) {
120                 NOT_DESTROYED
121               } else {
122                 NOT_ACTIVITY
123               }
124             } else throw IllegalStateException(
125               "Unexpected, should have 1 leaking status ${reporter.leakingReasons} or one label ${reporter.labels}"
126             )
127           }
128           .toList()
129       }
130     assertThat(contextWrapperStatuses.filter { it == DESTROYED }).hasSize(12)
131     assertThat(contextWrapperStatuses.filter { it == NOT_DESTROYED }).hasSize(6)
132     assertThat(contextWrapperStatuses.filter { it == NOT_ACTIVITY }).hasSize(0)
133   }
134 
135   @Test fun gcRootInNonPrimaryHeap() {
136     val analysis = analyzeHprof("gc_root_in_non_primary_heap.hprof")
137 
138     assertThat(analysis.applicationLeaks).hasSize(1)
139     val leak = analysis.applicationLeaks[0].leakTraces.first()
140     assertThat(leak.leakingObject.className).isEqualTo("com.example.leakcanary.MainActivity")
141   }
142 
143   @Test fun `MessageQueue shows list of messages as array`() {
144     val heapAnalyzer = HeapAnalyzer(OnAnalysisProgressListener.NO_OP)
145     val analysis = heapAnalyzer.analyze(
146       heapDumpFile = "gc_root_in_non_primary_heap.hprof".classpathFile(),
147       leakingObjectFinder = FilteringLeakingObjectFinder(
148         listOf(FilteringLeakingObjectFinder.LeakingObjectFilter { heapObject ->
149           heapObject is HeapInstance &&
150             heapObject instanceOf "android.os.Message" &&
151             heapObject["android.os.Message", "target"]?.valueAsInstance?.instanceClassName == "android.app.ActivityThread\$H" &&
152             heapObject["android.os.Message", "what"]!!.value.asInt!! == 132 // ENABLE_JIT
153         })
154       ),
155       referenceMatchers = AndroidReferenceMatchers.appDefaults,
156       computeRetainedHeapSize = true,
157       objectInspectors = AndroidObjectInspectors.appDefaults,
158       metadataExtractor = AndroidMetadataExtractor
159     )
160     println(analysis)
161     analysis as HeapAnalysisSuccess
162     assertThat(analysis.applicationLeaks).hasSize(1)
163     val leak = analysis.applicationLeaks[0].leakTraces.first()
164     val firstReference = leak.referencePath.first()
165     assertThat(firstReference.originObject.className).isEqualTo("android.os.MessageQueue")
166     assertThat(firstReference.referenceDisplayName).isEqualTo("[0]")
167   }
168 
169   @Test fun `duplicated unloaded classes are ignored`() {
170     val expectedDuplicatedClassNames = setOf(
171       "leakcanary.internal.DebuggerControl",
172       "shark.AndroidResourceIdNames\$Companion",
173       "shark.GraphContext",
174       "shark.AndroidResourceIdNames\$Companion\$readFromHeap$1",
175       "leakcanary.internal.HeapDumpTrigger\$saveResourceIdNamesToMemory$1",
176       "leakcanary.internal.HeapDumpTrigger\$saveResourceIdNamesToMemory$2",
177       "shark.AndroidResourceIdNames",
178       "leakcanary.internal.FutureResult",
179       "leakcanary.internal.AndroidHeapDumper\$showToast$1",
180       "android.widget.Toast\$TN",
181       "android.widget.Toast\$TN$1",
182       "android.widget.Toast\$TN$2",
183       "leakcanary.internal.AndroidHeapDumper\$showToast$1$1",
184       "com.squareup.leakcanary.core.R\$dimen",
185       "com.squareup.leakcanary.core.R\$layout",
186       "android.text.style.WrapTogetherSpan[]"
187     )
188 
189     val file = "unloaded_classes-stripped.hprof".classpathFile()
190 
191     val header = HprofHeader.parseHeaderOf(file)
192 
193     val stickyClasses = mutableListOf<Long>()
194     val classesAndNameStringId = mutableMapOf<Long, Long>()
195     val stringRecordById = mutableMapOf<Long, String>()
196     StreamingHprofReader.readerFor(file, header).readRecords(setOf(ROOT_STICKY_CLASS, STRING_IN_UTF8, LOAD_CLASS)) { tag, length, reader ->
197       when(tag) {
198         ROOT_STICKY_CLASS -> reader.readStickyClassGcRootRecord().apply {
199           stickyClasses += id
200         }
201         STRING_IN_UTF8 -> reader.readStringRecord(length).apply {
202           stringRecordById[id] = string
203         }
204         LOAD_CLASS -> reader.readLoadClassRecord().apply {
205           classesAndNameStringId[id] = classNameStringId
206         }
207       }
208     }
209     val duplicatedClassObjectIdsByNameStringId =
210       classesAndNameStringId.entries
211         .groupBy { (_, className) -> className }
212         .mapValues { (_, value) -> value.map { (key, _) -> key } }
213         .filter { (_, values) -> values.size > 1 }
214 
215     val actualDuplicatedClassNames = duplicatedClassObjectIdsByNameStringId.keys
216       .map { stringRecordById.getValue(it) }
217       .toSet()
218     assertThat(actualDuplicatedClassNames).isEqualTo(expectedDuplicatedClassNames)
219 
220     val duplicateRootClassObjectIdByClassName = duplicatedClassObjectIdsByNameStringId
221       .mapKeys { (key, _) -> stringRecordById.getValue(key) }
222       .mapValues { (_, value) -> value.single { it in stickyClasses } }
223 
224     file.openHeapGraph().use { graph ->
225       val expectedDuplicatedRootClassObjectIds =
226         duplicateRootClassObjectIdByClassName.values.toSortedSet()
227 
228       val actualDuplicatedRootClassObjectIds = duplicateRootClassObjectIdByClassName.keys
229         .map { className ->
230           graph.findClassByName(className)!!.objectId
231         }
232         .toSortedSet()
233 
234       assertThat(actualDuplicatedRootClassObjectIds).isEqualTo(
235         expectedDuplicatedRootClassObjectIds
236       )
237     }
238   }
239 
240   private fun analyzeHprof(fileName: String): HeapAnalysisSuccess {
241     return analyzeHprof(fileName.classpathFile())
242   }
243 
244   private fun analyzeHprof(hprofFile: File): HeapAnalysisSuccess {
245     SharkLog.logger = object : Logger {
246       override fun d(message: String) {
247         println(message)
248       }
249 
250       override fun d(
251         throwable: Throwable,
252         message: String
253       ) {
254         println(message)
255         throwable.printStackTrace()
256       }
257     }
258     val heapAnalyzer = HeapAnalyzer(OnAnalysisProgressListener.NO_OP)
259     val analysis = heapAnalyzer.analyze(
260       heapDumpFile = hprofFile,
261       leakingObjectFinder = FilteringLeakingObjectFinder(
262         AndroidObjectInspectors.appLeakingObjectFilters
263       ),
264       referenceMatchers = AndroidReferenceMatchers.appDefaults,
265       computeRetainedHeapSize = true,
266       objectInspectors = AndroidObjectInspectors.appDefaults,
267       metadataExtractor = AndroidMetadataExtractor
268     )
269     println(analysis)
270     return analysis as HeapAnalysisSuccess
271   }
272 }
273