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