xref: /aosp_15_r20/external/leakcanary2/shark/src/main/java/shark/HeapAnalyzer.kt (revision d9e8da70d8c9df9a41d7848ae506fb3115cae6e6)
1 /*
<lambda>null2  * Copyright (C) 2015 Square, Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package shark
17 
18 import shark.HeapAnalyzer.TrieNode.LeafNode
19 import shark.HeapAnalyzer.TrieNode.ParentNode
20 import shark.HeapObject.HeapClass
21 import shark.HeapObject.HeapInstance
22 import shark.HeapObject.HeapObjectArray
23 import shark.HeapObject.HeapPrimitiveArray
24 import shark.HprofHeapGraph.Companion.openHeapGraph
25 import shark.LeakTrace.GcRootType
26 import shark.LeakTraceObject.LeakingStatus
27 import shark.LeakTraceObject.LeakingStatus.LEAKING
28 import shark.LeakTraceObject.LeakingStatus.NOT_LEAKING
29 import shark.LeakTraceObject.LeakingStatus.UNKNOWN
30 import shark.LeakTraceObject.ObjectType.ARRAY
31 import shark.LeakTraceObject.ObjectType.CLASS
32 import shark.LeakTraceObject.ObjectType.INSTANCE
33 import shark.OnAnalysisProgressListener.Step.BUILDING_LEAK_TRACES
34 import shark.OnAnalysisProgressListener.Step.COMPUTING_NATIVE_RETAINED_SIZE
35 import shark.OnAnalysisProgressListener.Step.COMPUTING_RETAINED_SIZE
36 import shark.OnAnalysisProgressListener.Step.EXTRACTING_METADATA
37 import shark.OnAnalysisProgressListener.Step.FINDING_RETAINED_OBJECTS
38 import shark.OnAnalysisProgressListener.Step.INSPECTING_OBJECTS
39 import shark.OnAnalysisProgressListener.Step.PARSING_HEAP_DUMP
40 import shark.internal.AndroidNativeSizeMapper
41 import shark.internal.DominatorTree
42 import shark.internal.PathFinder
43 import shark.internal.PathFinder.PathFindingResults
44 import shark.internal.ReferencePathNode
45 import shark.internal.ReferencePathNode.ChildNode
46 import shark.internal.ReferencePathNode.RootNode
47 import shark.internal.ShallowSizeCalculator
48 import shark.internal.createSHA1Hash
49 import shark.internal.lastSegment
50 import java.io.File
51 import java.util.ArrayList
52 import java.util.concurrent.TimeUnit.NANOSECONDS
53 import shark.internal.AndroidReferenceReaders
54 import shark.internal.ApacheHarmonyInstanceRefReaders
55 import shark.internal.ChainingInstanceReferenceReader
56 import shark.internal.ClassReferenceReader
57 import shark.internal.DelegatingObjectReferenceReader
58 import shark.internal.FieldInstanceReferenceReader
59 import shark.internal.JavaLocalReferenceReader
60 import shark.internal.ObjectArrayReferenceReader
61 import shark.internal.OpenJdkInstanceRefReaders
62 import shark.internal.ReferenceLocationType
63 import shark.internal.ReferencePathNode.RootNode.LibraryLeakRootNode
64 import shark.internal.ReferenceReader
65 
66 /**
67  * Analyzes heap dumps to look for leaks.
68  */
69 class HeapAnalyzer constructor(
70   private val listener: OnAnalysisProgressListener
71 ) {
72 
73   private class FindLeakInput(
74     val graph: HeapGraph,
75     val referenceMatchers: List<ReferenceMatcher>,
76     val computeRetainedHeapSize: Boolean,
77     val objectInspectors: List<ObjectInspector>,
78     val referenceReader: ReferenceReader<HeapObject>
79   )
80 
81   @Deprecated("Use the non deprecated analyze method instead")
82   fun analyze(
83     heapDumpFile: File,
84     leakingObjectFinder: LeakingObjectFinder,
85     referenceMatchers: List<ReferenceMatcher> = emptyList(),
86     computeRetainedHeapSize: Boolean = false,
87     objectInspectors: List<ObjectInspector> = emptyList(),
88     metadataExtractor: MetadataExtractor = MetadataExtractor.NO_OP,
89     proguardMapping: ProguardMapping? = null
90   ): HeapAnalysis {
91     if (!heapDumpFile.exists()) {
92       val exception = IllegalArgumentException("File does not exist: $heapDumpFile")
93       return HeapAnalysisFailure(
94         heapDumpFile = heapDumpFile,
95         createdAtTimeMillis = System.currentTimeMillis(),
96         analysisDurationMillis = 0,
97         exception = HeapAnalysisException(exception)
98       )
99     }
100     listener.onAnalysisProgress(PARSING_HEAP_DUMP)
101     val sourceProvider = ConstantMemoryMetricsDualSourceProvider(FileSourceProvider(heapDumpFile))
102     return try {
103       sourceProvider.openHeapGraph(proguardMapping).use { graph ->
104         analyze(
105           heapDumpFile,
106           graph,
107           leakingObjectFinder,
108           referenceMatchers,
109           computeRetainedHeapSize,
110           objectInspectors,
111           metadataExtractor
112         ).let { result ->
113           if (result is HeapAnalysisSuccess) {
114             val lruCacheStats = (graph as HprofHeapGraph).lruCacheStats()
115             val randomAccessStats =
116               "RandomAccess[" +
117                 "bytes=${sourceProvider.randomAccessByteReads}," +
118                 "reads=${sourceProvider.randomAccessReadCount}," +
119                 "travel=${sourceProvider.randomAccessByteTravel}," +
120                 "range=${sourceProvider.byteTravelRange}," +
121                 "size=${heapDumpFile.length()}" +
122                 "]"
123             val stats = "$lruCacheStats $randomAccessStats"
124             result.copy(metadata = result.metadata + ("Stats" to stats))
125           } else result
126         }
127       }
128     } catch (throwable: Throwable) {
129       HeapAnalysisFailure(
130         heapDumpFile = heapDumpFile,
131         createdAtTimeMillis = System.currentTimeMillis(),
132         analysisDurationMillis = 0,
133         exception = HeapAnalysisException(throwable)
134       )
135     }
136   }
137 
138   /**
139    * Searches the heap dump for leaking instances and then computes the shortest strong reference
140    * path from those instances to the GC roots.
141    */
142   fun analyze(
143     heapDumpFile: File,
144     graph: HeapGraph,
145     leakingObjectFinder: LeakingObjectFinder,
146     referenceMatchers: List<ReferenceMatcher> = emptyList(),
147     computeRetainedHeapSize: Boolean = false,
148     objectInspectors: List<ObjectInspector> = emptyList(),
149     metadataExtractor: MetadataExtractor = MetadataExtractor.NO_OP,
150   ): HeapAnalysis {
151     val analysisStartNanoTime = System.nanoTime()
152 
153     val referenceReader = DelegatingObjectReferenceReader(
154       classReferenceReader = ClassReferenceReader(graph, referenceMatchers),
155       instanceReferenceReader = ChainingInstanceReferenceReader(
156         listOf(
157           JavaLocalReferenceReader(graph, referenceMatchers),
158         )
159           + OpenJdkInstanceRefReaders.values().mapNotNull { it.create(graph) }
160           + ApacheHarmonyInstanceRefReaders.values().mapNotNull { it.create(graph) }
161           + AndroidReferenceReaders.values().mapNotNull { it.create(graph) },
162         FieldInstanceReferenceReader(graph, referenceMatchers)
163       ),
164       objectArrayReferenceReader = ObjectArrayReferenceReader()
165     )
166     return analyze(
167       heapDumpFile,
168       graph,
169       leakingObjectFinder,
170       referenceMatchers,
171       computeRetainedHeapSize,
172       objectInspectors,
173       metadataExtractor,
174       referenceReader
175     ).run {
176       val updatedDurationMillis = since(analysisStartNanoTime)
177       when (this) {
178         is HeapAnalysisSuccess -> copy(analysisDurationMillis = updatedDurationMillis)
179         is HeapAnalysisFailure -> copy(analysisDurationMillis = updatedDurationMillis)
180       }
181     }
182   }
183 
184   // TODO Callers should add to analysisStartNanoTime
185   // Input should be a builder or part of the object state probs?
186   // Maybe there's some sort of helper for setting up the right analysis?
187   // There's a part focused on finding leaks, and then we add to that.
188   // Maybe the result isn't even a leaktrace yet, but something with live object ids?
189   // Ideally the result contains only what this can return. No file, etc.
190   // File: used to create the graph + in result
191   // leakingObjectFinder: Helper object, shared
192   // computeRetainedHeapSize: boolean param for single analysis
193   // referenceMatchers: param?. though honestly not great.
194   // objectInspectors: Helper object.
195   // metadataExtractor: helper object, not needed for leak finding
196   // referenceReader: can't be helper object, needs graph => param something that can produce it from
197   // graph (and in the impl we give that thing the referenceMatchers)
198   @Suppress("LongParameterList")
199   internal fun analyze(
200     // TODO Kill this file
201     heapDumpFile: File,
202     graph: HeapGraph,
203     leakingObjectFinder: LeakingObjectFinder,
204     referenceMatchers: List<ReferenceMatcher>,
205     computeRetainedHeapSize: Boolean,
206     objectInspectors: List<ObjectInspector>,
207     metadataExtractor: MetadataExtractor,
208     referenceReader: ReferenceReader<HeapObject>
209   ): HeapAnalysis {
210     val analysisStartNanoTime = System.nanoTime()
211     return try {
212       val helpers =
213         FindLeakInput(
214           graph, referenceMatchers, computeRetainedHeapSize, objectInspectors,
215           referenceReader
216         )
217       helpers.analyzeGraph(
218         metadataExtractor, leakingObjectFinder, heapDumpFile, analysisStartNanoTime
219       )
220     } catch (exception: Throwable) {
221       HeapAnalysisFailure(
222         heapDumpFile = heapDumpFile,
223         createdAtTimeMillis = System.currentTimeMillis(),
224         analysisDurationMillis = since(analysisStartNanoTime),
225         exception = HeapAnalysisException(exception)
226       )
227     }
228   }
229 
230   private fun FindLeakInput.analyzeGraph(
231     metadataExtractor: MetadataExtractor,
232     leakingObjectFinder: LeakingObjectFinder,
233     heapDumpFile: File,
234     analysisStartNanoTime: Long
235   ): HeapAnalysisSuccess {
236     listener.onAnalysisProgress(EXTRACTING_METADATA)
237     val metadata = metadataExtractor.extractMetadata(graph)
238 
239     val retainedClearedWeakRefCount = KeyedWeakReferenceFinder.findKeyedWeakReferences(graph)
240       .count { it.isRetained && !it.hasReferent }
241 
242     // This should rarely happens, as we generally remove all cleared weak refs right before a heap
243     // dump.
244     val metadataWithCount = if (retainedClearedWeakRefCount > 0) {
245       metadata + ("Count of retained yet cleared" to "$retainedClearedWeakRefCount KeyedWeakReference instances")
246     } else {
247       metadata
248     }
249 
250     listener.onAnalysisProgress(FINDING_RETAINED_OBJECTS)
251     val leakingObjectIds = leakingObjectFinder.findLeakingObjectIds(graph)
252 
253     val (applicationLeaks, libraryLeaks, unreachableObjects) = findLeaks(leakingObjectIds)
254 
255     return HeapAnalysisSuccess(
256       heapDumpFile = heapDumpFile,
257       createdAtTimeMillis = System.currentTimeMillis(),
258       analysisDurationMillis = since(analysisStartNanoTime),
259       metadata = metadataWithCount,
260       applicationLeaks = applicationLeaks,
261       libraryLeaks = libraryLeaks,
262       unreachableObjects = unreachableObjects
263     )
264   }
265 
266   private data class LeaksAndUnreachableObjects(
267     val applicationLeaks: List<ApplicationLeak>,
268     val libraryLeaks: List<LibraryLeak>,
269     val unreachableObjects: List<LeakTraceObject>
270   )
271 
272   private fun FindLeakInput.findLeaks(leakingObjectIds: Set<Long>): LeaksAndUnreachableObjects {
273     val pathFinder = PathFinder(graph, listener, referenceReader, referenceMatchers)
274     val pathFindingResults =
275       pathFinder.findPathsFromGcRoots(leakingObjectIds, computeRetainedHeapSize)
276 
277     val unreachableObjects = findUnreachableObjects(pathFindingResults, leakingObjectIds)
278 
279     val shortestPaths =
280       deduplicateShortestPaths(pathFindingResults.pathsToLeakingObjects)
281 
282     val inspectedObjectsByPath = inspectObjects(shortestPaths)
283 
284     val retainedSizes =
285       if (pathFindingResults.dominatorTree != null) {
286         computeRetainedSizes(inspectedObjectsByPath, pathFindingResults.dominatorTree)
287       } else {
288         null
289       }
290     val (applicationLeaks, libraryLeaks) = buildLeakTraces(
291       shortestPaths, inspectedObjectsByPath, retainedSizes
292     )
293     return LeaksAndUnreachableObjects(applicationLeaks, libraryLeaks, unreachableObjects)
294   }
295 
296   private fun FindLeakInput.findUnreachableObjects(
297     pathFindingResults: PathFindingResults,
298     leakingObjectIds: Set<Long>
299   ): List<LeakTraceObject> {
300     val reachableLeakingObjectIds =
301       pathFindingResults.pathsToLeakingObjects.map { it.objectId }.toSet()
302 
303     val unreachableLeakingObjectIds = leakingObjectIds - reachableLeakingObjectIds
304 
305     val unreachableObjectReporters = unreachableLeakingObjectIds.map { objectId ->
306       ObjectReporter(heapObject = graph.findObjectById(objectId))
307     }
308 
309     objectInspectors.forEach { inspector ->
310       unreachableObjectReporters.forEach { reporter ->
311         inspector.inspect(reporter)
312       }
313     }
314 
315     val unreachableInspectedObjects = unreachableObjectReporters.map { reporter ->
316       val reason = resolveStatus(reporter, leakingWins = true).let { (status, reason) ->
317         when (status) {
318           LEAKING -> reason
319           UNKNOWN -> "This is a leaking object"
320           NOT_LEAKING -> "This is a leaking object. Conflicts with $reason"
321         }
322       }
323       InspectedObject(
324         reporter.heapObject, LEAKING, reason, reporter.labels
325       )
326     }
327 
328     return buildLeakTraceObjects(unreachableInspectedObjects, null)
329   }
330 
331   internal sealed class TrieNode {
332     abstract val objectId: Long
333 
334     class ParentNode(override val objectId: Long) : TrieNode() {
335       val children = mutableMapOf<Long, TrieNode>()
336       override fun toString(): String {
337         return "ParentNode(objectId=$objectId, children=$children)"
338       }
339     }
340 
341     class LeafNode(
342       override val objectId: Long,
343       val pathNode: ReferencePathNode
344     ) : TrieNode()
345   }
346 
347   private fun deduplicateShortestPaths(
348     inputPathResults: List<ReferencePathNode>
349   ): List<ShortestPath> {
350     val rootTrieNode = ParentNode(0)
351 
352     inputPathResults.forEach { pathNode ->
353       // Go through the linked list of nodes and build the reverse list of instances from
354       // root to leaking.
355       val path = mutableListOf<Long>()
356       var leakNode: ReferencePathNode = pathNode
357       while (leakNode is ChildNode) {
358         path.add(0, leakNode.objectId)
359         leakNode = leakNode.parent
360       }
361       path.add(0, leakNode.objectId)
362       updateTrie(pathNode, path, 0, rootTrieNode)
363     }
364 
365     val outputPathResults = mutableListOf<ReferencePathNode>()
366     findResultsInTrie(rootTrieNode, outputPathResults)
367 
368     if (outputPathResults.size != inputPathResults.size) {
369       SharkLog.d {
370         "Found ${inputPathResults.size} paths to retained objects," +
371           " down to ${outputPathResults.size} after removing duplicated paths"
372       }
373     } else {
374       SharkLog.d { "Found ${outputPathResults.size} paths to retained objects" }
375     }
376 
377     return outputPathResults.map { retainedObjectNode ->
378       val shortestChildPath = mutableListOf<ChildNode>()
379       var node = retainedObjectNode
380       while (node is ChildNode) {
381         shortestChildPath.add(0, node)
382         node = node.parent
383       }
384       val rootNode = node as RootNode
385       ShortestPath(rootNode, shortestChildPath)
386     }
387   }
388 
389   private fun updateTrie(
390     pathNode: ReferencePathNode,
391     path: List<Long>,
392     pathIndex: Int,
393     parentNode: ParentNode
394   ) {
395     val objectId = path[pathIndex]
396     if (pathIndex == path.lastIndex) {
397       parentNode.children[objectId] = LeafNode(objectId, pathNode)
398     } else {
399       val childNode = parentNode.children[objectId] ?: run {
400         val newChildNode = ParentNode(objectId)
401         parentNode.children[objectId] = newChildNode
402         newChildNode
403       }
404       if (childNode is ParentNode) {
405         updateTrie(pathNode, path, pathIndex + 1, childNode)
406       }
407     }
408   }
409 
410   private fun findResultsInTrie(
411     parentNode: ParentNode,
412     outputPathResults: MutableList<ReferencePathNode>
413   ) {
414     parentNode.children.values.forEach { childNode ->
415       when (childNode) {
416         is ParentNode -> {
417           findResultsInTrie(childNode, outputPathResults)
418         }
419         is LeafNode -> {
420           outputPathResults += childNode.pathNode
421         }
422       }
423     }
424   }
425 
426   internal class ShortestPath(
427     val root: RootNode,
428     val childPath: List<ChildNode>
429   ) {
430 
431     val childPathWithDetails = childPath.map { it to it.lazyDetailsResolver.resolve() }
432 
433     fun asList() = listOf(root) + childPath
434 
435     fun firstLibraryLeakMatcher(): LibraryLeakReferenceMatcher? {
436       if (root is LibraryLeakRootNode) {
437         return root.matcher
438       }
439       return childPathWithDetails.map { it.second.matchedLibraryLeak }.firstOrNull { it != null }
440     }
441 
442     fun asNodesWithMatchers(): List<Pair<ReferencePathNode, LibraryLeakReferenceMatcher?>> {
443       val rootMatcher = if (root is LibraryLeakRootNode) {
444         root.matcher
445       } else null
446       val childPathWithMatchers =
447         childPathWithDetails.map { it.first to it.second.matchedLibraryLeak }
448       return listOf(root to rootMatcher) + childPathWithMatchers
449     }
450   }
451 
452   private fun FindLeakInput.buildLeakTraces(
453     shortestPaths: List<ShortestPath>,
454     inspectedObjectsByPath: List<List<InspectedObject>>,
455     retainedSizes: Map<Long, Pair<Int, Int>>?
456   ): Pair<List<ApplicationLeak>, List<LibraryLeak>> {
457     listener.onAnalysisProgress(BUILDING_LEAK_TRACES)
458 
459     val applicationLeaksMap = mutableMapOf<String, MutableList<LeakTrace>>()
460     val libraryLeaksMap =
461       mutableMapOf<String, Pair<LibraryLeakReferenceMatcher, MutableList<LeakTrace>>>()
462 
463     shortestPaths.forEachIndexed { pathIndex, shortestPath ->
464       val inspectedObjects = inspectedObjectsByPath[pathIndex]
465 
466       val leakTraceObjects = buildLeakTraceObjects(inspectedObjects, retainedSizes)
467 
468       val referencePath = buildReferencePath(shortestPath, leakTraceObjects)
469 
470       val leakTrace = LeakTrace(
471         gcRootType = GcRootType.fromGcRoot(shortestPath.root.gcRoot),
472         referencePath = referencePath,
473         leakingObject = leakTraceObjects.last()
474       )
475 
476       val firstLibraryLeakMatcher = shortestPath.firstLibraryLeakMatcher()
477       if (firstLibraryLeakMatcher != null) {
478         val signature: String = firstLibraryLeakMatcher.pattern.toString()
479           .createSHA1Hash()
480         libraryLeaksMap.getOrPut(signature) { firstLibraryLeakMatcher to mutableListOf() }
481           .second += leakTrace
482       } else {
483         applicationLeaksMap.getOrPut(leakTrace.signature) { mutableListOf() } += leakTrace
484       }
485     }
486     val applicationLeaks = applicationLeaksMap.map { (_, leakTraces) ->
487       ApplicationLeak(leakTraces)
488     }
489     val libraryLeaks = libraryLeaksMap.map { (_, pair) ->
490       val (matcher, leakTraces) = pair
491       LibraryLeak(leakTraces, matcher.pattern, matcher.description)
492     }
493     return applicationLeaks to libraryLeaks
494   }
495 
496   private fun FindLeakInput.inspectObjects(shortestPaths: List<ShortestPath>): List<List<InspectedObject>> {
497     listener.onAnalysisProgress(INSPECTING_OBJECTS)
498 
499     val leakReportersByPath = shortestPaths.map { path ->
500       val pathList = path.asNodesWithMatchers()
501       pathList
502         .mapIndexed { index, (node, _) ->
503           val reporter = ObjectReporter(heapObject = graph.findObjectById(node.objectId))
504           if (index + 1 < pathList.size) {
505             val (_, nextMatcher) = pathList[index + 1]
506             if (nextMatcher != null) {
507               reporter.labels += "Library leak match: ${nextMatcher.pattern}"
508             }
509           }
510           reporter
511         }
512     }
513 
514     objectInspectors.forEach { inspector ->
515       leakReportersByPath.forEach { leakReporters ->
516         leakReporters.forEach { reporter ->
517           inspector.inspect(reporter)
518         }
519       }
520     }
521 
522     return leakReportersByPath.map { leakReporters ->
523       computeLeakStatuses(leakReporters)
524     }
525   }
526 
527   private fun FindLeakInput.computeRetainedSizes(
528     inspectedObjectsByPath: List<List<InspectedObject>>,
529     dominatorTree: DominatorTree
530   ): Map<Long, Pair<Int, Int>> {
531     val nodeObjectIds = inspectedObjectsByPath.flatMap { inspectedObjects ->
532       inspectedObjects.filter { it.leakingStatus == UNKNOWN || it.leakingStatus == LEAKING }
533         .map { it.heapObject.objectId }
534     }.toSet()
535     listener.onAnalysisProgress(COMPUTING_NATIVE_RETAINED_SIZE)
536     val nativeSizeMapper = AndroidNativeSizeMapper(graph)
537     val nativeSizes = nativeSizeMapper.mapNativeSizes()
538     listener.onAnalysisProgress(COMPUTING_RETAINED_SIZE)
539     val shallowSizeCalculator = ShallowSizeCalculator(graph)
540     return dominatorTree.computeRetainedSizes(nodeObjectIds) { objectId ->
541       val nativeSize = nativeSizes[objectId] ?: 0
542       val shallowSize = shallowSizeCalculator.computeShallowSize(objectId)
543       nativeSize + shallowSize
544     }
545   }
546 
547   private fun buildLeakTraceObjects(
548     inspectedObjects: List<InspectedObject>,
549     retainedSizes: Map<Long, Pair<Int, Int>>?
550   ): List<LeakTraceObject> {
551     return inspectedObjects.map { inspectedObject ->
552       val heapObject = inspectedObject.heapObject
553       val className = recordClassName(heapObject)
554 
555       val objectType = when (heapObject) {
556         is HeapClass -> CLASS
557         is HeapObjectArray, is HeapPrimitiveArray -> ARRAY
558         else -> INSTANCE
559       }
560 
561       val retainedSizeAndObjectCount = retainedSizes?.get(inspectedObject.heapObject.objectId)
562 
563       LeakTraceObject(
564         type = objectType,
565         className = className,
566         labels = inspectedObject.labels,
567         leakingStatus = inspectedObject.leakingStatus,
568         leakingStatusReason = inspectedObject.leakingStatusReason,
569         retainedHeapByteSize = retainedSizeAndObjectCount?.first,
570         retainedObjectCount = retainedSizeAndObjectCount?.second
571       )
572     }
573   }
574 
575   private fun FindLeakInput.buildReferencePath(
576     shortestPath: ShortestPath,
577     leakTraceObjects: List<LeakTraceObject>
578   ): List<LeakTraceReference> {
579     return shortestPath.childPathWithDetails.mapIndexed { index, (childNode, details) ->
580       LeakTraceReference(
581         originObject = leakTraceObjects[index],
582         referenceType = when (details.locationType) {
583           ReferenceLocationType.INSTANCE_FIELD -> LeakTraceReference.ReferenceType.INSTANCE_FIELD
584           ReferenceLocationType.STATIC_FIELD -> LeakTraceReference.ReferenceType.STATIC_FIELD
585           ReferenceLocationType.LOCAL -> LeakTraceReference.ReferenceType.LOCAL
586           ReferenceLocationType.ARRAY_ENTRY -> LeakTraceReference.ReferenceType.ARRAY_ENTRY
587         },
588         owningClassName = graph.findObjectById(details.locationClassObjectId).asClass!!.name,
589         referenceName = details.name
590       )
591     }
592   }
593 
594   internal class InspectedObject(
595     val heapObject: HeapObject,
596     val leakingStatus: LeakingStatus,
597     val leakingStatusReason: String,
598     val labels: MutableSet<String>
599   )
600 
601   private fun computeLeakStatuses(leakReporters: List<ObjectReporter>): List<InspectedObject> {
602     val lastElementIndex = leakReporters.size - 1
603 
604     var lastNotLeakingElementIndex = -1
605     var firstLeakingElementIndex = lastElementIndex
606 
607     val leakStatuses = ArrayList<Pair<LeakingStatus, String>>()
608 
609     for ((index, reporter) in leakReporters.withIndex()) {
610       val resolvedStatusPair =
611         resolveStatus(reporter, leakingWins = index == lastElementIndex).let { statusPair ->
612           if (index == lastElementIndex) {
613             // The last element should always be leaking.
614             when (statusPair.first) {
615               LEAKING -> statusPair
616               UNKNOWN -> LEAKING to "This is the leaking object"
617               NOT_LEAKING -> LEAKING to "This is the leaking object. Conflicts with ${statusPair.second}"
618             }
619           } else statusPair
620         }
621 
622       leakStatuses.add(resolvedStatusPair)
623       val (leakStatus, _) = resolvedStatusPair
624       if (leakStatus == NOT_LEAKING) {
625         lastNotLeakingElementIndex = index
626         // Reset firstLeakingElementIndex so that we never have
627         // firstLeakingElementIndex < lastNotLeakingElementIndex
628         firstLeakingElementIndex = lastElementIndex
629       } else if (leakStatus == LEAKING && firstLeakingElementIndex == lastElementIndex) {
630         firstLeakingElementIndex = index
631       }
632     }
633 
634     val simpleClassNames = leakReporters.map { reporter ->
635       recordClassName(reporter.heapObject).lastSegment('.')
636     }
637 
638     for (i in 0 until lastNotLeakingElementIndex) {
639       val (leakStatus, leakStatusReason) = leakStatuses[i]
640       val nextNotLeakingIndex = generateSequence(i + 1) { index ->
641         if (index < lastNotLeakingElementIndex) index + 1 else null
642       }.first { index ->
643         leakStatuses[index].first == NOT_LEAKING
644       }
645 
646       // Element is forced to NOT_LEAKING
647       val nextNotLeakingName = simpleClassNames[nextNotLeakingIndex]
648       leakStatuses[i] = when (leakStatus) {
649         UNKNOWN -> NOT_LEAKING to "$nextNotLeakingName↓ is not leaking"
650         NOT_LEAKING -> NOT_LEAKING to "$nextNotLeakingName↓ is not leaking and $leakStatusReason"
651         LEAKING -> NOT_LEAKING to "$nextNotLeakingName↓ is not leaking. Conflicts with $leakStatusReason"
652       }
653     }
654 
655     if (firstLeakingElementIndex < lastElementIndex - 1) {
656       // We already know the status of firstLeakingElementIndex and lastElementIndex
657       for (i in lastElementIndex - 1 downTo firstLeakingElementIndex + 1) {
658         val (leakStatus, leakStatusReason) = leakStatuses[i]
659         val previousLeakingIndex = generateSequence(i - 1) { index ->
660           if (index > firstLeakingElementIndex) index - 1 else null
661         }.first { index ->
662           leakStatuses[index].first == LEAKING
663         }
664 
665         // Element is forced to LEAKING
666         val previousLeakingName = simpleClassNames[previousLeakingIndex]
667         leakStatuses[i] = when (leakStatus) {
668           UNKNOWN -> LEAKING to "$previousLeakingName↑ is leaking"
669           LEAKING -> LEAKING to "$previousLeakingName↑ is leaking and $leakStatusReason"
670           NOT_LEAKING -> throw IllegalStateException("Should never happen")
671         }
672       }
673     }
674 
675     return leakReporters.mapIndexed { index, objectReporter ->
676       val (leakingStatus, leakingStatusReason) = leakStatuses[index]
677       InspectedObject(
678         objectReporter.heapObject, leakingStatus, leakingStatusReason, objectReporter.labels
679       )
680     }
681   }
682 
683   private fun resolveStatus(
684     reporter: ObjectReporter,
685     leakingWins: Boolean
686   ): Pair<LeakingStatus, String> {
687     var status = UNKNOWN
688     var reason = ""
689     if (reporter.notLeakingReasons.isNotEmpty()) {
690       status = NOT_LEAKING
691       reason = reporter.notLeakingReasons.joinToString(" and ")
692     }
693     val leakingReasons = reporter.leakingReasons
694     if (leakingReasons.isNotEmpty()) {
695       val winReasons = leakingReasons.joinToString(" and ")
696       // Conflict
697       if (status == NOT_LEAKING) {
698         if (leakingWins) {
699           status = LEAKING
700           reason = "$winReasons. Conflicts with $reason"
701         } else {
702           reason += ". Conflicts with $winReasons"
703         }
704       } else {
705         status = LEAKING
706         reason = winReasons
707       }
708     }
709     return status to reason
710   }
711 
712   private fun recordClassName(
713     heap: HeapObject
714   ): String {
715     return when (heap) {
716       is HeapClass -> heap.name
717       is HeapInstance -> heap.instanceClassName
718       is HeapObjectArray -> heap.arrayClassName
719       is HeapPrimitiveArray -> heap.arrayClassName
720     }
721   }
722 
723   private fun since(analysisStartNanoTime: Long): Long {
724     return NANOSECONDS.toMillis(System.nanoTime() - analysisStartNanoTime)
725   }
726 }
727