xref: /aosp_15_r20/external/leakcanary2/shark/src/main/java/shark/internal/AndroidReferenceReaders.kt (revision d9e8da70d8c9df9a41d7848ae506fb3115cae6e6)

<lambda>null1 package shark.internal
2 
3 import shark.HeapGraph
4 import shark.HeapObject.HeapInstance
5 import shark.LibraryLeakReferenceMatcher
6 import shark.ReferencePattern.InstanceFieldPattern
7 import shark.ValueHolder
8 import shark.ValueHolder.ReferenceHolder
9 import shark.internal.ChainingInstanceReferenceReader.VirtualInstanceReferenceReader
10 import shark.internal.ChainingInstanceReferenceReader.VirtualInstanceReferenceReader.OptionalFactory
11 import shark.internal.Reference.LazyDetails
12 import shark.internal.ReferenceLocationType.ARRAY_ENTRY
13 import shark.internal.ReferenceLocationType.INSTANCE_FIELD
14 
15 internal enum class AndroidReferenceReaders : OptionalFactory {
16 
17   /**
18    * ActivityThread.mNewActivity is a linked list of ActivityClientRecord that keeps track of
19    * activities after they were resumed, until the main thread is idle. This is used to report
20    * analytics to system_server about how long it took for the main thread to settle after
21    * resuming an activity. Unfortunately, if the main thread never becomes idle, all these
22    * new activities leak in memory.
23    *
24    * We'd normally catch these with a pattern in AndroidReferenceMatchers, and we do have
25    * AndroidReferenceMatchers.ACTIVITY_THREAD__M_NEW_ACTIVITIES to do that, however this matching
26    * only works if none of the activities alive are waiting for idle. If any activity alive is
27    * still waiting for idle (which all the alive activities would be if they main thread is never
28    * idle) then ActivityThread.mActivities will reference an ActivityClientRecord through an
29    * ArrayMap and because ActivityClientRecord are reused that instance will also have its nextIdle
30    * fields set, so we're effectively traversing the ActivityThread.mNewActivity from a completely
31    * different and unexpected entry point.
32    *
33    * To fix that problem of broken pattern matching, we emit the mNewActivities field when
34    * finding an ActivityThread instance, and because custom ref readers have priority over the
35    * default instance field reader, we're guaranteed that mNewActivities is enqueued before
36    * mActivities. Unfortunately, that also means we can't rely on AndroidReferenceMatchers as
37    * those aren't used here, so we recreate our own LibraryLeakReferenceMatcher.
38    *
39    * We want to traverse mNewActivities before mActivities so we can't set isLowPriority to true
40    * like we would for normal path tagged as source of leak. So we will prioritize going through all
41    * activities in mNewActivities, some of which aren't destroyed yet (and therefore not leaking).
42    * Going through those paths of non leaking activities, we might find other leaks though.
43    * This would result in us tagging unrelated leaks as part of the mNewActivities leak. To prevent
44    * this, we traverse ActivityThread.mNewActivities as a linked list through
45    * ActivityClientRecord.nextIdle as a linked list, but we emit only ActivityClientRecord.activity
46    * fields if such activities are destroyed, which means any live activity in
47    * ActivityThread.mNewActivities will be discovered through the normal field navigation process
48    * and should go through ActivityThread.mActivities.
49    */
50   ACTIVITY_THREAD__NEW_ACTIVITIES {
51     override fun create(graph: HeapGraph): VirtualInstanceReferenceReader? {
52       val activityThreadClass = graph.findClassByName("android.app.ActivityThread") ?: return null
53 
54       if (activityThreadClass.readRecordFields().none {
55           activityThreadClass.instanceFieldName(it) == "mNewActivities"
56         }
57       ) {
58         return null
59       }
60 
61       val activityClientRecordClass =
62         graph.findClassByName("android.app.ActivityThread\$ActivityClientRecord") ?: return null
63 
64       val activityClientRecordFieldNames = activityClientRecordClass.readRecordFields()
65         .map { activityThreadClass.instanceFieldName(it) }
66         .toList()
67 
68       if ("nextIdle" !in activityClientRecordFieldNames ||
69         "activity" !in activityClientRecordFieldNames) {
70         return null
71       }
72 
73       val activityThreadClassId = activityThreadClass.objectId
74       val activityClientRecordClassId = activityClientRecordClass.objectId
75 
76       return object : VirtualInstanceReferenceReader {
77         override fun matches(instance: HeapInstance) =
78           instance.instanceClassId == activityThreadClassId ||
79             instance.instanceClassId == activityClientRecordClassId
80 
81         override fun read(source: HeapInstance): Sequence<Reference> {
82           return if (source.instanceClassId == activityThreadClassId) {
83             val mNewActivities =
84               source["android.app.ActivityThread", "mNewActivities"]!!.value.asObjectId!!
85             if (mNewActivities == ValueHolder.NULL_REFERENCE) {
86               emptySequence()
87             } else {
88               source.graph.context[ACTIVITY_THREAD__NEW_ACTIVITIES.name] = mNewActivities
89               sequenceOf(
90                 Reference(
91                   valueObjectId = mNewActivities,
92                   isLowPriority = false,
93                   lazyDetailsResolver = {
94                     LazyDetails(
95                       name = "mNewActivities",
96                       locationClassObjectId = activityThreadClassId,
97                       locationType = INSTANCE_FIELD,
98                       isVirtual = false,
99                       matchedLibraryLeak = LibraryLeakReferenceMatcher(
100                         pattern = InstanceFieldPattern(
101                           "android.app.ActivityThread", "mNewActivities"
102                         ),
103                         description = """
104                        New activities are leaked by ActivityThread until the main thread becomes idle.
105                        Tracked here: https://issuetracker.google.com/issues/258390457
106                      """.trimIndent()
107                       )
108                     )
109                   })
110               )
111             }
112           } else {
113             val mNewActivities =
114               source.graph.context.get<Long?>(ACTIVITY_THREAD__NEW_ACTIVITIES.name)
115             if (mNewActivities == null || source.objectId != mNewActivities) {
116               emptySequence()
117             } else {
118               generateSequence(source) { node ->
119                 node["android.app.ActivityThread\$ActivityClientRecord", "nextIdle"]!!.valueAsInstance
120               }.withIndex().mapNotNull { (index, node) ->
121 
122                 val activity = node["android.app.ActivityThread\$ActivityClientRecord", "activity"]!!.valueAsInstance
123                 if (activity == null ||
124                   // Skip non destroyed activities.
125                   // (!= true because we also skip if mDestroyed is missing)
126                   activity["android.app.Activity", "mDestroyed"]?.value?.asBoolean != true
127                 ) {
128                   null
129                 } else {
130                   Reference(
131                     valueObjectId = activity.objectId,
132                     isLowPriority = false,
133                     lazyDetailsResolver = {
134                       LazyDetails(
135                         name = "$index",
136                         locationClassObjectId = activityClientRecordClassId,
137                         locationType = ARRAY_ENTRY,
138                         isVirtual = true,
139                         matchedLibraryLeak = null
140                       )
141                     })
142                 }
143               }
144             }
145           }
146         }
147       }
148     }
149   },
150 
151 
152   MESSAGE_QUEUE {
153     override fun create(graph: HeapGraph): VirtualInstanceReferenceReader? {
154       val messageQueueClass =
155         graph.findClassByName("android.os.MessageQueue") ?: return null
156 
157       val messageQueueClassId = messageQueueClass.objectId
158 
159       return object : VirtualInstanceReferenceReader {
160         override fun matches(instance: HeapInstance) =
161           instance.instanceClassId == messageQueueClassId
162 
163         override fun read(source: HeapInstance): Sequence<Reference> {
164           val firstMessage = source["android.os.MessageQueue", "mMessages"]!!.valueAsInstance
165           return generateSequence(firstMessage) { node ->
166             node["android.os.Message", "next"]!!.valueAsInstance
167           }
168             .withIndex()
169             .map { (index, node) ->
170               Reference(
171                 valueObjectId = node.objectId,
172                 isLowPriority = false,
173                 lazyDetailsResolver = {
174                   LazyDetails(
175                     name = "$index",
176                     locationClassObjectId = messageQueueClassId,
177                     locationType = ARRAY_ENTRY,
178                     isVirtual = true,
179                     matchedLibraryLeak = null
180                   )
181                 }
182               )
183             }
184         }
185       }
186     }
187   },
188 
189   ANIMATOR_WEAK_REF_SUCKS {
190     override fun create(graph: HeapGraph): VirtualInstanceReferenceReader? {
191       val objectAnimatorClass =
192         graph.findClassByName("android.animation.ObjectAnimator") ?: return null
193 
194       val weakRefClassId =
195         graph.findClassByName("java.lang.ref.WeakReference")?.objectId ?: return null
196 
197       val objectAnimatorClassId = objectAnimatorClass.objectId
198 
199       return object : VirtualInstanceReferenceReader {
200         override fun matches(instance: HeapInstance) =
201           instance.instanceClassId == objectAnimatorClassId
202 
203         override fun read(source: HeapInstance): Sequence<Reference> {
204           val mTarget = source["android.animation.ObjectAnimator", "mTarget"]?.valueAsInstance
205             ?: return emptySequence()
206 
207           if (mTarget.instanceClassId != weakRefClassId) {
208             return emptySequence()
209           }
210 
211           val actualRef =
212             mTarget["java.lang.ref.Reference", "referent"]!!.value.holder as ReferenceHolder
213 
214           return if (actualRef.isNull) {
215             emptySequence()
216           } else {
217             sequenceOf(Reference(
218               valueObjectId = actualRef.value,
219               isLowPriority = true,
220               lazyDetailsResolver = {
221                 LazyDetails(
222                   name = "mTarget",
223                   locationClassObjectId = objectAnimatorClassId,
224                   locationType = INSTANCE_FIELD,
225                   matchedLibraryLeak = null,
226                   isVirtual = true
227                 )
228               }
229             ))
230           }
231         }
232       }
233     }
234   },
235 
236   SAFE_ITERABLE_MAP {
237     override fun create(graph: HeapGraph): VirtualInstanceReferenceReader? {
238       val mapClass =
239         graph.findClassByName(SAFE_ITERABLE_MAP_CLASS_NAME) ?: return null
240       // A subclass of SafeIterableMap with dual storage in a backing HashMap for fast get.
241       // Yes, that's a little weird.
242       val fastMapClass = graph.findClassByName(FAST_SAFE_ITERABLE_MAP_CLASS_NAME)
243 
244       val mapClassId = mapClass.objectId
245       val fastMapClassId = fastMapClass?.objectId
246 
247       return object : VirtualInstanceReferenceReader {
248         override fun matches(instance: HeapInstance) =
249           instance.instanceClassId.let { classId ->
250             classId == mapClassId || classId == fastMapClassId
251           }
252 
253         override fun read(source: HeapInstance): Sequence<Reference> {
254           val start = source[SAFE_ITERABLE_MAP_CLASS_NAME, "mStart"]!!.valueAsInstance
255 
256           val entries = generateSequence(start) { node ->
257             node[SAFE_ITERABLE_MAP_ENTRY_CLASS_NAME, "mNext"]!!.valueAsInstance
258           }
259 
260           val locationClassObjectId = source.instanceClassId
261           return entries.flatMap { entry ->
262             // mkey is never null
263             val key = entry[SAFE_ITERABLE_MAP_ENTRY_CLASS_NAME, "mKey"]!!.value
264 
265             val keyRef = Reference(
266               valueObjectId = key.asObjectId!!,
267               isLowPriority = false,
268               lazyDetailsResolver = {
269                 LazyDetails(
270                   name = "key()",
271                   locationClassObjectId = locationClassObjectId,
272                   locationType = ARRAY_ENTRY,
273                   isVirtual = true,
274                   matchedLibraryLeak = null
275                 )
276               }
277             )
278 
279             // mValue is never null
280             val value = entry[SAFE_ITERABLE_MAP_ENTRY_CLASS_NAME, "mValue"]!!.value
281 
282             val valueRef = Reference(
283               valueObjectId = value.asObjectId!!,
284               isLowPriority = false,
285               lazyDetailsResolver = {
286                 val keyAsString = key.asObject?.asInstance?.readAsJavaString()?.let { "\"$it\"" }
287                 val keyAsName =
288                   keyAsString ?: key.asObject?.toString() ?: "null"
289                 LazyDetails(
290                   name = keyAsName,
291                   locationClassObjectId = locationClassObjectId,
292                   locationType = ARRAY_ENTRY,
293                   isVirtual = true,
294                   matchedLibraryLeak = null
295                 )
296               }
297             )
298             sequenceOf(keyRef, valueRef)
299           }
300         }
301       }
302     }
303   },
304 
305   ARRAY_SET {
306     override fun create(graph: HeapGraph): VirtualInstanceReferenceReader? {
307       val arraySetClassId = graph.findClassByName(ARRAY_SET_CLASS_NAME)?.objectId ?:return null
308 
309       return object : VirtualInstanceReferenceReader {
310         override fun matches(instance: HeapInstance) = instance.instanceClassId == arraySetClassId
311 
312         override fun read(source: HeapInstance): Sequence<Reference> {
313           val mArray = source[ARRAY_SET_CLASS_NAME, "mArray"]!!.valueAsObjectArray!!
314           val locationClassObjectId = source.instanceClassId
315           return mArray.readElements()
316             .filter { it.isNonNullReference }
317             .map { reference ->
318               Reference(
319                 valueObjectId = reference.asNonNullObjectId!!,
320                 isLowPriority = false,
321                 lazyDetailsResolver = {
322                   LazyDetails(
323                     name = "element()",
324                     locationClassObjectId = locationClassObjectId,
325                     locationType = ARRAY_ENTRY,
326                     isVirtual = true,
327                     matchedLibraryLeak = null,
328                   )
329                 }
330               )
331             }
332         }
333       }
334     }
335   },
336 
337   ;
338 
339   companion object {
340     private const val ARRAY_SET_CLASS_NAME = "android.util.ArraySet"
341 
342     // Note: not supporting the support lib version of these, which is identical but with an
343     // "android" package prefix instead of "androidx".
344     private const val SAFE_ITERABLE_MAP_CLASS_NAME = "androidx.arch.core.internal.SafeIterableMap"
345     private const val FAST_SAFE_ITERABLE_MAP_CLASS_NAME =
346       "androidx.arch.core.internal.FastSafeIterableMap"
347     private const val SAFE_ITERABLE_MAP_ENTRY_CLASS_NAME =
348       "androidx.arch.core.internal.SafeIterableMap\$Entry"
349   }
350 }
351