Detect memory leaks in your instrumentation tests using LeakCanary

Marcos Holgado
ProAndroidDev
Published in
5 min readApr 25, 2019

--

I’ve had this article on my backlog for a long time since I started playing with performance tests back in August 2018 but I never got to finish it off. It seems a good time now since Py ⚔ released at AndroidMakers a new major version (2.0-alpha-1) with new updates and more importantly a new dump heap parser giving us a better memory performance (x10 less memory) and a faster analysis of the dump heap (x6 faster).

I started writing this article a few months ago and since then a few things have changed… for the better. Let’s see what the differences are!

All the code is available at my performance-test repository on GitHub:

https://github.com/marcosholgado/performance-test

LeakCanary is available here:

https://github.com/square/leakcanary

The Experiment

I already talked about this a little bit in my article “How Kotlin helps you avoid memory leaks”. The tl;dr; is that I was preparing a talk about performance testing and I wanted to demonstrate how to easily detect memory leaks during your instrumentation tests by using LeakCanary.

I wrote (stole from the LeakCanary sample app) this Activity which leaks memory when pressing the “Leak” button. The code is in Java but if you read the article that I mentioned before, you should know how to write it in Kotlin as well :).

This is the test that creates the memory leak, very simple stuff. It performs a click in the button which creates the runnable and, because the test finishes at that point, the activity will get destroyed before the thread has time to finish (20 seconds) creating a memory leak.

@Test
fun testLeaks() {
onView(withId(R.id.button)).perform(click())
}

LeakCanary 1.6

I already had half of this written so let’s see what you had to do previously and what you can do to solve some small issues.

If you follow the documentation the steps are quite straightforward. We first need a dependency on leakcanary-android-instrumentation:

androidTestImplementation "com.squareup.leakcanary:leakcanary-android-instrumentation:${leakCanaryVersion}"

Then install the RefWatcher in your test application:

open class TestApplication: MyApplication() {
override fun setupLeakCanary() {
InstrumentationLeakDetector.instrumentationRefWatcher(this)
.buildAndInstall()
}
}

And finally use the FailTestOnLeakRunListener on your gradle file:

android {
defaultConfig {
// ...
testInstrumentationRunnerArgument "listener", "com.squareup.leakcanary.FailTestOnLeakRunListener"
}
}

You can then run the test and see how it fails with a very long stack trace.

This is great! We ran an instrumentation test and it failed because of a memory leak that we introduced. It is exactly what we were after except for one small issue.

Time is precious

The test took 20s 448ms to run. In other words, a very simple test that should run in 1 or 2 seconds took 20 and what is worse, because we are using the FailTestOnLeakRunListener on our grade file, all the instrumentation tests are going to be analysed for memory leaks.

Since analysing the dump heap takes a long time we don’t want to do that for every single test, instead we want to use it on very specific tests. One way to go about this is to remove the listener from gradle and add it manually on the terminal for the tests where we want to use it.

adb shell am instrument -w  \
> -e listener com.squareup.leakcanary.FailTestOnLeakRunListener \
> -e class com.marcosholgado.performancetest.SeventhTest#testLeaks \
> com.mh.perftest.test/androidx.test.runner.AndroidJUnitRunner

But what I would really like to do is being able to annotate my test with @LeakTest to let LeakCanary know that I want to analyse that specific test and ignore the leak detection in the rest of the tests.

@Test
@LeakTest
fun testLeaks() {
onView(withId(R.id.button)).perform(click())
}

Thankfully LeakCanary gives us the ability to override the FailTestOnLeakRunListener, and, to be more precise, the skipLeakDetectionReason method. Since that method takes a description, we can use it to find (or not) our LeakTest annotation and either carry on with the leak detection or skip it.

The only thing left to do is replace the listener provided by LeakCanary with our new listener in the Gradle file.

android {
defaultConfig {
// ...
testInstrumentationRunnerArgument "listener", "com.marcosholgado.performancetest.ninthTest.MyOtherLeakRunListener"
}
}

LeakCanary 2.0

So what’s different in LeakCanary 2.0?

Well, for starters we don’t need to install the RefWatcher in our test application so we can get rid of that. Secondly the new dump heap analyser is much faster and has a better memory performance.

In LeakCanary 1.6 our instrumentation test took 20s 448ms. With Leak Canary 2.0 it takes 11s 225ms. That’s half the time! A very big difference compared to the previous version.

Finally, the trace given is much more readable and points you to the possible root cause of the memory leak in a more understandable way.

I still think the use of some sort of annotation is needed, the analysis of the dump heap takes less time but it is still significant for us to not want it to run on every single instrumentation test. However the fact that the time has been reduced so drastically and that we are still on alpha it means that there is room for improvement and who knows? maybe one day we won’t need to restrict the detection of memory leaks to a subset of our tests.

For now, you can implement your own listener but I had a chat with Py to maybe incorporate this functionality as part of LeakCanary. You can follow that PR in this link if you are interested: https://github.com/square/leakcanary/pull/1309

That was everything, I hope this article helped you better understand how to use LeakCanary to add memory leak detection in your instrumentation tests. You have no excuses to avoid memory leaks anymore, specially since the new version has a much faster parsing time of the dump heap. Also thanks to Py ⚔ for his amazing work with LeakCanary.

If you have any questions please leave a comment or reach out on Twitter.

--

--

Senior Android Developer at DuckDuckGo. Speaker, Kotlin lover and I also fly planes. www.marcosholgado.com