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

<lambda>null1 package shark.internal
2 
3 import shark.GcRoot.ThreadObject
4 import shark.HeapGraph
5 import shark.HeapObject.HeapClass
6 import shark.HeapObject.HeapInstance
7 import shark.HeapObject.HeapObjectArray
8 import shark.HeapObject.HeapPrimitiveArray
9 import shark.IgnoredReferenceMatcher
10 import shark.OnAnalysisProgressListener
11 import shark.ValueHolder
12 
13 /**
14  * Exposes high level APIs to compute and render a dominator tree. This class
15  * needs to be public to be used by other LeakCanary modules but is internal and
16  * its API might change at any moment.
17  *
18  * Note that the exposed APIs are not optimized for speed, memory or IO.
19  *
20  * Eventually this capability should become part of the Shark public APIs, please
21  * open an issue if you'd like to use this directly.
22  */
23 class ObjectDominators {
24 
25   internal data class DominatorNode(
26     val shallowSize: Int,
27     val retainedSize: Int,
28     val retainedCount: Int,
29     val dominatedObjectIds: List<Long>
30   )
31 
32   fun renderDominatorTree(
33     graph: HeapGraph,
34     ignoredRefs: List<IgnoredReferenceMatcher>,
35     minRetainedSize: Int,
36     threadName: String? = null,
37     printStringContent: Boolean = false
38   ): String {
39     val stringBuilder = StringBuilder()
40 
41     val dominatorTree = buildDominatorTree(graph, ignoredRefs)
42 
43     val root = dominatorTree.getValue(ValueHolder.NULL_REFERENCE)
44     stringBuilder.append(
45       "Total retained: ${root.retainedSize} bytes in ${root.retainedCount} objects. Root dominators: ${root.dominatedObjectIds.size}\n\n"
46     )
47 
48     val rootIds = if (threadName != null) {
49       setOf(graph.gcRoots.first { gcRoot ->
50         gcRoot is ThreadObject &&
51           graph.objectExists(gcRoot.id) &&
52           graph.findObjectById(gcRoot.id)
53             .asInstance!!["java.lang.Thread", "name"]!!
54             .value.readAsJavaString() == threadName
55       }.id)
56     } else {
57       root.dominatedObjectIds.filter { dominatorTree.getValue(it).retainedSize > minRetainedSize }
58     }
59 
60     rootIds
61       .forEach { objectId ->
62         printTree(
63           stringBuilder, graph, dominatorTree, objectId, minRetainedSize, 0, "", true,
64           printStringContent
65         )
66         stringBuilder.append("\n")
67       }
68     return stringBuilder.toString()
69   }
70 
71   @Suppress("LongParameterList")
72   private fun printTree(
73     stringBuilder: StringBuilder,
74     graph: HeapGraph,
75     tree: Map<Long, DominatorNode>,
76     objectId: Long,
77     minSize: Int,
78     depth: Int,
79     prefix: String,
80     isLast: Boolean,
81     printStringContent: Boolean
82   ) {
83     val node = tree.getValue(objectId)
84     val heapObject = graph.findObjectById(objectId)
85     val className = when (heapObject) {
86       is HeapClass -> "class ${heapObject.name}"
87       is HeapInstance -> heapObject.instanceClassName
88       is HeapObjectArray -> heapObject.arrayClassName
89       is HeapPrimitiveArray -> heapObject.arrayClassName
90     }
91     val anchor = if (depth == 0) "" else if (isLast) "╰─" else "├─"
92     val size = if (node.retainedSize != node.shallowSize) {
93       "${node.retainedSize} bytes (${node.shallowSize} self)"
94     } else {
95       "${node.shallowSize} bytes"
96     }
97     val count = if (node.retainedCount > 1) {
98       " ${node.retainedCount} objects"
99     } else {
100       ""
101     }
102     val stringContent = if (
103       printStringContent &&
104       heapObject is HeapInstance &&
105       heapObject.instanceClassName == "java.lang.String"
106     ) " \"${heapObject.readAsJavaString()}\"" else ""
107     stringBuilder.append(
108       "$prefix$anchor$className #${heapObject.objectIndex} Retained: $size$count$stringContent\n"
109     )
110 
111     val newPrefix = when {
112       depth == 0 -> ""
113       isLast -> {
114         "$prefix  "
115       }
116       else -> {
117         "$prefix│ "
118       }
119     }
120 
121     val largeChildren = node.dominatedObjectIds.filter { tree.getValue(it).retainedSize > minSize }
122     val lastIndex = node.dominatedObjectIds.lastIndex
123 
124     largeChildren.forEachIndexed { index, objectId ->
125       printTree(
126         stringBuilder,
127         graph, tree, objectId, minSize, depth + 1, newPrefix,
128         index == lastIndex,
129         printStringContent
130       )
131     }
132     if (largeChildren.size < node.dominatedObjectIds.size) {
133       stringBuilder.append("$newPrefix╰┄\n")
134     }
135   }
136 
137   private fun buildDominatorTree(
138     graph: HeapGraph,
139     ignoredRefs: List<IgnoredReferenceMatcher>
140   ): Map<Long, DominatorNode> {
141     val referenceReader = DelegatingObjectReferenceReader(
142       classReferenceReader = ClassReferenceReader(graph, emptyList()),
143       instanceReferenceReader = ChainingInstanceReferenceReader(
144         listOf(JavaLocalReferenceReader(graph, emptyList())),
145         FieldInstanceReferenceReader(graph, emptyList())
146       ),      objectArrayReferenceReader = ObjectArrayReferenceReader()
147     )
148 
149     val pathFinder = PathFinder(
150       graph,
151       OnAnalysisProgressListener.NO_OP, referenceReader,  ignoredRefs
152     )
153     val nativeSizeMapper = AndroidNativeSizeMapper(graph)
154     val nativeSizes = nativeSizeMapper.mapNativeSizes()
155     val shallowSizeCalculator = ShallowSizeCalculator(graph)
156 
157     val result = pathFinder.findPathsFromGcRoots(setOf(), true)
158     return result.dominatorTree!!.buildFullDominatorTree { objectId ->
159       val nativeSize = nativeSizes[objectId] ?: 0
160       val shallowSize = shallowSizeCalculator.computeShallowSize(objectId)
161       nativeSize + shallowSize
162     }
163   }
164 }
165