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