<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