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