1 package leakcanary
2 
3 import android.os.SystemClock
4 import leakcanary.internal.InstrumentationHeapAnalyzer
5 import leakcanary.internal.InstrumentationHeapDumpFileProvider
6 import leakcanary.HeapAnalysisDecision.NoHeapAnalysis
7 import leakcanary.internal.RetryingHeapAnalyzer
8 import leakcanary.internal.friendly.checkNotMainThread
9 import leakcanary.internal.friendly.measureDurationMillis
10 import shark.HeapAnalysisFailure
11 import shark.HeapAnalysisSuccess
12 import shark.SharkLog
13 
14 /**
15  * Default [DetectLeaksAssert] implementation. Uses public helpers so you should be able to
16  * create our own implementation if needed.
17  *
18  * Leak detection can be skipped by annotating tests with [SkipLeakDetection] which requires the
19  * [TestDescriptionHolder] test rule be applied and evaluating when [assertNoLeaks]
20  * is called.
21  *
22  * For improved leak detection, you should consider updating [LeakCanary.Config.leakingObjectFinder]
23  * to `FilteringLeakingObjectFinder(AndroidObjectInspectors.appLeakingObjectFilters)` when running
24  * in instrumentation tests. This changes leak detection from being incremental (based on
25  * [AppWatcher] to also scanning for all objects of known types in the heap).
26  */
27 class AndroidDetectLeaksAssert(
28   private val detectLeaksInterceptor: DetectLeaksInterceptor = AndroidDetectLeaksInterceptor(),
29   private val heapAnalysisReporter: HeapAnalysisReporter = NoLeakAssertionFailedError.throwOnApplicationLeaks()
30 ) : DetectLeaksAssert {
assertNoLeaksnull31   override fun assertNoLeaks(tag: String) {
32     val assertionStartUptimeMillis = SystemClock.uptimeMillis()
33     try {
34       runLeakChecks(tag, assertionStartUptimeMillis)
35     } finally {
36       val totalDurationMillis = SystemClock.uptimeMillis() - assertionStartUptimeMillis
37       totalVmDurationMillis += totalDurationMillis
38       SharkLog.d { "Spent $totalDurationMillis ms detecting leaks on $tag, VM total so far: $totalVmDurationMillis ms" }
39     }
40   }
41 
runLeakChecksnull42   private fun runLeakChecks(tag: String, assertionStartUptimeMillis: Long) {
43     if (TestDescriptionHolder.isEvaluating()) {
44       val testDescription = TestDescriptionHolder.testDescription
45       if (SkipLeakDetection.shouldSkipTest(testDescription, tag)) {
46         return
47       }
48     }
49     checkNotMainThread()
50 
51     val waitForRetainedDurationMillis = measureDurationMillis {
52       val yesNo = detectLeaksInterceptor.waitUntilReadyForHeapAnalysis()
53       if (yesNo is NoHeapAnalysis) {
54         SharkLog.d { "Test can keep going: no heap dump performed (${yesNo.reason})" }
55         return
56       }
57     }
58 
59     val heapDumpFile = InstrumentationHeapDumpFileProvider().newHeapDumpFile()
60 
61     val config = LeakCanary.config
62 
63     KeyedWeakReference.heapDumpUptimeMillis = SystemClock.uptimeMillis()
64     val heapDumpDurationMillis = measureDurationMillis {
65       config.heapDumper.dumpHeap(heapDumpFile)
66     }
67     val heapDumpUptimeMillis = KeyedWeakReference.heapDumpUptimeMillis
68     AppWatcher.objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
69 
70     val heapAnalyzer = RetryingHeapAnalyzer(
71       InstrumentationHeapAnalyzer(
72         leakingObjectFinder = config.leakingObjectFinder,
73         referenceMatchers = config.referenceMatchers,
74         computeRetainedHeapSize = config.computeRetainedHeapSize,
75         metadataExtractor = config.metadataExtractor,
76         objectInspectors = config.objectInspectors,
77         proguardMapping = null
78       )
79     )
80     val analysisResult = heapAnalyzer.analyze(heapDumpFile)
81     val totalDurationMillis = SystemClock.uptimeMillis() - assertionStartUptimeMillis
82     val heapAnalysisWithExtraDetails = analysisResult.let {
83       when (it) {
84         is HeapAnalysisSuccess -> it.copy(
85           dumpDurationMillis = heapDumpDurationMillis,
86           metadata = it.metadata + mapOf(
87             ASSERTION_TAG to tag,
88             WAIT_FOR_RETAINED to waitForRetainedDurationMillis.toString(),
89             TOTAL_DURATION to totalDurationMillis.toString()
90           ),
91         )
92         is HeapAnalysisFailure -> it.copy(dumpDurationMillis = heapDumpDurationMillis)
93       }
94     }
95     heapAnalysisReporter.reportHeapAnalysis(heapAnalysisWithExtraDetails)
96   }
97 
98   companion object {
99     private const val ASSERTION_TAG = "assertionTag"
100     private const val WAIT_FOR_RETAINED = "waitForRetainedDurationMillis"
101     private const val TOTAL_DURATION = "totalDurationMillis"
102     private var totalVmDurationMillis = 0L
103 
104     val HeapAnalysisSuccess.assertionTag: String?
105       get() = metadata[ASSERTION_TAG]
106 
107     val HeapAnalysisSuccess.waitForRetainedDurationMillis: Int?
108       get() = metadata[WAIT_FOR_RETAINED]?.toInt()
109 
110     val HeapAnalysisSuccess.totalDurationMillis: Int?
111       get() = metadata[TOTAL_DURATION]?.toInt()
112   }
113 }
114