1 package shark 2 3 import shark.LeakTrace.Companion.ZERO_WIDTH_SPACE 4 import shark.LeakTraceObject.LeakingStatus 5 import shark.LeakTraceObject.LeakingStatus.LEAKING 6 import shark.LeakTraceObject.LeakingStatus.NOT_LEAKING 7 import shark.LeakTraceObject.LeakingStatus.UNKNOWN 8 import shark.internal.lastSegment 9 import java.io.Serializable 10 import java.util.Locale 11 import kotlin.math.ln 12 import kotlin.math.pow 13 14 data class LeakTraceObject( 15 val type: ObjectType, 16 /** 17 * Class name of the object. 18 * The class name format is the same as what would be returned by [Class.getName]. 19 */ 20 val className: String, 21 22 /** 23 * Labels that were computed during analysis. A label provides extra information that helps 24 * understand the state of the leak trace object. 25 */ 26 val labels: Set<String>, 27 val leakingStatus: LeakingStatus, 28 val leakingStatusReason: String, 29 /** 30 * The minimum number of bytes which would be freed if all references to this object were 31 * released. Not null only if the retained heap size was computed AND [leakingStatus] is 32 * equal to [LeakingStatus.UNKNOWN] or [LeakingStatus.LEAKING]. 33 */ 34 val retainedHeapByteSize: Int?, 35 /** 36 * The minimum number of objects which would be unreachable if all references to this object were 37 * released. Not null only if the retained heap size was computed AND [leakingStatus] is 38 * equal to [LeakingStatus.UNKNOWN] or [LeakingStatus.LEAKING]. 39 */ 40 val retainedObjectCount: Int? 41 ) : Serializable { 42 43 /** 44 * Returns {@link #className} without the package, ie stripped of any string content before the 45 * last period (included). 46 */ 47 val classSimpleName: String get() = className.lastSegment('.') 48 49 val typeName 50 get() = type.name.toLowerCase(Locale.US) 51 toStringnull52 override fun toString(): String { 53 val firstLinePrefix = "" 54 val additionalLinesPrefix = "$ZERO_WIDTH_SPACE " 55 return toString(firstLinePrefix, additionalLinesPrefix, true) 56 } 57 toStringnull58 internal fun toString( 59 firstLinePrefix: String, 60 additionalLinesPrefix: String, 61 showLeakingStatus: Boolean, 62 typeName: String = this.typeName 63 ): String { 64 val leakStatus = when (leakingStatus) { 65 UNKNOWN -> "UNKNOWN" 66 NOT_LEAKING -> "NO ($leakingStatusReason)" 67 LEAKING -> "YES ($leakingStatusReason)" 68 } 69 70 var result = "" 71 result += "$firstLinePrefix$className $typeName" 72 if (showLeakingStatus) { 73 result += "\n${additionalLinesPrefix}Leaking: $leakStatus" 74 } 75 76 if (retainedHeapByteSize != null) { 77 val humanReadableRetainedHeapSize = 78 humanReadableByteCount(retainedHeapByteSize.toLong()) 79 result += "\n${additionalLinesPrefix}Retaining $humanReadableRetainedHeapSize in $retainedObjectCount objects" 80 } 81 for (label in labels) { 82 result += "\n${additionalLinesPrefix}$label" 83 } 84 return result 85 } 86 87 enum class ObjectType { 88 CLASS, 89 ARRAY, 90 INSTANCE 91 } 92 93 enum class LeakingStatus { 94 /** The object was needed and therefore expected to be reachable. */ 95 NOT_LEAKING, 96 97 /** The object was no longer needed and therefore expected to be unreachable. */ 98 LEAKING, 99 100 /** No decision can be made about the provided object. */ 101 UNKNOWN; 102 } 103 104 companion object { 105 private const val serialVersionUID = -3616216391305196341L 106 107 // https://stackoverflow.com/a/3758880 humanReadableByteCountnull108 private fun humanReadableByteCount(bytes: Long): String { 109 val unit = 1000 110 if (bytes < unit) return "$bytes B" 111 val exp = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt() 112 val pre = "kMGTPE"[exp - 1] 113 return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre) 114 } 115 } 116 }