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

kshark.HeapAnalysis.kt Maven / Gradle / Ivy

package kshark

import kshark.internal.createSHA1Hash
import java.io.File
import java.io.Serializable

/**
 * The result of an analysis performed by [HeapAnalyzer], either a [HeapAnalysisSuccess] or a
 * [HeapAnalysisFailure]. This class is serializable however there are no guarantees of forward
 * compatibility.
 */
sealed class HeapAnalysis : Serializable {
  /**
   * The hprof file that was analyzed.
   */
  abstract val heapDumpFile: File

  /**
   * The [System.currentTimeMillis] when this [HeapAnalysis] instance was created.
   */
  abstract val createdAtTimeMillis: Long

  /**
   * Total time spent dumping the heap.
   */
  abstract val dumpDurationMillis: Long

  /**
   * Total time spent analyzing the heap.
   */
  abstract val analysisDurationMillis: Long

  companion object {
    private const val serialVersionUID: Long = -8657286725869987172
    const val DUMP_DURATION_UNKNOWN: Long = -1
  }
}

/**
 * The analysis performed by [HeapAnalyzer] did not complete successfully.
 */
data class HeapAnalysisFailure(
  override val heapDumpFile: File,
  override val createdAtTimeMillis: Long,
  override val dumpDurationMillis: Long = DUMP_DURATION_UNKNOWN,
  override val analysisDurationMillis: Long,
  /**
   * An exception wrapping the actual exception that was thrown.
   */
  val exception: HeapAnalysisException
) : HeapAnalysis() {

  override fun toString(): String {
    return """====================================
HEAP ANALYSIS FAILED

You can report this failure at https://github.com/square/leakcanary/issues
Please provide the stacktrace, metadata and the heap dump file.
====================================
STACKTRACE

$exception====================================
METADATA

Build.VERSION.SDK_INT: ${androidSdkInt()}
Build.MANUFACTURER: ${androidManufacturer()}
LeakCanary version: ${leakCanaryVersion()}
Analysis duration: $analysisDurationMillis ms
Heap dump file path: ${heapDumpFile.absolutePath}
Heap dump timestamp: $createdAtTimeMillis
===================================="""
  }

  companion object {
    private const val serialVersionUID: Long = 8483254400637792414
  }
}

/**
 * The result of a successful heap analysis performed by [HeapAnalyzer].
 */
data class HeapAnalysisSuccess(
  override val heapDumpFile: File,
  override val createdAtTimeMillis: Long,
  override val dumpDurationMillis: Long = DUMP_DURATION_UNKNOWN,
  override val analysisDurationMillis: Long,
  val metadata: Map,
  /**
   * The list of [ApplicationLeak] found in the heap dump by [HeapAnalyzer].
   */
  val applicationLeaks: List,
  /**
   * The list of [LibraryLeak] found in the heap dump by [HeapAnalyzer].
   */
  val libraryLeaks: List,
  val unreachableObjects: List
) : HeapAnalysis() {
  /**
   * The list of [Leak] found in the heap dump by [HeapAnalyzer], ie all [applicationLeaks] and
   * all [libraryLeaks] in one list.
   */
  val allLeaks: Sequence
    get() = applicationLeaks.asSequence() + libraryLeaks.asSequence()

  override fun toString(): String {
    return """====================================
HEAP ANALYSIS RESULT
====================================
${applicationLeaks.size} APPLICATION LEAKS

References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.
${
      if (applicationLeaks.isNotEmpty()) "\n" + applicationLeaks.joinToString(
        "\n\n"
      ) + "\n" else ""
    }====================================
${libraryLeaks.size} LIBRARY LEAKS

A Library Leak is a leak caused by a known bug in 3rd party code that you do not have control over.
See https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#4-categorizing-leaks
${
      if (libraryLeaks.isNotEmpty()) "\n" + libraryLeaks.joinToString(
        "\n\n"
      ) + "\n" else ""
    }====================================
${unreachableObjects.size} UNREACHABLE OBJECTS

An unreachable object is still in memory but LeakCanary could not find a strong reference path
from GC roots.
${
      if (unreachableObjects.isNotEmpty()) "\n" + unreachableObjects.joinToString(
        "\n\n"
      ) + "\n" else ""
    }====================================
METADATA

Please include this in bug reports and Stack Overflow questions.
${
      if (metadata.isNotEmpty()) "\n" + metadata.map { "${it.key}: ${it.value}" }.joinToString(
        "\n"
      ) else ""
    }
Analysis duration: $analysisDurationMillis ms
Heap dump file path: ${heapDumpFile.absolutePath}
Heap dump timestamp: $createdAtTimeMillis
Heap dump duration: ${if (dumpDurationMillis != DUMP_DURATION_UNKNOWN) "$dumpDurationMillis ms" else "Unknown"}
===================================="""
  }

  companion object {
    private const val serialVersionUID: Long = 130453013437459642

    /**
     * If [fromV20] was serialized in LeakCanary 2.0, you must deserialize it and call this
     * method to create a usable [HeapAnalysisSuccess] instance.
     */
    fun upgradeFrom20Deserialized(fromV20: HeapAnalysisSuccess): HeapAnalysisSuccess {
      val applicationLeaks = fromV20.applicationLeaks
        .map { it.leakTraceFromV20() }
        .groupBy { it.signature }
        .values
        .map {
          ApplicationLeak(it)
        }

      val libraryLeaks = fromV20.libraryLeaks
        .map { it to it.leakTraceFromV20() }
        .groupBy { it.second.signature }
        .values
        .map { listOfPairs ->
          val libraryLeakFrom20 = listOfPairs.first()
            .first
          LibraryLeak(pattern = libraryLeakFrom20.pattern,
            description = libraryLeakFrom20.description,
            leakTraces = listOfPairs.map { it.second }
          )
        }
      return HeapAnalysisSuccess(
        heapDumpFile = fromV20.heapDumpFile,
        createdAtTimeMillis = fromV20.createdAtTimeMillis,
        analysisDurationMillis = fromV20.analysisDurationMillis,
        metadata = fromV20.metadata,
        applicationLeaks = applicationLeaks,
        libraryLeaks = libraryLeaks,
        unreachableObjects = listOf()
      )
    }
  }
}

