1 /*
2  * Copyright (C) 2018 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 leakcanary
17 
18 import android.os.SystemClock
19 import leakcanary.InstrumentationLeakDetector.Result.AnalysisPerformed
20 import leakcanary.InstrumentationLeakDetector.Result.NoAnalysis
21 import leakcanary.HeapAnalysisDecision.NoHeapAnalysis
22 import leakcanary.internal.InstrumentationHeapAnalyzer
23 import leakcanary.internal.InstrumentationHeapDumpFileProvider
24 import leakcanary.internal.RetryingHeapAnalyzer
25 import leakcanary.internal.friendly.measureDurationMillis
26 import org.junit.runner.notification.RunListener
27 import shark.HeapAnalysis
28 import shark.HeapAnalysisException
29 import shark.HeapAnalysisFailure
30 import shark.HeapAnalysisSuccess
31 import shark.SharkLog
32 
33 /**
34  * Deprecated: Use LeakAssertions instead
35  *
36  * [InstrumentationLeakDetector] can be used to detect memory leaks in instrumentation tests.
37  *
38  * To use it, you need to add an instrumentation test listener (e.g. [FailTestOnLeakRunListener])
39  * that will invoke [detectLeaks].
40  *
41  * ### Add an instrumentation test listener
42  *
43  * LeakCanary provides [FailTestOnLeakRunListener], but you can also implement
44  * your own [RunListener] and call [detectLeaks] directly if you need a more custom
45  * behavior (for instance running it only once per test suite).
46  *
47  * All you need to do is add the following to the defaultConfig of your build.gradle:
48  *
49  * `testInstrumentationRunnerArgument "listener", "leakcanary.FailTestOnLeakRunListener"`
50  *
51  * Then you can run your instrumentation tests via Gradle as usually, and they will fail when
52  * a memory leak is detected:
53  *
54  * `./gradlew leakcanary-sample:connectedCheck`
55  *
56  * If instead you want to run UI tests via adb, add a *listener* execution argument to
57  * your command line for running the UI tests:
58  * `-e listener leakcanary.FailTestOnLeakRunListener`. The full command line
59  * should look something like this:
60  * ```shell
61  * adb shell am instrument \\
62  * -w com.android.foo/android.support.test.runner.AndroidJUnitRunner \\
63  * -e listener leakcanary.FailTestOnLeakRunListener
64  * ```
65  *
66  * ### Rationale
67  * Instead of using the [InstrumentationLeakDetector], one could simply enable LeakCanary in
68  * instrumentation tests.
69  *
70  * This approach would have two disadvantages:
71  *
72  *  - Heap dumps freeze the VM, and the leak analysis is IO and CPU heavy. This can slow down
73  * the test and introduce flakiness
74  *  - The leak analysis is asynchronous by default. This means the tests could finish and the
75  *  process dies before the analysis is finished.
76  *
77  * The approach taken here is to collect all objects to watch as you run the test, but not
78  * do any heap dump during the test. Then, at the end, if any of the watched objects is still in
79  * memory we dump the heap and perform a blocking analysis. There is only one heap dump performed,
80  * no matter the number of objects retained.
81  */
82 @Deprecated("Use LeakAssertions instead")
83 class InstrumentationLeakDetector {
84 
85   /**
86    * The result of calling [detectLeaks], which is either [NoAnalysis] or [AnalysisPerformed].
87    */
88   sealed class Result {
89     class NoAnalysis(val reason: String) : Result()
90     class AnalysisPerformed(val heapAnalysis: HeapAnalysis) : Result()
91   }
92 
93   /**
94    * Looks for retained objects, triggers a heap dump if needed and performs an analysis.
95    */
96   @Suppress("ReturnCount")
detectLeaksnull97   fun detectLeaks(): Result {
98     val retainedObjectsChecker = AndroidDetectLeaksInterceptor()
99     when(val yesNo = retainedObjectsChecker.waitUntilReadyForHeapAnalysis()) {
100       is NoHeapAnalysis -> {
101         return NoAnalysis(yesNo.reason)
102       }
103     }
104 
105     val heapDumpFile = InstrumentationHeapDumpFileProvider().newHeapDumpFile()
106 
107     val config = LeakCanary.config
108 
109     KeyedWeakReference.heapDumpUptimeMillis = SystemClock.uptimeMillis()
110     val heapDumpDurationMillis = try {
111       measureDurationMillis {
112         config.heapDumper.dumpHeap(heapDumpFile)
113       }
114     } catch (exception: Exception) {
115       SharkLog.d(exception) { "Could not dump heap" }
116       return AnalysisPerformed(
117         HeapAnalysisFailure(
118           heapDumpFile = heapDumpFile,
119           createdAtTimeMillis = System.currentTimeMillis(),
120           dumpDurationMillis = 0,
121           analysisDurationMillis = 0,
122           exception = HeapAnalysisException(exception)
123         )
124       )
125     } finally {
126       val heapDumpUptimeMillis = KeyedWeakReference.heapDumpUptimeMillis
127       AppWatcher.objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
128     }
129 
130     val heapAnalyzer = RetryingHeapAnalyzer(
131       InstrumentationHeapAnalyzer(
132         leakingObjectFinder = config.leakingObjectFinder,
133         referenceMatchers = config.referenceMatchers,
134         computeRetainedHeapSize = config.computeRetainedHeapSize,
135         metadataExtractor = config.metadataExtractor,
136         objectInspectors = config.objectInspectors,
137         proguardMapping = null
138       )
139     )
140 
141     val heapAnalysis = heapAnalyzer.analyze(heapDumpFile).let {
142       when (it) {
143         is HeapAnalysisSuccess -> it.copy(dumpDurationMillis = heapDumpDurationMillis)
144         is HeapAnalysisFailure -> it.copy(dumpDurationMillis = heapDumpDurationMillis)
145       }
146     }
147 
148     return AnalysisPerformed(heapAnalysis)
149   }
150 
151   companion object {
152 
153     @Deprecated(
154       "This is a no-op as LeakCanary automatically detects tests",
155       replaceWith = ReplaceWith("")
156     )
updateConfignull157     fun updateConfig() = Unit
158   }
159 }
160