All Downloads are FREE. Search and download functionalities are using the official Maven repository.

kshark.internal.PathFinder.kt Maven / Gradle / Ivy

@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
package kshark.internal

import kshark.GcRoot
import kshark.GcRoot.JavaFrame
import kshark.GcRoot.JniGlobal
import kshark.GcRoot.ThreadObject
import kshark.HeapGraph
import kshark.HeapObject
import kshark.HeapObject.HeapClass
import kshark.HeapObject.HeapInstance
import kshark.HeapObject.HeapObjectArray
import kshark.HeapObject.HeapPrimitiveArray
import kshark.HprofRecord.HeapDumpRecord.ObjectRecord.ClassDumpRecord.FieldRecord
import kshark.IgnoredReferenceMatcher
import kshark.LeakTraceReference.ReferenceType.ARRAY_ENTRY
import kshark.LeakTraceReference.ReferenceType.INSTANCE_FIELD
import kshark.LeakTraceReference.ReferenceType.LOCAL
import kshark.LeakTraceReference.ReferenceType.STATIC_FIELD
import kshark.LibraryLeakReferenceMatcher
import kshark.OnAnalysisProgressListener
import kshark.OnAnalysisProgressListener.Step.FINDING_DOMINATORS
import kshark.OnAnalysisProgressListener.Step.FINDING_PATHS_TO_RETAINED_OBJECTS
import kshark.PrimitiveType
import kshark.PrimitiveType.BOOLEAN
import kshark.PrimitiveType.BYTE
import kshark.PrimitiveType.CHAR
import kshark.PrimitiveType.DOUBLE
import kshark.PrimitiveType.FLOAT
import kshark.PrimitiveType.INT
import kshark.PrimitiveType.LONG
import kshark.PrimitiveType.SHORT
import kshark.ReferenceMatcher
import kshark.ReferencePattern
import kshark.ReferencePattern.InstanceFieldPattern
import kshark.ReferencePattern.NativeGlobalVariablePattern
import kshark.ReferencePattern.StaticFieldPattern
import kshark.ValueHolder
import kshark.ValueHolder.ReferenceHolder
import kshark.internal.PathFinder.VisitTracker.Dominated
import kshark.internal.PathFinder.VisitTracker.Visited
import kshark.internal.ReferencePathNode.ChildNode
import kshark.internal.ReferencePathNode.ChildNode.LibraryLeakChildNode
import kshark.internal.ReferencePathNode.ChildNode.NormalNode
import kshark.internal.ReferencePathNode.LibraryLeakNode
import kshark.internal.ReferencePathNode.RootNode
import kshark.internal.ReferencePathNode.RootNode.LibraryLeakRootNode
import kshark.internal.ReferencePathNode.RootNode.NormalRootNode
import kshark.internal.hppc.LongScatterSet
import java.util.ArrayDeque
import java.util.Deque
import java.util.LinkedHashMap

/**
 * Not thread safe.
 *
 * Finds the shortest path from leaking references to a gc root, first ignoring references
 * identified as "to visit last" and then visiting them as needed if no path is
 * found.
 */
