<lambda>null1 package shark
2
3 import shark.LeakTraceObject.LeakingStatus.LEAKING
4 import shark.LeakTraceObject.LeakingStatus.NOT_LEAKING
5 import shark.LeakTraceObject.LeakingStatus.UNKNOWN
6 import shark.LeakTraceReference.ReferenceType.INSTANCE_FIELD
7 import shark.LeakTraceReference.ReferenceType.STATIC_FIELD
8 import shark.internal.createSHA1Hash
9 import java.io.Serializable
10
11 /**
12 * The best strong reference path from a GC root to the leaking object. "Best" here means the
13 * shortest prioritized path. A large number of distinct paths can generally be found leading
14 * to a leaking object. Shark prioritizes paths that don't go through known
15 * [LibraryLeakReferenceMatcher] (because those are known to create leaks so it's more interesting
16 * to find other paths causing leaks), then it prioritize paths that don't go through java local
17 * gc roots (because those are harder to reason about). Taking those priorities into account,
18 * finding the shortest path means there are less [LeakTraceReference] that can be suspected to
19 * cause the leak.
20 */
21 data class LeakTrace(
22 /**
23 * The Garbage Collection root that references the [LeakTraceReference.originObject] in
24 * the first [LeakTraceReference] of [referencePath].
25 */
26 val gcRootType: GcRootType,
27 val referencePath: List<LeakTraceReference>,
28 val leakingObject: LeakTraceObject
29 ) : Serializable {
30
31 /**
32 * The minimum number of bytes which would be freed if the leak was fixed.
33 * Null if the retained heap size was not computed.
34 */
35 val retainedHeapByteSize: Int?
36 get() {
37 val allObjects = listOf(leakingObject) + referencePath.map { it.originObject }
38 return allObjects.filter { it.leakingStatus == LEAKING }
39 .mapNotNull { it.retainedHeapByteSize }
40 // The minimum released is the max held by a leaking object.
41 .max()
42 }
43
44 /**
45 * The minimum number of objects which would be unreachable if the leak was fixed. Null if the
46 * retained heap size was not computed.
47 */
48 val retainedObjectCount: Int?
49 get() {
50 val allObjects = listOf(leakingObject) + referencePath.map { it.originObject }
51 return allObjects.filter { it.leakingStatus == LEAKING }
52 .mapNotNull { it.retainedObjectCount }
53 // The minimum released is the max held by a leaking object.
54 .max()
55 }
56
57 /**
58 * A part of [referencePath] that contains the references suspected to cause the leak.
59 * Starts at the last non leaking object and ends before the first leaking object.
60 */
61 val suspectReferenceSubpath
62 get() = referencePath.asSequence()
63 .filterIndexed { index, _ ->
64 referencePathElementIsSuspect(index)
65 }
66
67 /**
68 * A SHA1 hash that represents this leak trace. This can be useful to group together similar
69 * leak traces.
70 *
71 * The signature is a hash of [suspectReferenceSubpath].
72 */
73 val signature: String
74 get() = suspectReferenceSubpath
75 .joinToString(separator = "") { element ->
76 element.originObject.className + element.referenceGenericName
77 }
78 .createSHA1Hash()
79
80 /**
81 * Returns true if the [referencePath] element at the provided [index] contains a reference
82 * that is suspected to cause the leak, ie if [index] is greater than or equal to the index
83 * of the [LeakTraceReference] of the last non leaking object and strictly lower than the index
84 * of the [LeakTraceReference] of the first leaking object.
85 */
86 fun referencePathElementIsSuspect(index: Int): Boolean {
87 return when (referencePath[index].originObject.leakingStatus) {
88 UNKNOWN -> true
89 NOT_LEAKING -> index == referencePath.lastIndex ||
90 referencePath[index + 1].originObject.leakingStatus != NOT_LEAKING
91 else -> false
92 }
93 }
94
95 override fun toString(): String = leakTraceAsString(showLeakingStatus = true)
96
97 fun toSimplePathString(): String = leakTraceAsString(showLeakingStatus = false)
98
99 private fun leakTraceAsString(showLeakingStatus: Boolean): String {
100 var result = """
101 ┬───
102 │ GC Root: ${gcRootType.description}
103 │
104 """.trimIndent()
105
106 referencePath.forEachIndexed { index, element ->
107 val originObject = element.originObject
108 result += "\n"
109 result += originObject.toString(
110 firstLinePrefix = "├─ ",
111 additionalLinesPrefix = "│ ",
112 showLeakingStatus = showLeakingStatus,
113 typeName = originObject.typeName
114 )
115 result += getNextElementString(this, element, index, showLeakingStatus)
116 }
117
118 result += "\n"
119 result += leakingObject.toString(
120 firstLinePrefix = "╰→ ",
121 additionalLinesPrefix = "$ZERO_WIDTH_SPACE ",
122 showLeakingStatus = showLeakingStatus
123 )
124 return result
125 }
126
127 enum class GcRootType(val description: String) {
128 JNI_GLOBAL("Global variable in native code"),
129 JNI_LOCAL("Local variable in native code"),
130 JAVA_FRAME("Java local variable"),
131 NATIVE_STACK("Input or output parameters in native code"),
132 STICKY_CLASS("System class"),
133 THREAD_BLOCK("Thread block"),
134 MONITOR_USED(
135 "Monitor (anything that called the wait() or notify() methods, or that is synchronized.)"
136 ),
137 THREAD_OBJECT("Thread object"),
138 JNI_MONITOR("Root JNI monitor"),
139 ;
140
141 companion object {
142 fun fromGcRoot(gcRoot: GcRoot): GcRootType = when (gcRoot) {
143 is GcRoot.JniGlobal -> JNI_GLOBAL
144 is GcRoot.JniLocal -> JNI_LOCAL
145 is GcRoot.JavaFrame -> JAVA_FRAME
146 is GcRoot.NativeStack -> NATIVE_STACK
147 is GcRoot.StickyClass -> STICKY_CLASS
148 is GcRoot.ThreadBlock -> THREAD_BLOCK
149 is GcRoot.MonitorUsed -> MONITOR_USED
150 is GcRoot.ThreadObject -> THREAD_OBJECT
151 is GcRoot.JniMonitor -> JNI_MONITOR
152 else -> throw IllegalStateException("Unexpected gc root $gcRoot")
153 }
154 }
155 }
156
157 companion object {
158 private fun getNextElementString(
159 leakTrace: LeakTrace,
160 reference: LeakTraceReference,
161 index: Int,
162 showLeakingStatus: Boolean
163 ): String {
164 val static = if (reference.referenceType == STATIC_FIELD) " static" else ""
165
166 val referenceLinePrefix = " ↓$static ${reference.owningClassSimpleName.removeSuffix("[]")}" +
167 when (reference.referenceType) {
168 STATIC_FIELD, INSTANCE_FIELD -> "."
169 else -> ""
170 }
171
172 val referenceName = reference.referenceDisplayName
173 val referenceLine = referenceLinePrefix + referenceName
174
175 return if (showLeakingStatus && leakTrace.referencePathElementIsSuspect(index)) {
176 val spaces = " ".repeat(referenceLinePrefix.length)
177 val underline = "~".repeat(referenceName.length)
178 "\n│$referenceLine\n│$spaces$underline"
179 } else {
180 "\n│$referenceLine"
181 }
182 }
183
184 internal const val ZERO_WIDTH_SPACE = '\u200b'
185 private const val serialVersionUID = -6315725584154386429
186 }
187 }
188