blob: 0d544a8676b19ad08adc0d008791ac12367c9903 [file] [log] [blame] [edit]
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.leakcanary
import android.app.Application
import android.os.Debug
import android.os.SystemClock
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.squareup.leakcanary.InstrumentationLeakDetector.Companion.instrumentationRefWatcher
import com.squareup.leakcanary.internal.LeakCanaryInternals
import org.junit.runner.notification.RunListener
import java.io.File
import java.util.concurrent.CopyOnWriteArrayList
/**
* [InstrumentationLeakDetector] can be used to detect memory leaks in instrumentation tests.
*
* To use it, you need to:
*
* * Install a custom RefWatcher that will not trigger heapdumps while the tests run.
* * Add an instrumentation test listener (a [RunListener]) that will invoke
* [detectLeaks]
*
* ### Installing the instrumentation RefWatcher
*
* For [detectLeaks] to work correctly, the [RefWatcher] must keep track of
* references but not trigger any heap dump until this [detectLeaks] runs, otherwise an
* analysis in progress might prevent this listener from performing its own analysis.
*
* Create and install the [RefWatcher] instance using
* [instrumentationRefWatcher] instead of
* [LeakCanary.install] or [LeakCanary.refWatcher].
*
* ```
* public class InstrumentationExampleApplication extends ExampleApplication {
* @Override protected void setupLeakCanary() {
* InstrumentationLeakDetector.instrumentationRefWatcher(this)
* .buildAndInstall();
* }
* }
* ```
*
* ### Add an instrumentation test listener
*
* LeakCanary provides [FailTestOnLeakRunListener], but you should feel free to implement
* your own [RunListener] and call [.detectLeaks] directly if you need a more custom
* behavior (for instance running it only once per test suite, or reporting to a backend).
*
* All you need to do is add the following to the defaultConfig of your build.gradle:
*
* `testInstrumentationRunnerArgument "listener", "com.squareup.leakcanary.FailTestOnLeakRunListener"`
*
* Then you can run your instrumentation tests via Gradle as usually, and they will fail when
* a memory leak is detected:
*
* `./gradlew leakcanary-sample:connectedCheck`
*
* If instead you want to run UI tests via adb, add a *listener* execution argument to
* your command line for running the UI tests:
* `-e listener com.squareup.leakcanary.FailTestOnLeakRunListener`. The full command line
* should look something like this:
* ```
* adb shell am instrument \\
* -w com.android.foo/android.support.test.runner.AndroidJUnitRunner \\
* -e listener com.squareup.leakcanary.FailTestOnLeakRunListener
* ```
*
* ### Rationale
* Instead of using the [FailTestOnLeakRunListener], one could simply enable LeakCanary in
* instrumentation tests.
*
* This approach would have two disadvantages:
*
* * Heap dumps freeze the VM, and the leak analysis is IO and CPU heavy. This can slow down
* the test and introduce flakiness
* * The leak analysis is asynchronous by default, and happens in a separate process. This means
* the tests could finish and the process die before the analysis is finished.
*
* The approach taken here is to collect all references to watch as you run the test, but not
* do any heap dump during the test. Then, at the end, if any of the watched objects is still in
* memory we dump the heap and perform a blocking analysis. There is only one heap dump performed,
* no matter the number of objects leaking, and then we iterate on the leaking references in the
* heap dump and provide all result in a [InstrumentationLeakResults].
*/
class InstrumentationLeakDetector {
fun detectLeaks(): InstrumentationLeakResults {
val instrumentation = getInstrumentation()
val context = instrumentation.targetContext
val refWatcher = LeakCanary.installedRefWatcher()
val retainedKeys = refWatcher.retainedKeys
if (refWatcher.isEmpty) {
return InstrumentationLeakResults.NONE
}
instrumentation.waitForIdleSync()
if (refWatcher.isEmpty) {
return InstrumentationLeakResults.NONE
}
GcTrigger.DEFAULT.runGc()
if (refWatcher.isEmpty) {
return InstrumentationLeakResults.NONE
}
// Waiting for any delayed UI post (e.g. scroll) to clear. This shouldn't be needed, but
// Android simply has way too many delayed posts that aren't canceled when views are detached.
SystemClock.sleep(2000)
if (refWatcher.isEmpty) {
return InstrumentationLeakResults.NONE
}
// Aaand we wait some more.
// 4 seconds (2+2) is greater than the 3 seconds delay for
// FINISH_TOKEN in android.widget.Filter
SystemClock.sleep(2000)
GcTrigger.DEFAULT.runGc()
if (refWatcher.isEmpty) {
return InstrumentationLeakResults.NONE
}
// We're always reusing the same file since we only execute this once at a time.
val heapDumpFile = File(context.filesDir, "instrumentation_tests_heapdump.hprof")
try {
Debug.dumpHprofData(heapDumpFile.absolutePath)
} catch (e: Exception) {
CanaryLog.d(e, "Could not dump heap")
return InstrumentationLeakResults.NONE
}
val heapDumpBuilder = LeakCanaryInternals.installedHeapDumpBuilder
val heapAnalyzer = HeapAnalyzer(
heapDumpBuilder.excludedRefs, AnalyzerProgressListener.NONE,
heapDumpBuilder.reachabilityInspectorClasses
)
val trackedReferences = heapAnalyzer.findTrackedReferences(heapDumpFile)
val detectedLeaks = mutableListOf<InstrumentationLeakResults.Result>()
val excludedLeaks = mutableListOf<InstrumentationLeakResults.Result>()
val failures = mutableListOf<InstrumentationLeakResults.Result>()
trackedReferences
// Ignore any Weak Reference that this test does not care about.
.filter { retainedKeys.contains(it.key) }
.forEach { trackedReference ->
val heapDump = HeapDump.builder()
.heapDumpFile(heapDumpFile)
.excludedRefs(heapDumpBuilder.excludedRefs)
.reachabilityInspectorClasses(heapDumpBuilder.reachabilityInspectorClasses)
.build()
val analysisResult =
heapAnalyzer.checkForLeak(heapDumpFile, trackedReference.key, false)
val leakResult = InstrumentationLeakResults.Result(heapDump, analysisResult)
if (analysisResult.leakFound) {
if (!analysisResult.excludedLeak) {
detectedLeaks.add(leakResult)
} else {
excludedLeaks.add(leakResult)
}
} else if (analysisResult.failure != null) {
failures.add(leakResult)
}
}
CanaryLog.d(
"Found %d proper leaks, %d excluded leaks and %d leak analysis failures",
detectedLeaks.size,
excludedLeaks.size,
failures.size
)
return InstrumentationLeakResults(detectedLeaks, excludedLeaks, failures)
}
companion object {
/**
* Returns a new [AndroidRefWatcherBuilder] that will create a [RefWatcher] suitable
* for instrumentation tests. This [RefWatcher] will never trigger a heap dump. This should
* be installed from the test application class, and should be used in combination with a
* [RunListener] that calls [detectLeaks], for instance
* [FailTestOnLeakRunListener].
*/
fun instrumentationRefWatcher(application: Application): AndroidRefWatcherBuilder {
return LeakCanary.refWatcher(application)
.watchExecutor(object : WatchExecutor {
// Storing weak refs to ensure they make it to the queue.
val trackedReferences: MutableList<Retryable> = CopyOnWriteArrayList()
override fun execute(retryable: Retryable) {
trackedReferences.add(retryable)
}
})
}
}
}