Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revisit heap growth detection APIs #2621

Merged
merged 21 commits into from Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
44 changes: 44 additions & 0 deletions docs/changelog.md
Expand Up @@ -3,6 +3,50 @@

Please thank our [contributors](https://github.com/square/leakcanary/graphs/contributors) 🙏 🙏 🙏.

## Version 3.0 Alpha 2 (not released yet)

### Heap Growth

* Deleted the `shark-heap-growth` artifact, the code has been merged into the `shark*` and `leakcanary*` modules.
* New `leakcanary-core` module that includes runtime leak detection utilities that aren't Android specific.
* Optimization: for known data structures that don't reference the rest of the graph beyond the references we
known about, we explore them locally at once and stop enqueuing their internals, which reduces the memory
footprint and the IO reads.
* Revamped the APIs for setup. Here's an updated example usage:

```groovy
dependencies {
androidTestImplementation 'com.squareup.leakcanary:leakcanary-core:3.0-alpha-2'
androidTestImplementation 'com.squareup.leakcanary:shark-android:3.0-alpha-2'
}
```

```kotlin
class MyEspressoTest {
val detector = ObjectGrowthDetector
.androidDetector()
.fromHeapDumpingRepeatedScenario(
heapGraphProvider = HeapGraphProvider.dumpingAndDeletingGraphProvider(
heapDumper = { Debug.dumpHprofData(it.absolutePath) },
),
maxHeapDumps = 5,
scenarioLoopsPerDump = 1,
)

@Test
fun greeter_says_hello_does_not_leak() {
// Runs repeatedly until the heap stops growing or we reach max heap dumps.
val heapGrowth = detector.findRepeatedlyGrowingObjects {
onView(withId(R.id.name_field)).perform(typeText("Steve"))
onView(withId(R.id.greet_button)).perform(click())
onView(withText("Hello Steve!")).check(matches(isDisplayed()))
}

assertThat(heapGrowth.growingObjects).isEmpty()
}
}
```

## Version 2.14 (2024-04-17)

* 🐛 [#2650](https://github.com/square/leakcanary/issues/2650) Removed accidental usage of `SettableFuture`, a `WorkManager` internal class, which will be removed in a **future release** of WorkManager. After updating WorkManager to that future release, **all versions of LeakCanary from 2.8 to 2.13 will crash on leak analysis**. To avoid a nasty surprise in the near future, **update to LeakCanary 2.14**.
Expand Down
Expand Up @@ -12,6 +12,10 @@ public final class leakcanary/AndroidDebugHeapDumper : leakcanary/HeapDumper {
public fun dumpHeap (Ljava/io/File;)V
}

public final class leakcanary/AndroidDebugHeapDumperKt {
public static final fun androidDumper (Lleakcanary/HeapDumper$Companion;)Lleakcanary/AndroidDebugHeapDumper;
}

public final class leakcanary/BackgroundThreadHeapAnalyzer : leakcanary/EventListener {
public static final field INSTANCE Lleakcanary/BackgroundThreadHeapAnalyzer;
public fun onEvent (Lleakcanary/EventListener$Event;)V
Expand Down Expand Up @@ -64,10 +68,6 @@ public final class leakcanary/EventListener$Event$HeapDumpFailed : leakcanary/Ev
public final fun getWillRetryLater ()Z
}

public abstract interface class leakcanary/HeapDumper {
public abstract fun dumpHeap (Ljava/io/File;)V
}

public final class leakcanary/LazyForwardingEventListener : leakcanary/EventListener {
public fun <init> (Lkotlin/jvm/functions/Function0;)V
public fun onEvent (Lleakcanary/EventListener$Event;)V
Expand Down
1 change: 1 addition & 0 deletions leakcanary/leakcanary-android-core/build.gradle
Expand Up @@ -8,6 +8,7 @@ dependencies {
api projects.shark.sharkAndroid
api projects.objectWatcher.objectWatcherAndroidCore
api projects.objectWatcher.objectWatcherAndroidAndroidx
api projects.leakcanary.leakcanaryCore
implementation libs.kotlin.stdlib

// Optional dependency
Expand Down
Expand Up @@ -14,3 +14,5 @@ object AndroidDebugHeapDumper : HeapDumper {
Debug.dumpHprofData(heapDumpFile.absolutePath)
}
}

fun HeapDumper.Companion.androidDumper() = AndroidDebugHeapDumper
1 change: 1 addition & 0 deletions leakcanary/leakcanary-android-instrumentation/build.gradle
Expand Up @@ -6,6 +6,7 @@ plugins {

dependencies {
api projects.leakcanary.leakcanaryAndroidCore
api projects.shark.sharkAndroid

implementation libs.androidX.test.runner
implementation libs.kotlin.stdlib
Expand Down
@@ -1,16 +1,17 @@
package leakcanary

import android.os.SystemClock
import androidx.test.platform.app.InstrumentationRegistry
import java.io.File
import kotlin.time.Duration.Companion.milliseconds
import leakcanary.HeapAnalysisDecision.NoHeapAnalysis
import leakcanary.internal.InstrumentationHeapAnalyzer
import leakcanary.internal.InstrumentationHeapDumpFileProvider
import leakcanary.internal.RetryingHeapAnalyzer
import leakcanary.internal.friendly.checkNotMainThread
import leakcanary.internal.friendly.measureDurationMillis
import shark.HeapAnalysisFailure
import shark.HeapAnalysisSuccess
import shark.SharkLog
import kotlin.time.Duration.Companion.milliseconds

/**
* Default [DetectLeaksAssert] implementation. Uses public helpers so you should be able to
Expand Down Expand Up @@ -40,7 +41,10 @@ class AndroidDetectLeaksAssert(
}
}

private fun runLeakChecks(tag: String, assertionStartUptimeMillis: Long) {
private fun runLeakChecks(
tag: String,
assertionStartUptimeMillis: Long
) {
if (TestDescriptionHolder.isEvaluating()) {
val testDescription = TestDescriptionHolder.testDescription
if (SkipLeakDetection.shouldSkipTest(testDescription, tag)) {
Expand All @@ -57,7 +61,15 @@ class AndroidDetectLeaksAssert(
}
}

val heapDumpFile = InstrumentationHeapDumpFileProvider().newHeapDumpFile()
val heapDumpFileProvider = HeapDumpFileProvider.datetimeFormattedFileProvider(
directory = File(
InstrumentationRegistry.getInstrumentation().targetContext.filesDir,
"instrumentation_tests"
),
prefix = "instrumentation_tests"
)

val heapDumpFile = heapDumpFileProvider.newHeapDumpFile()

val config = LeakCanary.config

Expand Down

This file was deleted.

Expand Up @@ -38,6 +38,7 @@ import shark.HprofHeapGraph.Companion.openHeapGraph
import shark.IgnoredReferenceMatcher
import shark.ObjectDominators
import shark.ObjectDominators.OfflineDominatorNode
import shark.ReferenceMatcher
import shark.ValueHolder

sealed interface TreeMapState {
Expand Down Expand Up @@ -65,7 +66,7 @@ class TreeMapViewModel @Inject constructor(
AndroidReferenceMatchers.REFERENCES, AndroidReferenceMatchers.FINALIZER_WATCHDOG_DAEMON
)
val ignoredRefs =
AndroidReferenceMatchers.buildKnownReferences(weakAndFinalizerRefs).map { matcher ->
ReferenceMatcher.fromListBuilders(weakAndFinalizerRefs).map { matcher ->
matcher as IgnoredReferenceMatcher
}

Expand Down Expand Up @@ -153,7 +154,7 @@ fun OnDeviceHeapTreemapPreview() {
AndroidReferenceMatchers.REFERENCES, AndroidReferenceMatchers.FINALIZER_WATCHDOG_DAEMON
)
val ignoredRefs =
AndroidReferenceMatchers.buildKnownReferences(weakAndFinalizerRefs).map { matcher ->
ReferenceMatcher.fromListBuilders(weakAndFinalizerRefs).map { matcher ->
matcher as IgnoredReferenceMatcher
}

Expand Down
51 changes: 51 additions & 0 deletions leakcanary/leakcanary-core/api/leakcanary-core.api
@@ -0,0 +1,51 @@
public final class leakcanary/DateFormatHeapDumpFileProvider : leakcanary/HeapDumpFileProvider {
public static final field Companion Lleakcanary/DateFormatHeapDumpFileProvider$Companion;
public static final field TIME_PATTERN Ljava/lang/String;
public fun <init> (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun newHeapDumpFile ()Ljava/io/File;
}

public final class leakcanary/DateFormatHeapDumpFileProvider$Companion {
}

public final class leakcanary/DateFormatHeapDumpFileProviderKt {
public static final fun datetimeFormattedFileProvider (Lleakcanary/HeapDumpFileProvider$Companion;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Lleakcanary/HeapDumpFileProvider;
public static synthetic fun datetimeFormattedFileProvider$default (Lleakcanary/HeapDumpFileProvider$Companion;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lleakcanary/HeapDumpFileProvider;
}

public final class leakcanary/DumpingAndDeletingHeapGraphProvider : shark/HeapGraphProvider {
public fun <init> (Lleakcanary/HeapDumpFileProvider;Lleakcanary/HeapDumper;)V
public fun openHeapGraph ()Lshark/CloseableHeapGraph;
}

public final class leakcanary/DumpingAndDeletingHeapGraphProviderKt {
public static final fun dumpingAndDeletingGraphProvider (Lshark/HeapGraphProvider$Companion;Lleakcanary/HeapDumper;Lleakcanary/HeapDumpFileProvider;)Lleakcanary/DumpingAndDeletingHeapGraphProvider;
public static synthetic fun dumpingAndDeletingGraphProvider$default (Lshark/HeapGraphProvider$Companion;Lleakcanary/HeapDumper;Lleakcanary/HeapDumpFileProvider;ILjava/lang/Object;)Lleakcanary/DumpingAndDeletingHeapGraphProvider;
}

public abstract interface class leakcanary/HeapDumpFileProvider {
public static final field Companion Lleakcanary/HeapDumpFileProvider$Companion;
public abstract fun newHeapDumpFile ()Ljava/io/File;
}

public final class leakcanary/HeapDumpFileProvider$Companion {
}

public abstract interface class leakcanary/HeapDumper {
public static final field Companion Lleakcanary/HeapDumper$Companion;
public abstract fun dumpHeap (Ljava/io/File;)V
}

public final class leakcanary/HeapDumper$Companion {
}

public final class leakcanary/TempHeapDumpFileProvider : leakcanary/HeapDumpFileProvider {
public static final field INSTANCE Lleakcanary/TempHeapDumpFileProvider;
public fun newHeapDumpFile ()Ljava/io/File;
}

public final class leakcanary/TempHeapDumpFileProviderKt {
public static final fun tempFileProvider (Lleakcanary/HeapDumpFileProvider$Companion;)Lleakcanary/HeapDumpFileProvider;
}

11 changes: 11 additions & 0 deletions leakcanary/leakcanary-core/build.gradle
@@ -0,0 +1,11 @@
plugins {
id("org.jetbrains.kotlin.jvm")
id("com.vanniktech.maven.publish")
}

sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8

dependencies {
api projects.shark.shark
}
3 changes: 3 additions & 0 deletions leakcanary/leakcanary-core/gradle.properties
@@ -0,0 +1,3 @@
POM_ARTIFACT_ID=leakcanary-core
POM_NAME=LeakCanary Core
POM_PACKAGING=jar
@@ -0,0 +1,60 @@
package leakcanary

import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

class DateFormatHeapDumpFileProvider(
private val heapDumpDirectoryProvider: () -> File,
private val dateProvider: () -> Date = { Date() },
prefix: String = "",
suffix: String = ""
) : HeapDumpFileProvider {

private val dateFormatPattern =
"${escape(prefix)}$TIME_PATTERN${escape("$suffix.hprof")}"

private val timeFormatThreadLocal = object : ThreadLocal<SimpleDateFormat>() {
// Lint is drunk and thinks we use the pattern 'u'
@Suppress("NewApi")
override fun initialValue() =
SimpleDateFormat(dateFormatPattern, Locale.US)
}

override fun newHeapDumpFile(): File {
val heapDumpDirectory = heapDumpDirectoryProvider()
val date = dateProvider()
val fileName = timeFormatThreadLocal.get()!!.format(date)
return File(heapDumpDirectory, fileName)
}

private fun escape(string: String) = if (string != "") {
"'$string'"
} else ""

companion object {
const val TIME_PATTERN = "yyyy-MM-dd_HH-mm-ss_SSS"
}
}

fun HeapDumpFileProvider.Companion.datetimeFormattedFileProvider(
directory: File,
prefix: String = "",
suffix: String = "",
dateProvider: () -> Date = { Date() },
): HeapDumpFileProvider {
return DateFormatHeapDumpFileProvider(
heapDumpDirectoryProvider = {
directory.apply {
mkdirs()
check(exists()) {
"Expected heap dump folder to exist: $absolutePath"
}
}
},
dateProvider = dateProvider,
prefix = prefix,
suffix = suffix
)
}
@@ -0,0 +1,30 @@
package leakcanary

import shark.CloseableHeapGraph
import shark.HeapGraphProvider
import shark.HprofHeapGraph.Companion.openHeapGraph

class DumpingAndDeletingHeapGraphProvider(
private val heapDumpFileProvider: HeapDumpFileProvider,
private val heapDumper: HeapDumper
) : HeapGraphProvider {
override fun openHeapGraph(): CloseableHeapGraph {
val heapDumpFile = heapDumpFileProvider.newHeapDumpFile()
heapDumper.dumpHeap(heapDumpFile)
check(heapDumpFile.exists()) {
"Expected file to exist after heap dump: ${heapDumpFile.absolutePath}"
}
val realGraph = heapDumpFile.openHeapGraph()
return object : CloseableHeapGraph by realGraph {
override fun close() {
realGraph.close()
heapDumpFile.delete()
}
}
}
}

fun HeapGraphProvider.Companion.dumpingAndDeletingGraphProvider(
heapDumper: HeapDumper,
heapDumpFileProvider: HeapDumpFileProvider = TempHeapDumpFileProvider,
) = DumpingAndDeletingHeapGraphProvider(heapDumpFileProvider, heapDumper)
@@ -0,0 +1,18 @@
package leakcanary

import java.io.File
import java.util.Date

fun interface HeapDumpFileProvider {

/**
* Returns a [File] that can be passed to a [HeapDumper] to dump the heap.
*/
fun newHeapDumpFile(): File

/**
* This allows external modules to add factory methods for implementations of this interface as
* extension functions of this companion object.
*/
companion object
}
Expand Up @@ -11,4 +11,10 @@ fun interface HeapDumper {
* Implementations can throw a runtime exception if heap dumping failed.
*/
fun dumpHeap(heapDumpFile: File)

/**
* This allows external modules to add factory methods for implementations of this interface as
* extension functions of this companion object.
*/
companion object
}