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

<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