xref: /aosp_15_r20/external/leakcanary2/shark-graph/src/main/java/shark/HprofHeapGraph.kt (revision d9e8da70d8c9df9a41d7848ae506fb3115cae6e6)
1 package shark
2 
3 import shark.GcRoot.Debugger
4 import shark.GcRoot.Finalizing
5 import shark.GcRoot.InternedString
6 import shark.GcRoot.JavaFrame
7 import shark.GcRoot.JniGlobal
8 import shark.GcRoot.JniLocal
9 import shark.GcRoot.JniMonitor
10 import shark.GcRoot.MonitorUsed
11 import shark.GcRoot.NativeStack
12 import shark.GcRoot.ReferenceCleanup
13 import shark.GcRoot.StickyClass
14 import shark.GcRoot.ThreadBlock
15 import shark.GcRoot.ThreadObject
16 import shark.GcRoot.Unknown
17 import shark.GcRoot.Unreachable
18 import shark.GcRoot.VmInternal
19 import shark.HeapObject.HeapClass
20 import shark.HeapObject.HeapInstance
21 import shark.HeapObject.HeapObjectArray
22 import shark.HeapObject.HeapPrimitiveArray
23 import shark.HprofRecord.HeapDumpRecord.ObjectRecord
24 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord
25 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord.FieldRecord
26 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord.StaticFieldRecord
27 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.InstanceDumpRecord
28 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.ObjectArrayDumpRecord
29 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord
30 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.BooleanArrayDump
31 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ByteArrayDump
32 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.CharArrayDump
33 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.DoubleArrayDump
34 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.FloatArrayDump
35 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.IntArrayDump
36 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.LongArrayDump
37 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ShortArrayDump
38 import shark.HprofVersion.ANDROID
39 import shark.internal.FieldValuesReader
40 import shark.internal.HprofInMemoryIndex
41 import shark.internal.IndexedObject
42 import shark.internal.IndexedObject.IndexedClass
43 import shark.internal.IndexedObject.IndexedInstance
44 import shark.internal.IndexedObject.IndexedObjectArray
45 import shark.internal.IndexedObject.IndexedPrimitiveArray
46 import shark.internal.LruCache
47 import java.io.File
48 import kotlin.reflect.KClass
49 import shark.PrimitiveType.BYTE
50 import shark.PrimitiveType.INT
51 
52 /**
53  * A [HeapGraph] that reads from an Hprof file indexed by [HprofIndex].
54  */
55 class HprofHeapGraph internal constructor(
56   private val header: HprofHeader,
57   private val reader: RandomAccessHprofReader,
58   private val index: HprofInMemoryIndex
59 ) : CloseableHeapGraph {
60 
61   override val identifierByteSize: Int get() = header.identifierByteSize
62 
63   override val context = GraphContext()
64 
65   override val objectCount: Int
66     get() = classCount + instanceCount + objectArrayCount + primitiveArrayCount
67 
68   override val classCount: Int
69     get() = index.classCount
70 
71   override val instanceCount: Int
72     get() = index.instanceCount
73 
74   override val objectArrayCount: Int
75     get() = index.objectArrayCount
76 
77   override val primitiveArrayCount: Int
78     get() = index.primitiveArrayCount
79 
80   override val gcRoots: List<GcRoot>
81     get() = index.gcRoots()
82 
83   override val objects: Sequence<HeapObject>
84     get() {
85       var objectIndex = 0
86       return index.indexedObjectSequence()
<lambda>null87         .map {
88           wrapIndexedObject(objectIndex++, it.second, it.first)
89         }
90     }
91 
92   override val classes: Sequence<HeapClass>
93     get() {
94       var objectIndex = 0
95       return index.indexedClassSequence()
<lambda>null96         .map {
97           val objectId = it.first
98           val indexedObject = it.second
99           HeapClass(this, indexedObject, objectId, objectIndex++)
100         }
101     }
102 
103   override val instances: Sequence<HeapInstance>
104     get() {
105       var objectIndex = classCount
106       return index.indexedInstanceSequence()
<lambda>null107         .map {
108           val objectId = it.first
109           val indexedObject = it.second
110           HeapInstance(this, indexedObject, objectId, objectIndex++)
111         }
112     }
113 
114   override val objectArrays: Sequence<HeapObjectArray>
115     get() {
116       var objectIndex = classCount + instanceCount
<lambda>null117       return index.indexedObjectArraySequence().map {
118         val objectId = it.first
119         val indexedObject = it.second
120         HeapObjectArray(this, indexedObject, objectId, objectIndex++)
121       }
122     }
123 
124   override val primitiveArrays: Sequence<HeapPrimitiveArray>
125     get() {
126       var objectIndex = classCount + instanceCount + objectArrayCount
<lambda>null127       return index.indexedPrimitiveArraySequence().map {
128         val objectId = it.first
129         val indexedObject = it.second
130         HeapPrimitiveArray(this, indexedObject, objectId, objectIndex++)
131       }
132     }
133 
134   private val objectCache = LruCache<Long, ObjectRecord>(INTERNAL_LRU_CACHE_SIZE)
135 
136   // java.lang.Object is the most accessed class in Heap, so we want to memoize a reference to it
137   private val javaLangObjectClass: HeapClass? = findClassByName("java.lang.Object")
138 
139   internal val objectArrayRecordNonElementSize = 2 * identifierByteSize + 2 * INT.byteSize
140 
141   internal val primitiveArrayRecordNonElementSize =
142     identifierByteSize + 2 * INT.byteSize + BYTE.byteSize
143 
144   /**
145    * This is only public so that we can publish stats. Accessing this requires casting
146    * [HeapGraph] to [HprofHeapGraph] so it's really not a public API. May change at any time!
147    */
lruCacheStatsnull148   fun lruCacheStats(): String = objectCache.toString()
149 
150   override fun findObjectById(objectId: Long): HeapObject {
151     return findObjectByIdOrNull(objectId) ?: throw IllegalArgumentException(
152       "Object id $objectId not found in heap dump."
153     )
154   }
155 
findObjectByIndexnull156   override fun findObjectByIndex(objectIndex: Int): HeapObject {
157     require(objectIndex in 0 until objectCount) {
158       "$objectIndex should be in range [0, $objectCount["
159     }
160     val (objectId, indexedObject) = index.objectAtIndex(objectIndex)
161     return wrapIndexedObject(objectIndex, indexedObject, objectId)
162   }
163 
findObjectByIdOrNullnull164   override fun findObjectByIdOrNull(objectId: Long): HeapObject? {
165     if (objectId == javaLangObjectClass?.objectId) return javaLangObjectClass
166 
167     val (objectIndex, indexedObject) = index.indexedObjectOrNull(objectId) ?: return null
168     return wrapIndexedObject(objectIndex, indexedObject, objectId)
169   }
170 
findClassByNamenull171   override fun findClassByName(className: String): HeapClass? {
172     val heapDumpClassName = if (header.version != ANDROID) {
173       val indexOfArrayChar = className.indexOf('[')
174       if (indexOfArrayChar != -1) {
175         val dimensions = (className.length - indexOfArrayChar) / 2
176         val componentClassName = className.substring(0, indexOfArrayChar)
177         "[".repeat(dimensions) + when (componentClassName) {
178           "char" -> 'C'
179           "float" -> 'F'
180           "double" -> 'D'
181           "byte" -> 'B'
182           "short" -> 'S'
183           "int" -> 'I'
184           "long" -> 'J'
185           else -> "L$componentClassName;"
186         }
187       } else {
188         className
189       }
190     } else {
191       className
192     }
193     val classId = index.classId(heapDumpClassName)
194     return if (classId == null) {
195       null
196     } else {
197       return findObjectById(classId) as HeapClass
198     }
199   }
200 
objectExistsnull201   override fun objectExists(objectId: Long): Boolean {
202     return index.objectIdIsIndexed(objectId)
203   }
204 
findHeapDumpIndexnull205   override fun findHeapDumpIndex(objectId: Long): Int {
206     val (_, indexedObject) = index.indexedObjectOrNull(objectId)?: throw IllegalArgumentException(
207       "Object id $objectId not found in heap dump."
208     )
209     val position = indexedObject.position
210 
211     var countObjectsBefore = 1
212     index.indexedObjectSequence()
213       .forEach {
214         if (position > it.second.position) {
215           countObjectsBefore++
216         }
217       }
218     return countObjectsBefore
219   }
220 
findObjectByHeapDumpIndexnull221   override fun findObjectByHeapDumpIndex(heapDumpIndex: Int): HeapObject {
222     require(heapDumpIndex in 1..objectCount) {
223       "$heapDumpIndex should be in range [1, $objectCount]"
224     }
225     val (objectId, _) = index.indexedObjectSequence().toList().sortedBy { it.second.position }[heapDumpIndex]
226     return findObjectById(objectId)
227   }
228 
closenull229   override fun close() {
230     reader.close()
231   }
232 
classDumpStaticFieldsnull233   internal fun classDumpStaticFields(indexedClass: IndexedClass): List<StaticFieldRecord> {
234     return index.classFieldsReader.classDumpStaticFields(indexedClass)
235   }
236 
classDumpFieldsnull237   internal fun classDumpFields(indexedClass: IndexedClass): List<FieldRecord> {
238     return index.classFieldsReader.classDumpFields(indexedClass)
239   }
240 
classDumpHasReferenceFieldsnull241   internal fun classDumpHasReferenceFields(indexedClass: IndexedClass): Boolean {
242     return index.classFieldsReader.classDumpHasReferenceFields(indexedClass)
243   }
244 
fieldNamenull245   internal fun fieldName(
246     classId: Long,
247     fieldRecord: FieldRecord
248   ): String {
249     return index.fieldName(classId, fieldRecord.nameStringId)
250   }
251 
staticFieldNamenull252   internal fun staticFieldName(
253     classId: Long,
254     fieldRecord: StaticFieldRecord
255   ): String {
256     return index.fieldName(classId, fieldRecord.nameStringId)
257   }
258 
createFieldValuesReadernull259   internal fun createFieldValuesReader(record: InstanceDumpRecord) =
260     FieldValuesReader(record, identifierByteSize)
261 
262   internal fun className(classId: Long): String {
263     val hprofClassName = index.className(classId)
264     if (header.version != ANDROID) {
265       if (hprofClassName.startsWith('[')) {
266         val arrayCharLastIndex = hprofClassName.lastIndexOf('[')
267         val brackets = "[]".repeat(arrayCharLastIndex + 1)
268         return when (val typeChar = hprofClassName[arrayCharLastIndex + 1]) {
269           'L' -> {
270             val classNameStart = arrayCharLastIndex + 2
271             hprofClassName.substring(classNameStart, hprofClassName.length - 1) + brackets
272           }
273           'Z' -> "boolean$brackets"
274           'C' -> "char$brackets"
275           'F' -> "float$brackets"
276           'D' -> "double$brackets"
277           'B' -> "byte$brackets"
278           'S' -> "short$brackets"
279           'I' -> "int$brackets"
280           'J' -> "long$brackets"
281           else -> error("Unexpected type char $typeChar")
282         }
283       }
284     }
285     return hprofClassName
286   }
287 
readObjectArrayDumpRecordnull288   internal fun readObjectArrayDumpRecord(
289     objectId: Long,
290     indexedObject: IndexedObjectArray
291   ): ObjectArrayDumpRecord {
292     return readObjectRecord(objectId, indexedObject) {
293       readObjectArrayDumpRecord()
294     }
295   }
296 
readObjectArrayByteSizenull297   internal fun readObjectArrayByteSize(
298     objectId: Long,
299     indexedObject: IndexedObjectArray
300   ): Int {
301     val cachedRecord = objectCache[objectId] as ObjectArrayDumpRecord?
302     if (cachedRecord != null) {
303       return cachedRecord.elementIds.size * identifierByteSize
304     }
305     val position = indexedObject.position + identifierByteSize + PrimitiveType.INT.byteSize
306     val size = PrimitiveType.INT.byteSize.toLong()
307     val thinRecordSize = reader.readRecord(position, size) {
308       readInt()
309     }
310     return thinRecordSize * identifierByteSize
311   }
312 
readPrimitiveArrayDumpRecordnull313   internal fun readPrimitiveArrayDumpRecord(
314     objectId: Long,
315     indexedObject: IndexedPrimitiveArray
316   ): PrimitiveArrayDumpRecord {
317     return readObjectRecord(objectId, indexedObject) {
318       readPrimitiveArrayDumpRecord()
319     }
320   }
321 
readPrimitiveArrayByteSizenull322   internal fun readPrimitiveArrayByteSize(
323     objectId: Long,
324     indexedObject: IndexedPrimitiveArray
325   ): Int {
326     val cachedRecord = objectCache[objectId] as PrimitiveArrayDumpRecord?
327     if (cachedRecord != null) {
328       return when (cachedRecord) {
329         is BooleanArrayDump -> cachedRecord.array.size * PrimitiveType.BOOLEAN.byteSize
330         is CharArrayDump -> cachedRecord.array.size * PrimitiveType.CHAR.byteSize
331         is FloatArrayDump -> cachedRecord.array.size * PrimitiveType.FLOAT.byteSize
332         is DoubleArrayDump -> cachedRecord.array.size * PrimitiveType.DOUBLE.byteSize
333         is ByteArrayDump -> cachedRecord.array.size * PrimitiveType.BYTE.byteSize
334         is ShortArrayDump -> cachedRecord.array.size * PrimitiveType.SHORT.byteSize
335         is IntArrayDump -> cachedRecord.array.size * PrimitiveType.INT.byteSize
336         is LongArrayDump -> cachedRecord.array.size * PrimitiveType.LONG.byteSize
337       }
338     }
339     val position = indexedObject.position + identifierByteSize + PrimitiveType.INT.byteSize
340     val size = reader.readRecord(position, PrimitiveType.INT.byteSize.toLong()) {
341       readInt()
342     }
343     return size * indexedObject.primitiveType.byteSize
344   }
345 
readClassDumpRecordnull346   internal fun readClassDumpRecord(
347     objectId: Long,
348     indexedObject: IndexedClass
349   ): ClassDumpRecord {
350     return readObjectRecord(objectId, indexedObject) {
351       readClassDumpRecord()
352     }
353   }
354 
readInstanceDumpRecordnull355   internal fun readInstanceDumpRecord(
356     objectId: Long,
357     indexedObject: IndexedInstance
358   ): InstanceDumpRecord {
359     return readObjectRecord(objectId, indexedObject) {
360       readInstanceDumpRecord()
361     }
362   }
363 
readObjectRecordnull364   private fun <T : ObjectRecord> readObjectRecord(
365     objectId: Long,
366     indexedObject: IndexedObject,
367     readBlock: HprofRecordReader.() -> T
368   ): T {
369     val objectRecordOrNull = objectCache[objectId]
370     @Suppress("UNCHECKED_CAST")
371     if (objectRecordOrNull != null) {
372       return objectRecordOrNull as T
373     }
374     return reader.readRecord(indexedObject.position, indexedObject.recordSize) {
375       readBlock()
376     }.apply { objectCache.put(objectId, this) }
377   }
378 
wrapIndexedObjectnull379   private fun wrapIndexedObject(
380     objectIndex: Int,
381     indexedObject: IndexedObject,
382     objectId: Long
383   ): HeapObject {
384     return when (indexedObject) {
385       is IndexedClass -> {
386         HeapClass(this, indexedObject, objectId, objectIndex)
387       }
388       is IndexedInstance -> {
389         HeapInstance(this, indexedObject, objectId, objectIndex)
390       }
391       is IndexedObjectArray -> {
392         HeapObjectArray(this, indexedObject, objectId, objectIndex)
393       }
394       is IndexedPrimitiveArray -> HeapPrimitiveArray(this, indexedObject, objectId, objectIndex)
395     }
396   }
397 
398   companion object {
399 
400     /**
401      * This is not a public API, it's only public so that we can evaluate the effectiveness of
402      * different cache size in tests in a different module.
403      *
404      * LRU cache size of 3000 is a sweet spot to balance hits vs memory usage.
405      * This is based on running InstrumentationLeakDetectorTest a bunch of time on a
406      * Pixel 2 XL API 28. Hit count was ~120K, miss count ~290K
407      */
408     var INTERNAL_LRU_CACHE_SIZE = 3000
409 
410     /**
411      * A facility for opening a [CloseableHeapGraph] from a [File].
412      * This first parses the file headers with [HprofHeader.parseHeaderOf], then indexes the file content
413      * with [HprofIndex.indexRecordsOf] and then opens a [CloseableHeapGraph] from the index, which
414      * you are responsible for closing after using.
415      */
openHeapGraphnull416     fun File.openHeapGraph(
417       proguardMapping: ProguardMapping? = null,
418       indexedGcRootTypes: Set<HprofRecordTag> = HprofIndex.defaultIndexedGcRootTags()
419     ): CloseableHeapGraph {
420       return FileSourceProvider(this).openHeapGraph(proguardMapping, indexedGcRootTypes)
421     }
422 
DualSourceProvidernull423     fun DualSourceProvider.openHeapGraph(
424       proguardMapping: ProguardMapping? = null,
425       indexedGcRootTypes: Set<HprofRecordTag> = HprofIndex.defaultIndexedGcRootTags()
426     ): CloseableHeapGraph {
427       val header = openStreamingSource().use { HprofHeader.parseHeaderOf(it) }
428       val index = HprofIndex.indexRecordsOf(this, header, proguardMapping, indexedGcRootTypes)
429       return index.openHeapGraph()
430     }
431 
432     @Deprecated(
433       "Replaced by HprofIndex.indexRecordsOf().openHeapGraph() or File.openHeapGraph()",
434       replaceWith = ReplaceWith(
435         "HprofIndex.indexRecordsOf(hprof, proguardMapping, indexedGcRootTypes)" +
436           ".openHeapGraph()"
437       )
438     )
indexHprofnull439     fun indexHprof(
440       hprof: Hprof,
441       proguardMapping: ProguardMapping? = null,
442       indexedGcRootTypes: Set<KClass<out GcRoot>> = deprecatedDefaultIndexedGcRootTypes()
443     ): HeapGraph {
444       val indexedRootTags = indexedGcRootTypes.map {
445         when (it) {
446           Unknown::class -> HprofRecordTag.ROOT_UNKNOWN
447           JniGlobal::class -> HprofRecordTag.ROOT_JNI_GLOBAL
448           JniLocal::class -> HprofRecordTag.ROOT_JNI_LOCAL
449           JavaFrame::class -> HprofRecordTag.ROOT_JAVA_FRAME
450           NativeStack::class -> HprofRecordTag.ROOT_NATIVE_STACK
451           StickyClass::class -> HprofRecordTag.ROOT_STICKY_CLASS
452           ThreadBlock::class -> HprofRecordTag.ROOT_THREAD_BLOCK
453           MonitorUsed::class -> HprofRecordTag.ROOT_MONITOR_USED
454           ThreadObject::class -> HprofRecordTag.ROOT_THREAD_OBJECT
455           InternedString::class -> HprofRecordTag.ROOT_INTERNED_STRING
456           Finalizing::class -> HprofRecordTag.ROOT_FINALIZING
457           Debugger::class -> HprofRecordTag.ROOT_DEBUGGER
458           ReferenceCleanup::class -> HprofRecordTag.ROOT_REFERENCE_CLEANUP
459           VmInternal::class -> HprofRecordTag.ROOT_VM_INTERNAL
460           JniMonitor::class -> HprofRecordTag.ROOT_JNI_MONITOR
461           Unreachable::class -> HprofRecordTag.ROOT_UNREACHABLE
462           else -> error("Unknown root $it")
463         }
464       }.toSet()
465       val index =
466         HprofIndex.indexRecordsOf(
467           FileSourceProvider(hprof.file), hprof.header, proguardMapping, indexedRootTags
468         )
469       val graph = index.openHeapGraph()
470       hprof.attachClosable(graph)
471       return graph
472     }
473 
deprecatedDefaultIndexedGcRootTypesnull474     private fun deprecatedDefaultIndexedGcRootTypes() = setOf(
475       JniGlobal::class,
476       JavaFrame::class,
477       JniLocal::class,
478       MonitorUsed::class,
479       NativeStack::class,
480       StickyClass::class,
481       ThreadBlock::class,
482       ThreadObject::class,
483       JniMonitor::class
484     )
485   }
486 }
487