main

square/leakcanary

Last updated at: 29/12/2023 09:38

AndroidDetectLeaksAssert.kt

TLDR

The file AndroidDetectLeaksAssert.kt is a Kotlin file in the leakcanary package. It provides the default implementation of the DetectLeaksAssert interface and contains methods and properties related to leak detection and heap analysis in Android applications.

Methods

assertNoLeaks

This method is called to assert that there are no memory leaks in the application. It performs various checks and analysis to detect leaks. The method takes a tag parameter which is used for identifying the assertion. The method also logs the total duration spent in leak detection.

Classes

AndroidDetectLeaksAssert

This class provides the default implementation of the DetectLeaksAssert interface. It implements the assertNoLeaks method and contains private helper methods for performing leak detection and analysis. The class also defines a companion object that contains constants and properties related to heap analysis.

package leakcanary

import android.os.SystemClock
import leakcanary.internal.InstrumentationHeapAnalyzer
import leakcanary.internal.InstrumentationHeapDumpFileProvider
import leakcanary.HeapAnalysisDecision.NoHeapAnalysis
import leakcanary.internal.RetryingHeapAnalyzer
import leakcanary.internal.friendly.checkNotMainThread
import leakcanary.internal.friendly.measureDurationMillis
import shark.HeapAnalysisFailure
import shark.HeapAnalysisSuccess
import shark.SharkLog

/**
 * Default [DetectLeaksAssert] implementation. Uses public helpers so you should be able to
 * create our own implementation if needed.
 *
 * Leak detection can be skipped by annotating tests with [SkipLeakDetection] which requires the
 * [TestDescriptionHolder] test rule be applied and evaluating when [assertNoLeaks]
 * is called.
 *
 * For improved leak detection, you should consider updating [LeakCanary.Config.leakingObjectFinder]
 * to `FilteringLeakingObjectFinder(AndroidObjectInspectors.appLeakingObjectFilters)` when running
 * in instrumentation tests. This changes leak detection from being incremental (based on
 * [AppWatcher] to also scanning for all objects of known types in the heap).
 */
class AndroidDetectLeaksAssert(
  private val detectLeaksInterceptor: DetectLeaksInterceptor = AndroidDetectLeaksInterceptor(),
  private val heapAnalysisReporter: HeapAnalysisReporter = NoLeakAssertionFailedError.throwOnApplicationLeaks()
) : DetectLeaksAssert {
  override fun assertNoLeaks(tag: String) {
    val assertionStartUptimeMillis = SystemClock.uptimeMillis()
    try {
      runLeakChecks(tag, assertionStartUptimeMillis)
    } finally {
      val totalDurationMillis = SystemClock.uptimeMillis() - assertionStartUptimeMillis
      totalVmDurationMillis += totalDurationMillis
      SharkLog.d { "Spent $totalDurationMillis ms detecting leaks on $tag, VM total so far: $totalVmDurationMillis ms" }
    }
  }

  private fun runLeakChecks(tag: String, assertionStartUptimeMillis: Long) {
    if (TestDescriptionHolder.isEvaluating()) {
      val testDescription = TestDescriptionHolder.testDescription
      if (SkipLeakDetection.shouldSkipTest(testDescription, tag)) {
        return
      }
    }
    checkNotMainThread()

    val waitForRetainedDurationMillis = measureDurationMillis {
      val yesNo = detectLeaksInterceptor.waitUntilReadyForHeapAnalysis()
      if (yesNo is NoHeapAnalysis) {
        SharkLog.d { "Test can keep going: no heap dump performed (${yesNo.reason})" }
        return
      }
    }

    val heapDumpFile = InstrumentationHeapDumpFileProvider().newHeapDumpFile()

    val config = LeakCanary.config

    KeyedWeakReference.heapDumpUptimeMillis = SystemClock.uptimeMillis()
    val heapDumpDurationMillis = measureDurationMillis {
      config.heapDumper.dumpHeap(heapDumpFile)
    }
    val heapDumpUptimeMillis = KeyedWeakReference.heapDumpUptimeMillis
    AppWatcher.objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)

    val heapAnalyzer = RetryingHeapAnalyzer(
      InstrumentationHeapAnalyzer(
        leakingObjectFinder = config.leakingObjectFinder,
        referenceMatchers = config.referenceMatchers,
        computeRetainedHeapSize = config.computeRetainedHeapSize,
        metadataExtractor = config.metadataExtractor,
        objectInspectors = config.objectInspectors,
        proguardMapping = null
      )
    )
    val analysisResult = heapAnalyzer.analyze(heapDumpFile)
    val totalDurationMillis = SystemClock.uptimeMillis() - assertionStartUptimeMillis
    val heapAnalysisWithExtraDetails = analysisResult.let {
      when (it) {
        is HeapAnalysisSuccess -> it.copy(
          dumpDurationMillis = heapDumpDurationMillis,
          metadata = it.metadata + mapOf(
            ASSERTION_TAG to tag,
            WAIT_FOR_RETAINED to waitForRetainedDurationMillis.toString(),
            TOTAL_DURATION to totalDurationMillis.toString()
          ),
        )
        is HeapAnalysisFailure -> it.copy(dumpDurationMillis = heapDumpDurationMillis)
      }
    }
    heapAnalysisReporter.reportHeapAnalysis(heapAnalysisWithExtraDetails)
  }

  companion object {
    private const val ASSERTION_TAG = "assertionTag"
    private const val WAIT_FOR_RETAINED = "waitForRetainedDurationMillis"
    private const val TOTAL_DURATION = "totalDurationMillis"
    private var totalVmDurationMillis = 0L

    val HeapAnalysisSuccess.assertionTag: String?
      get() = metadata[ASSERTION_TAG]

    val HeapAnalysisSuccess.waitForRetainedDurationMillis: Int?
      get() = metadata[WAIT_FOR_RETAINED]?.toInt()

    val HeapAnalysisSuccess.totalDurationMillis: Int?
      get() = metadata[TOTAL_DURATION]?.toInt()
  }
}