internal class PathFinder(
  private val graph: HeapGraph,
  private val listener: OnAnalysisProgressListener,
  referenceMatchers: List
) {

  class PathFindingResults(
    val pathsToLeakingObjects: List,
    val dominatorTree: DominatorTree?
  )

  sealed class VisitTracker {

    abstract fun visited(
      objectId: Long,
      parentObjectId: Long
    ): Boolean

    class Dominated(expectedElements: Int) : VisitTracker() {
      /**
       * Tracks visited objecs and their dominator.
       * If an object is not in [dominatorTree] then it hasn't been enqueued yet.
       * If an object is in [dominatorTree] but not in [State.toVisitSet] nor [State.toVisitLastSet]
       * then it has already been dequeued.
       *
       * If an object is dominated by more than one GC root then its dominator is set to
       * [ValueHolder.NULL_REFERENCE].
       */
      val dominatorTree = DominatorTree(expectedElements)
      override fun visited(
        objectId: Long,
        parentObjectId: Long
      ): Boolean {
        return dominatorTree.updateDominated(objectId, parentObjectId)
      }
    }

    class Visited(expectedElements: Int) : VisitTracker() {
      /**
       * Set of visited objects.
       */
      private val visitedSet = LongScatterSet(expectedElements)
      override fun visited(
        objectId: Long,
        parentObjectId: Long
      ): Boolean {
        return !visitedSet.add(objectId)
      }
    }
  }

  private class State(
    val leakingObjectIds: LongScatterSet,
    val sizeOfObjectInstances: Int,
    val computeRetainedHeapSize: Boolean,
    val javaLangObjectId: Long,
    estimatedVisitedObjects: Int
  ) {

    /** Set of objects to visit */
    val toVisitQueue: Deque = ArrayDeque()

    /**
     * Objects to visit when [toVisitQueue] is empty. Should contain [JavaFrame] gc roots first,
     * then [LibraryLeakNode].
     */
    val toVisitLastQueue: Deque = ArrayDeque()

    /**
     * Enables fast checking of whether a node is already in the queue.
     */
    val toVisitSet = LongScatterSet()
    val toVisitLastSet = LongScatterSet()

    val queuesNotEmpty: Boolean
      get() = toVisitQueue.isNotEmpty() || toVisitLastQueue.isNotEmpty()

    val visitTracker = if (computeRetainedHeapSize) {
      Dominated(estimatedVisitedObjects)
    } else {
      Visited(estimatedVisitedObjects)
    }

    /**
     * A marker for when we're done exploring the graph of higher priority references and start
     * visiting the lower priority references, at which point we won't add any reference to
     * the high priority queue anymore.
     */
    var visitingLast = false
  }

  private val fieldNameByClassName: Map>
  private val staticFieldNameByClassName: Map>
  private val threadNameReferenceMatchers: Map
  private val jniGlobalReferenceMatchers: Map

  init {
    val fieldNameByClassName = mutableMapOf>()
    val staticFieldNameByClassName = mutableMapOf>()
    val threadNames = mutableMapOf()
    val jniGlobals = mutableMapOf()

    val appliedRefMatchers = referenceMatchers.filter {
      (it is IgnoredReferenceMatcher || (it is LibraryLeakReferenceMatcher && it.patternApplies(
        graph
      )))
    }

    appliedRefMatchers.forEach { referenceMatcher ->
      when (val pattern = referenceMatcher.pattern) {
        is ReferencePattern.JavaLocalPattern -> {
          threadNames[pattern.threadName] = referenceMatcher
        }
        is StaticFieldPattern -> {
          val mapOrNull = staticFieldNameByClassName[pattern.className]
          val map = if (mapOrNull != null) mapOrNull else {
            val newMap = mutableMapOf()
            staticFieldNameByClassName[pattern.className] = newMap
            newMap
          }
          map[pattern.fieldName] = referenceMatcher
        }
        is InstanceFieldPattern -> {
          val mapOrNull = fieldNameByClassName[pattern.className]
          val map = if (mapOrNull != null) mapOrNull else {
            val newMap = mutableMapOf()
            fieldNameByClassName[pattern.className] = newMap
            newMap
          }
          map[pattern.fieldName] = referenceMatcher
        }
        is NativeGlobalVariablePattern -> {
          jniGlobals[pattern.className] = referenceMatcher
        }
      }
    }
    this.fieldNameByClassName = fieldNameByClassName
    this.staticFieldNameByClassName = staticFieldNameByClassName
    this.threadNameReferenceMatchers = threadNames
    this.jniGlobalReferenceMatchers = jniGlobals
  }

  fun findPathsFromGcRoots(
    leakingObjectIds: Set,
    computeRetainedHeapSize: Boolean
  ): PathFindingResults {
    listener.onAnalysisProgress(FINDING_PATHS_TO_RETAINED_OBJECTS)

    val objectClass = graph.findClassByName("java.lang.Object")
    val sizeOfObjectInstances = determineSizeOfObjectInstances(objectClass, graph)
    val javaLangObjectId = objectClass?.objectId ?: -1

    // Estimate of how many objects we'll visit. This is a conservative estimate, we should always
    // visit more than that but this limits the number of early array growths.
    val estimatedVisitedObjects = (graph.instanceCount / 2).coerceAtLeast(4)

    val state = State(
      leakingObjectIds = leakingObjectIds.toLongScatterSet(),
      sizeOfObjectInstances = sizeOfObjectInstances,
      computeRetainedHeapSize = computeRetainedHeapSize,
      javaLangObjectId = javaLangObjectId,
      estimatedVisitedObjects = estimatedVisitedObjects
    )

    return state.findPathsFromGcRoots()
  }

  private fun determineSizeOfObjectInstances(
    objectClass: HeapClass?,
    graph: HeapGraph
  ): Int {
    return if (objectClass != null) {
      // In Android 16 ClassDumpRecord.instanceSize for java.lang.Object can be 8 yet there are 0
      // fields. This is likely because there is extra per instance data that isn't coming from
      // fields in the Object class. See #1374
      val objectClassFieldSize = objectClass.readFieldsByteSize()

      // shadow$_klass_ (object id) + shadow$_monitor_ (Int)
      val sizeOfObjectOnArt = graph.identifierByteSize + INT.byteSize
      if (objectClassFieldSize == sizeOfObjectOnArt) {
        sizeOfObjectOnArt
      } else {
        0
      }
    } else {
      0
    }
  }

  private fun Set.toLongScatterSet(): LongScatterSet {
    val longScatterSet = LongScatterSet()
    longScatterSet.ensureCapacity(size)
    forEach { longScatterSet.add(it) }
    return longScatterSet
  }

  private fun State.findPathsFromGcRoots(): PathFindingResults {
    enqueueGcRoots()

    val shortestPathsToLeakingObjects = mutableListOf()
    visitingQueue@ while (queuesNotEmpty) {
      val node = poll()
      if (leakingObjectIds.contains(node.objectId)) {
        shortestPathsToLeakingObjects.add(node)
        // Found all refs, stop searching (unless computing retained size)
        if (shortestPathsToLeakingObjects.size == leakingObjectIds.size()) {
          if (computeRetainedHeapSize) {
            listener.onAnalysisProgress(FINDING_DOMINATORS)
          } else {
            break@visitingQueue
          }
        }
      }

      when (val heapObject = graph.findObjectById(node.objectId)) {
        is HeapClass -> visitClassRecord(heapObject, node)
        is HeapInstance -> visitInstance(heapObject, node)
        is HeapObjectArray -> visitObjectArray(heapObject, node)
      }
    }
    return PathFindingResults(
      shortestPathsToLeakingObjects,
      if (visitTracker is Dominated) visitTracker.dominatorTree else null
    )
  }

  private fun State.poll(): ReferencePathNode {
    return if (!visitingLast && !toVisitQueue.isEmpty()) {
      val removedNode = toVisitQueue.poll()
      toVisitSet.remove(removedNode.objectId)
      removedNode
    } else {
      visitingLast = true
      val removedNode = toVisitLastQueue.poll()
      toVisitLastSet.remove(removedNode.objectId)
      removedNode
    }
  }

  private fun State.enqueueGcRoots() {
    val gcRoots = sortedGcRoots()

    val threadNames = mutableMapOf()
    val threadsBySerialNumber = mutableMapOf>()
    gcRoots.forEach { (objectRecord, gcRoot) ->
      when (gcRoot) {
        is ThreadObject -> {
          threadsBySerialNumber[gcRoot.threadSerialNumber] = objectRecord.asInstance!! to gcRoot
          enqueue(NormalRootNode(gcRoot.id, gcRoot))
        }
        is JavaFrame -> {
          val threadPair = threadsBySerialNumber[gcRoot.threadSerialNumber]
          if (threadPair == null) {
            // Could not find the thread that this java frame is for.
            enqueue(NormalRootNode(gcRoot.id, gcRoot))
          } else {
            val (threadInstance, threadRoot) = threadPair
            val threadName = threadNames[threadInstance] ?: {
              val name = threadInstance[Thread::class, "name"]?.value?.readAsJavaString() ?: ""
              threadNames[threadInstance] = name
              name
            }()
            val referenceMatcher = threadNameReferenceMatchers[threadName]

            if (referenceMatcher !is IgnoredReferenceMatcher) {
              val rootNode = NormalRootNode(threadRoot.id, gcRoot)

              val refFromParentType = LOCAL
              // Unfortunately Android heap dumps do not include stack trace data, so
              // JavaFrame.frameNumber is always -1 and we cannot know which method is causing the
              // reference to be held.
              val refFromParentName = ""

              val childNode = if (referenceMatcher is LibraryLeakReferenceMatcher) {
                LibraryLeakChildNode(
                  objectId = gcRoot.id,
                  parent = rootNode,
                  refFromParentType = refFromParentType,
                  refFromParentName = refFromParentName,
                  matcher = referenceMatcher
                )
              } else {
                NormalNode(
                  objectId = gcRoot.id,
                  parent = rootNode,
                  refFromParentType = refFromParentType,
                  refFromParentName = refFromParentName
                )
              }
              enqueue(childNode)
            }
          }
        }
        is JniGlobal -> {
          val referenceMatcher = when (objectRecord) {
            is HeapClass -> jniGlobalReferenceMatchers[objectRecord.name]
            is HeapInstance -> jniGlobalReferenceMatchers[objectRecord.instanceClassName]
            is HeapObjectArray -> jniGlobalReferenceMatchers[objectRecord.arrayClassName]
            is HeapPrimitiveArray -> jniGlobalReferenceMatchers[objectRecord.arrayClassName]
          }
          if (referenceMatcher !is IgnoredReferenceMatcher) {
            if (referenceMatcher is LibraryLeakReferenceMatcher) {
              enqueue(LibraryLeakRootNode(gcRoot.id, gcRoot, referenceMatcher))
            } else {
              enqueue(NormalRootNode(gcRoot.id, gcRoot))
            }
          }
        }
        else -> enqueue(NormalRootNode(gcRoot.id, gcRoot))
      }
    }
  }

  /**
   * Sorting GC roots to get stable shortest path
   * Once sorted all ThreadObject Gc Roots are located before JavaLocalPattern Gc Roots.
   * This ensures ThreadObjects are visited before JavaFrames, and threadsBySerialNumber can be
   * built before JavaFrames.
   */
  private fun sortedGcRoots(): List> {
    val rootClassName: (HeapObject) -> String = { graphObject ->
      when (graphObject) {
        is HeapClass -> {
          graphObject.name
        }
        is HeapInstance -> {
          graphObject.instanceClassName
        }
        is HeapObjectArray -> {
          graphObject.arrayClassName
        }
        is HeapPrimitiveArray -> {
          graphObject.arrayClassName
        }
      }
    }

    return graph.gcRoots
      .filter { gcRoot ->
        // GC roots sometimes reference objects that don't exist in the heap dump
        // See https://github.com/square/leakcanary/issues/1516
        graph.objectExists(gcRoot.id)
      }
      .map { graph.findObjectById(it.id) to it }
      .sortedWith(Comparator { (graphObject1, root1), (graphObject2, root2) ->
        // Sorting based on pattern name first. In reverse order so that ThreadObject is before JavaLocalPattern
        val gcRootTypeComparison = root2::class.java.name.compareTo(root1::class.java.name)
        if (gcRootTypeComparison != 0) {
          gcRootTypeComparison
        } else {
          rootClassName(graphObject1).compareTo(rootClassName(graphObject2))
        }
      })
  }

  private fun State.visitClassRecord(
    heapClass: HeapClass,
    parent: ReferencePathNode
  ) {
    val ignoredStaticFields = staticFieldNameByClassName[heapClass.name] ?: emptyMap()

    for (staticField in heapClass.readStaticFields()) {
      if (!staticField.value.isNonNullReference) {
        continue
      }

      val fieldName = staticField.name
      if (fieldName == "\$staticOverhead" || fieldName == "\$classOverhead") {
        continue
      }

      // Note: instead of calling staticField.value.asObjectId!! we cast holder to ReferenceHolder
      // and access value directly. This allows us to avoid unnecessary boxing of Long.
      val objectId = (staticField.value.holder as ReferenceHolder).value

      val node = when (val referenceMatcher = ignoredStaticFields[fieldName]) {
        null -> NormalNode(
          objectId = objectId,
          parent = parent,
          refFromParentType = STATIC_FIELD,
          refFromParentName = fieldName
        )
        is LibraryLeakReferenceMatcher -> LibraryLeakChildNode(
          objectId = objectId,
          parent = parent,
          refFromParentType = STATIC_FIELD,
          refFromParentName = fieldName,
          matcher = referenceMatcher
        )
        is IgnoredReferenceMatcher -> null
      }
      if (node != null) {
        enqueue(node)
      }
    }
  }

  private fun State.visitInstance(
    instance: HeapInstance,
    parent: ReferencePathNode
  ) {
    val fieldReferenceMatchers = LinkedHashMap()

    instance.instanceClass.classHierarchy.forEach {
      val referenceMatcherByField = fieldNameByClassName[it.name]
      if (referenceMatcherByField != null) {
        for ((fieldName, referenceMatcher) in referenceMatcherByField) {
          if (!fieldReferenceMatchers.containsKey(fieldName)) {
            fieldReferenceMatchers[fieldName] = referenceMatcher
          }
        }
      }
    }
    val classHierarchy =
      instance.instanceClass.classHierarchyWithoutJavaLangObject(javaLangObjectId)
    val fieldNamesAndValues =
      instance.readAllNonNullFieldsOfReferenceType(classHierarchy)

    fieldNamesAndValues.sortBy { it.fieldName }

    fieldNamesAndValues.forEach { instanceRefField ->
      val node = when (val referenceMatcher = fieldReferenceMatchers[instanceRefField.fieldName]) {
        null -> NormalNode(
          objectId = instanceRefField.refObjectId,
          parent = parent,
          refFromParentType = INSTANCE_FIELD,
          refFromParentName = instanceRefField.fieldName,
          owningClassId = instanceRefField.declaringClassId
        )
        is LibraryLeakReferenceMatcher ->
          LibraryLeakChildNode(
            objectId = instanceRefField.refObjectId,
            parent = parent,
            refFromParentType = INSTANCE_FIELD,
            refFromParentName = instanceRefField.fieldName,
            matcher = referenceMatcher,
            owningClassId = instanceRefField.declaringClassId
          )
        is IgnoredReferenceMatcher -> null
      }
      if (node != null) {
        enqueue(node)
      }
    }
  }

  private class InstanceRefField(
    val declaringClassId: Long,
    val refObjectId: Long,
    val fieldName: String
  )

  private fun HeapInstance.readAllNonNullFieldsOfReferenceType(
    classHierarchy: List
  ): MutableList {
    // Assigning to local variable to avoid repeated lookup and cast:
    // HeapInstance.graph casts HeapInstance.hprofGraph to HeapGraph in its getter
    val hprofGraph = graph
    var fieldReader: FieldIdReader? = null
    val result = mutableListOf()
    var skipBytesCount = 0

    for (heapClass in classHierarchy) {
      for (fieldRecord in heapClass.readRecordFields()) {
        if (fieldRecord.type != PrimitiveType.REFERENCE_HPROF_TYPE) {
          // Skip all fields that are not references. Track how many bytes to skip
          skipBytesCount += hprofGraph.getRecordSize(fieldRecord)
        } else {
          // Initialize id reader if it's not yet initialized. Replaces `lazy` without synchronization
          if (fieldReader == null) {
            fieldReader = FieldIdReader(readRecord(), hprofGraph.identifierByteSize)
          }

          // Skip the accumulated bytes offset
          fieldReader.skipBytes(skipBytesCount)
          skipBytesCount = 0

          val objectId = fieldReader.readId()
          if (objectId != 0L) {
            result.add(
              InstanceRefField(
                heapClass.objectId, objectId, heapClass.instanceFieldName(fieldRecord)
              )
            )
          }
        }
      }
    }
    return result
  }

  /**
   * Returns class hierarchy for an instance, but without it's root element, which is always
   * java.lang.Object.
   * Why do we want class hierarchy without java.lang.Object?
   * In pre-M there were no ref fields in java.lang.Object; and FieldIdReader wouldn't be created
   * Android M added shadow$_klass_ reference to class, so now it triggers extra record read.
   * Solution: skip heap class for java.lang.Object completely when reading the records
   * @param javaLangObjectId ID of the java.lang.Object to run comparison against
   */
  private fun HeapClass.classHierarchyWithoutJavaLangObject(
    javaLangObjectId: Long
  ): List {
    val result = mutableListOf()
    var parent: HeapClass? = this
    while (parent != null && parent.objectId != javaLangObjectId) {
      result += parent
      parent = parent.superclass
    }
    return result
  }

  private fun HeapGraph.getRecordSize(field: FieldRecord) =
    when (field.type) {
      PrimitiveType.REFERENCE_HPROF_TYPE -> identifierByteSize
      BOOLEAN.hprofType -> 1
      CHAR.hprofType -> 2
      FLOAT.hprofType -> 4
      DOUBLE.hprofType -> 8
      BYTE.hprofType -> 1
      SHORT.hprofType -> 2
      INT.hprofType -> 4
      LONG.hprofType -> 8
      else -> throw IllegalStateException("Unknown type ${field.type}")
    }

  private fun State.visitObjectArray(
    objectArray: HeapObjectArray,
    parent: ReferencePathNode
  ) {
    val record = objectArray.readRecord()
    val nonNullElementIds = record.elementIds.filter { objectId ->
      objectId != ValueHolder.NULL_REFERENCE && graph.objectExists(objectId)
    }
    nonNullElementIds.forEachIndexed { index, elementId ->
      val name = index.toString()
      enqueue(
        NormalNode(
          objectId = elementId,
          parent = parent,
          refFromParentType = ARRAY_ENTRY,
          refFromParentName = name
        )
      )
    }
  }

  //Added by Kwai.
  //Using a pruning method to optimize performance by add threshold
  // of same class instance's enqueue times.
  private val SAME_INSTANCE_THRESHOLD = 1024
  private var instanceCountMap = mutableMapOf()
  private fun isOverThresholdInstance(graphObject: HeapInstance): Boolean {
    if (graphObject.instanceClassName.startsWith("java.util")
            || graphObject.instanceClassName.startsWith("android.util")
            || graphObject.instanceClassName.startsWith("java.lang.String")
    //|| graphObject.instanceClassName.startsWith("kotlin.collections")
    //|| graphObject.instanceClassName.startsWith("com.google.common")
    )
      return false
    var count: Short? = instanceCountMap[graphObject.instanceClassId]
    if (count == null) count = 0
    if (count < SAME_INSTANCE_THRESHOLD) {
      instanceCountMap[graphObject.instanceClassId] = count.plus(1).toShort()
    }
    return count >= SAME_INSTANCE_THRESHOLD
  }


  @Suppress("ReturnCount")
  private fun State.enqueue(
    node: ReferencePathNode
  ) {
    if (node.objectId == ValueHolder.NULL_REFERENCE) {
      return
    }

    val visitLast =
      visitingLast ||
        node is LibraryLeakNode ||
        // We deprioritize thread objects because on Lollipop the thread local values are stored
        // as a field.
        (node is RootNode && node.gcRoot is ThreadObject) ||
        (node is NormalNode && node.parent is RootNode && node.parent.gcRoot is JavaFrame)

    val parentObjectId = if (node is RootNode) {
      ValueHolder.NULL_REFERENCE
    } else {
      (node as ChildNode).parent.objectId
    }

    val alreadyEnqueued = visitTracker.visited(node.objectId, parentObjectId)

    if (alreadyEnqueued) {
      // Has already been enqueued and would be added to visit last => don't enqueue.
      if (visitLast) {
        return
      }
      // Has already been enqueued and exists in the to visit set => don't enqueue
      if (toVisitSet.contains(node.objectId)) {
        return
      }
      // Has already been enqueued, is not in toVisitSet, is not in toVisitLast => has been visited
      if (!toVisitLastSet.contains(node.objectId)) {
        return
      }
    }

    // Because of the checks and return statements right before, from this point on, if
    // alreadyEnqueued then it's currently enqueued in the visit last set.
    if (alreadyEnqueued) {
      // Move from "visit last" to "visit first" queue.
      toVisitQueue.add(node)
      toVisitSet.add(node.objectId)
      val nodeToRemove = toVisitLastQueue.first { it.objectId == node.objectId }
      toVisitLastQueue.remove(nodeToRemove)
      toVisitLastSet.remove(node.objectId)
      return
    }

    val isLeakingObject = leakingObjectIds.contains(node.objectId)

    if (!isLeakingObject) {
      val skip = when (val graphObject = graph.findObjectById(node.objectId)) {
        is HeapClass -> false
        is HeapInstance ->
          when {
            graphObject.isPrimitiveWrapper -> true
            graphObject.instanceClassName == "java.lang.String" -> {
              // We ignore the fact that String references a value array to avoid having
              // to read the string record and find the object id for that array, since we know
              // it won't be interesting anyway.
              // That also means the value array isn't added to the dominator tree, so we need to
              // add that back when computing shallow size in ShallowSizeCalculator.
              // Another side effect is that if the array is referenced elsewhere, we might
              // double count its side.
              true
            }
            graphObject.instanceClass.instanceByteSize <= sizeOfObjectInstances -> true
            graphObject.instanceClass.classHierarchy.all { heapClass ->
              heapClass.objectId == javaLangObjectId || !heapClass.hasReferenceInstanceFields
            } -> true
            //Added by Kwai.
            //comment this when String leak are necessary to report.
            //graphObject.instanceClassName == "java.lang.String" -> true
            isOverThresholdInstance(graphObject) -> true
            else -> false
          }
        is HeapObjectArray -> when {
          graphObject.isSkippablePrimitiveWrapperArray -> {
            // Same optimization as we did for String above, as we know primitive wrapper arrays
            // aren't interesting.
            true
          }
          else -> false
        }
        is HeapPrimitiveArray -> true
      }
      if (skip) {
        return
      }
    }
    if (visitLast) {
      toVisitLastQueue.add(node)
      toVisitLastSet.add(node.objectId)
    } else {
      toVisitQueue.add(node)
      toVisitSet.add(node.objectId)
    }
  }
}

private val skippablePrimitiveWrapperArrayTypes = setOf(
  Boolean::class,
  Char::class,
  Float::class,
  Double::class,
  Byte::class,
  Short::class,
  Int::class,
  Long::class
).map { it.javaObjectType.name + "[]" }

internal val HeapObjectArray.isSkippablePrimitiveWrapperArray: Boolean
  get() = arrayClassName in skippablePrimitiveWrapperArrayTypes





© 2015 - 2025 Weber Informatics LLC | Privacy Policy