/**
 * A leak found by [HeapAnalyzer], either an [ApplicationLeak] or a [LibraryLeak].
 */
sealed class Leak : Serializable {

  /**
   * Group of leak traces which share the same leak signature.
   */
  abstract val leakTraces: List

  /**
   * Sum of [LeakTrace.retainedHeapByteSize] for all elements in [leakTraces].
   * Null if the retained heap size was not computed.
   */
  val totalRetainedHeapByteSize: Int?
    get() = if (leakTraces.first().retainedHeapByteSize == null) {
      null
    } else {
      leakTraces.sumBy { it.retainedHeapByteSize!! }
    }

  /**
   * Sum of [LeakTrace.retainedObjectCount] for all elements in [leakTraces].
   * Null if the retained heap size was not computed.
   */
  val totalRetainedObjectCount: Int?
    get() = if (leakTraces.first().retainedObjectCount == null) {
      null
    } else {
      leakTraces.sumBy { it.retainedObjectCount!! }
    }

  /**
   * A unique SHA1 hash that represents this group of leak traces.
   *
   * For [ApplicationLeak] this is based on [LeakTrace.signature] and for [LibraryLeak] this is
   * based on [LibraryLeak.pattern].
   */
  abstract val signature: String

  abstract val shortDescription: String

  override fun toString(): String {
    return (if (totalRetainedHeapByteSize != null) "$totalRetainedHeapByteSize bytes retained by leaking objects\n" else "") +
      (if (leakTraces.size > 1) "Displaying only 1 leak trace out of ${leakTraces.size} with the same signature\n" else "") +
      "Signature: $signature\n" +
      leakTraces.first()
  }

  companion object {
    private const val serialVersionUID: Long = -2287572510360910916
  }
}

/**
 * A leak found by [HeapAnalyzer], where the only path to the leaking object required going
 * through a reference matched by [pattern], as provided to a [LibraryLeakReferenceMatcher]
 * instance. This is a known leak in library code that is beyond your control.
 */
data class LibraryLeak(
  override val leakTraces: List,
  /**
   * The pattern that matched one of the references in each of [leakTraces], as provided to a
   * [LibraryLeakReferenceMatcher] instance.
   */
  val pattern: ReferencePattern,
  /**
   * A description that conveys what we know about this library leak.
   */
  val description: String
) : Leak() {
  override val signature: String
    get() = pattern.toString().createSHA1Hash()

  override val shortDescription: String
    get() = pattern.toString()

  override fun toString(): String {
    return """Leak pattern: $pattern
Description: $description
${super.toString()}
"""
  }

  /** This field is kept to support backward compatible deserialization. */
  private val leakTrace: LeakTrace? = null

  /** This field is kept to support backward compatible deserialization. */
  private val retainedHeapByteSize: Int? = null

  internal fun leakTraceFromV20() = leakTrace!!.fromV20(retainedHeapByteSize)

  companion object {
    private const val serialVersionUID: Long = 3943636164568681903
  }
}

/**
 * A leak found by [HeapAnalyzer] in your application.
 */
data class ApplicationLeak(
  override val leakTraces: List
) : Leak() {
  override val signature: String
    get() = leakTraces.first().signature

  override val shortDescription: String
    get() {
      val leakTrace = leakTraces.first()
      return leakTrace.suspectReferenceSubpath.firstOrNull()?.let { firstSuspectReferencePath ->
        val referenceName = firstSuspectReferencePath.referenceGenericName
        firstSuspectReferencePath.originObject.classSimpleName + "." + referenceName
      } ?: leakTrace.leakingObject.className
    }

  // Override required to avoid the default toString() from data classes
  override fun toString(): String {
    return super.toString()
  }

  /** This field is kept to support backward compatible deserialization. */
  private val leakTrace: LeakTrace? = null

  /** This field is kept to support backward compatible deserialization. */
  private val retainedHeapByteSize: Int? = null

  internal fun leakTraceFromV20() = leakTrace!!.fromV20(retainedHeapByteSize)

  companion object {
    private const val serialVersionUID: Long = 524928276700576863
  }
}

private fun androidSdkInt(): Int {
  return try {
    val versionClass = Class.forName("android.os.Build\$VERSION")
    val sdkIntField = versionClass.getDeclaredField("SDK_INT")
    sdkIntField.get(null) as Int
  } catch (e: Exception) {
    -1
  }
}

private fun androidManufacturer(): String {
  return try {
    val buildClass = Class.forName("android.os.Build")
    val manufacturerField = buildClass.getDeclaredField("MANUFACTURER")
    manufacturerField.get(null) as String
  } catch (e: Exception) {
    "Unknown"
  }
}

private fun leakCanaryVersion(): String {
  return try {
    val versionHolderClass = Class.forName("leakcanary.internal.InternalLeakCanary")
    val versionField = versionHolderClass.getDeclaredField("version")
    versionField.isAccessible = true
    versionField.get(null) as String
  } catch (e: Exception) {
    "Unknown"
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy