Inline collaborators and add support for proper hprof handling on failures
diff --git a/leakcanary/leakcanary-android-test/src/main/java/leakcanary/RepeatingAndroidInProcessScenario.kt b/leakcanary/leakcanary-android-test/src/main/java/leakcanary/RepeatingAndroidInProcessScenario.kt
index 4651abf..144cba8 100644
--- a/leakcanary/leakcanary-android-test/src/main/java/leakcanary/RepeatingAndroidInProcessScenario.kt
+++ b/leakcanary/leakcanary-android-test/src/main/java/leakcanary/RepeatingAndroidInProcessScenario.kt
@@ -23,9 +23,9 @@
.withDetectorWarmup(objectGrowthDetector, androidHeap = true),
heapDumpStorageStrategy: HeapDumpStorageStrategy = HeapDumpStorageStrategy.DeleteOnHeapDumpClose(),
): RepeatingScenarioObjectGrowthDetector {
- return repeatingDumpingTestScenario(
+ return DumpingRepeatingScenarioObjectGrowthDetector(
objectGrowthDetector = objectGrowthDetector,
- heapDumpDirectoryProvider = heapDumpDirectoryProvider,
+ heapDumpFileProvider = TestHeapDumpFileProvider(heapDumpDirectoryProvider),
heapDumper = heapDumper,
heapDumpStorageStrategy = heapDumpStorageStrategy,
)
diff --git a/leakcanary/leakcanary-android-uiautomator/src/main/java/leakcanary/RepeatingUiAutomatorScenario.kt b/leakcanary/leakcanary-android-uiautomator/src/main/java/leakcanary/RepeatingUiAutomatorScenario.kt
index fc3ea2f..a2d53bf 100644
--- a/leakcanary/leakcanary-android-uiautomator/src/main/java/leakcanary/RepeatingUiAutomatorScenario.kt
+++ b/leakcanary/leakcanary-android-uiautomator/src/main/java/leakcanary/RepeatingUiAutomatorScenario.kt
@@ -27,9 +27,9 @@
UiAutomatorShellFileDeleter.deleteFileUsingShell(heapDumpFile)
},
): RepeatingScenarioObjectGrowthDetector {
- return repeatingDumpingTestScenario(
+ return DumpingRepeatingScenarioObjectGrowthDetector(
objectGrowthDetector = objectGrowthDetector,
- heapDumpDirectoryProvider = heapDumpDirectoryProvider,
+ heapDumpFileProvider = TestHeapDumpFileProvider(heapDumpDirectoryProvider),
heapDumper = heapDumper,
heapDumpStorageStrategy = heapDumpStorageStrategy,
)
diff --git a/leakcanary/leakcanary-core/api/leakcanary-core.api b/leakcanary/leakcanary-core/api/leakcanary-core.api
index 9e55b9d..0c89725 100644
--- a/leakcanary/leakcanary-core/api/leakcanary-core.api
+++ b/leakcanary/leakcanary-core/api/leakcanary-core.api
@@ -9,14 +9,9 @@
public final class leakcanary/DatetimeFormattedHeapDumpFileProvider$Companion {
}
-public final class leakcanary/DumpingHeapGraphProvider : shark/HeapGraphProvider {
- public fun <init> (Lleakcanary/HeapDumpFileProvider;Lleakcanary/HeapDumper;Lleakcanary/DumpingHeapGraphProvider$HeapDumpClosedListener;)V
- public synthetic fun <init> (Lleakcanary/HeapDumpFileProvider;Lleakcanary/HeapDumper;Lleakcanary/DumpingHeapGraphProvider$HeapDumpClosedListener;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
- public fun openHeapGraph ()Lshark/CloseableHeapGraph;
-}
-
-public abstract interface class leakcanary/DumpingHeapGraphProvider$HeapDumpClosedListener {
- public abstract fun onHeapDumpClosed (Ljava/io/File;)V
+public final class leakcanary/DumpingRepeatingScenarioObjectGrowthDetector : shark/RepeatingScenarioObjectGrowthDetector {
+ public fun <init> (Lshark/ObjectGrowthDetector;Lleakcanary/HeapDumpFileProvider;Lleakcanary/HeapDumper;Lleakcanary/HeapDumpStorageStrategy;)V
+ public fun findRepeatedlyGrowingObjects (IILkotlin/jvm/functions/Function0;)Lshark/HeapDiff;
}
public abstract interface class leakcanary/HeapDumpDirectoryProvider {
@@ -27,6 +22,52 @@
public abstract fun newHeapDumpFile ()Ljava/io/File;
}
+public abstract interface class leakcanary/HeapDumpStorageStrategy {
+ public abstract fun onHeapDiffResult (Ljava/lang/Object;)V
+ public abstract fun onHeapDumpClosed (Ljava/io/File;)V
+ public abstract fun onHeapDumped (Ljava/io/File;)V
+}
+
+public final class leakcanary/HeapDumpStorageStrategy$DefaultImpls {
+ public static fun onHeapDiffResult (Lleakcanary/HeapDumpStorageStrategy;Ljava/lang/Object;)V
+ public static fun onHeapDumpClosed (Lleakcanary/HeapDumpStorageStrategy;Ljava/io/File;)V
+ public static fun onHeapDumped (Lleakcanary/HeapDumpStorageStrategy;Ljava/io/File;)V
+}
+
+public final class leakcanary/HeapDumpStorageStrategy$DeleteOnHeapDumpClose : leakcanary/HeapDumpStorageStrategy {
+ public fun <init> ()V
+ public fun <init> (Lkotlin/jvm/functions/Function1;)V
+ public synthetic fun <init> (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public fun onHeapDiffResult (Ljava/lang/Object;)V
+ public fun onHeapDumpClosed (Ljava/io/File;)V
+ public fun onHeapDumped (Ljava/io/File;)V
+}
+
+public final class leakcanary/HeapDumpStorageStrategy$KeepHeapDumps : leakcanary/HeapDumpStorageStrategy {
+ public static final field INSTANCE Lleakcanary/HeapDumpStorageStrategy$KeepHeapDumps;
+ public fun onHeapDiffResult (Ljava/lang/Object;)V
+ public fun onHeapDumpClosed (Ljava/io/File;)V
+ public fun onHeapDumped (Ljava/io/File;)V
+}
+
+public final class leakcanary/HeapDumpStorageStrategy$KeepHeapDumpsOnObjectsGrowing : leakcanary/HeapDumpStorageStrategy {
+ public fun <init> ()V
+ public fun <init> (Lkotlin/jvm/functions/Function1;)V
+ public synthetic fun <init> (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public fun onHeapDiffResult (Ljava/lang/Object;)V
+ public fun onHeapDumpClosed (Ljava/io/File;)V
+ public fun onHeapDumped (Ljava/io/File;)V
+}
+
+public final class leakcanary/HeapDumpStorageStrategy$KeepZippedHeapDumpsOnObjectsGrowing : leakcanary/HeapDumpStorageStrategy {
+ public fun <init> ()V
+ public fun <init> (Lkotlin/jvm/functions/Function1;)V
+ public synthetic fun <init> (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public fun onHeapDiffResult (Ljava/lang/Object;)V
+ public fun onHeapDumpClosed (Ljava/io/File;)V
+ public fun onHeapDumped (Ljava/io/File;)V
+}
+
public abstract interface class leakcanary/HeapDumper {
public static final field Companion Lleakcanary/HeapDumper$Companion;
public abstract fun dumpHeap (Ljava/io/File;)V
diff --git a/leakcanary/leakcanary-core/src/main/java/leakcanary/DumpingHeapGraphProvider.kt b/leakcanary/leakcanary-core/src/main/java/leakcanary/DumpingHeapGraphProvider.kt
deleted file mode 100644
index c805bd9..0000000
--- a/leakcanary/leakcanary-core/src/main/java/leakcanary/DumpingHeapGraphProvider.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package leakcanary
-
-import java.io.File
-import leakcanary.DumpingHeapGraphProvider.HeapDumpClosedListener
-import shark.CloseableHeapGraph
-import shark.HeapGraphProvider
-import shark.HprofHeapGraph.Companion.openHeapGraph
-
-class DumpingHeapGraphProvider(
- private val heapDumpFileProvider: HeapDumpFileProvider,
- private val heapDumper: HeapDumper,
- private val heapDumpClosedListener: HeapDumpClosedListener = HeapDumpClosedListener {}
-) : 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() {
- try {
- realGraph.close()
- } finally {
- heapDumpClosedListener.onHeapDumpClosed(heapDumpFile)
- }
- }
- }
- }
-
- fun interface HeapDumpClosedListener {
- fun onHeapDumpClosed(heapDumpFile: File)
- }
-}
diff --git a/leakcanary/leakcanary-core/src/main/java/leakcanary/DumpingRepeatingScenarioObjectGrowthDetector.kt b/leakcanary/leakcanary-core/src/main/java/leakcanary/DumpingRepeatingScenarioObjectGrowthDetector.kt
new file mode 100644
index 0000000..96641f4
--- /dev/null
+++ b/leakcanary/leakcanary-core/src/main/java/leakcanary/DumpingRepeatingScenarioObjectGrowthDetector.kt
@@ -0,0 +1,92 @@
+package leakcanary
+
+import java.io.File
+import shark.HeapDiff
+import shark.HeapTraversalInput
+import shark.HeapTraversalOutput
+import shark.HprofHeapGraph.Companion.openHeapGraph
+import shark.InitialState
+import shark.ObjectGrowthDetector
+import shark.RepeatingScenarioObjectGrowthDetector
+import shark.SharkLog
+
+/**
+ * A [RepeatingScenarioObjectGrowthDetector] suitable for junit based automated tests that
+ * can dump the heap.
+ *
+ * @see [RepeatingScenarioObjectGrowthDetector.findRepeatedlyGrowingObjects]
+ */
+class DumpingRepeatingScenarioObjectGrowthDetector(
+ private val objectGrowthDetector: ObjectGrowthDetector,
+ private val heapDumpFileProvider: HeapDumpFileProvider,
+ private val heapDumper: HeapDumper,
+ private val heapDumpStorageStrategy: HeapDumpStorageStrategy,
+) : RepeatingScenarioObjectGrowthDetector {
+
+ override fun findRepeatedlyGrowingObjects(
+ maxHeapDumps: Int,
+ scenarioLoopsPerDump: Int,
+ roundTripScenario: () -> Unit
+ ): HeapDiff {
+ val heapDiff = try {
+ findRepeatedlyGrowingObjectsInner(scenarioLoopsPerDump, maxHeapDumps, roundTripScenario)
+ } catch (exception: Throwable) {
+ heapDumpStorageStrategy.onHeapDiffResult(Result.failure(exception))
+ throw exception
+ }
+ heapDumpStorageStrategy.onHeapDiffResult(Result.success(heapDiff))
+ return heapDiff
+ }
+
+ private fun findRepeatedlyGrowingObjectsInner(
+ scenarioLoopsPerDump: Int,
+ maxHeapDumps: Int,
+ roundTripScenario: () -> Unit
+ ): HeapDiff {
+ var lastTraversalOutput: HeapTraversalInput = InitialState(scenarioLoopsPerDump)
+ for (i in 1..maxHeapDumps) {
+ repeat(scenarioLoopsPerDump) {
+ roundTripScenario()
+ }
+ val heapDumpFile = heapDumpFileProvider.newHeapDumpFile()
+ heapDumper.dumpHeap(heapDumpFile)
+ check(heapDumpFile.exists()) {
+ "Expected file to exist after heap dump: ${heapDumpFile.absolutePath}"
+ }
+ heapDumpStorageStrategy.onHeapDumped(heapDumpFile)
+ lastTraversalOutput = try {
+ heapDumpFile.findGrowingObjects(lastTraversalOutput)
+ } finally {
+ heapDumpStorageStrategy.onHeapDumpClosed(heapDumpFile)
+ }
+ if (lastTraversalOutput is HeapDiff) {
+ if (!lastTraversalOutput.isGrowing) {
+ return lastTraversalOutput
+ } else if (i < maxHeapDumps) {
+ // Log unless it's the last diff, which typically gets printed by calling code.
+ SharkLog.d {
+ "After ${lastTraversalOutput.traversalCount} heap dumps with $scenarioLoopsPerDump scenario iterations before each, " +
+ "${lastTraversalOutput.growingObjects.size} growing nodes:\n" + lastTraversalOutput.growingObjects
+ }
+ }
+ }
+ }
+ check(lastTraversalOutput is HeapDiff) {
+ "Final output should be a HeapGrowth, traversalCount ${lastTraversalOutput.traversalCount - 1} " +
+ "should be >= 2. Output: $lastTraversalOutput"
+ }
+ return lastTraversalOutput
+ }
+
+ private fun File.findGrowingObjects(
+ previousTraversal: HeapTraversalInput
+ ): HeapTraversalOutput {
+ return openHeapGraph().use { heapGraph ->
+ objectGrowthDetector.findGrowingObjects(
+ heapGraph = heapGraph,
+ previousTraversal = previousTraversal,
+ )
+ }
+ }
+}
+
diff --git a/leakcanary/leakcanary-core/src/main/java/leakcanary/HeapDumpDirectoryProvider.kt b/leakcanary/leakcanary-core/src/main/java/leakcanary/HeapDumpDirectoryProvider.kt
index a9ad851..fdb6f36 100644
--- a/leakcanary/leakcanary-core/src/main/java/leakcanary/HeapDumpDirectoryProvider.kt
+++ b/leakcanary/leakcanary-core/src/main/java/leakcanary/HeapDumpDirectoryProvider.kt
@@ -4,7 +4,7 @@
fun interface HeapDumpDirectoryProvider {
/**
- * Expected to be only once per [HeapDumpFileProvider] implementation instance.
+ * Expected to be called only once per [HeapDumpFileProvider] implementation instance.
*/
fun heapDumpDirectory(): File
}
diff --git a/leakcanary/leakcanary-core/src/main/java/leakcanary/HeapDumpStorageStrategy.kt b/leakcanary/leakcanary-core/src/main/java/leakcanary/HeapDumpStorageStrategy.kt
new file mode 100644
index 0000000..4a7dbb7
--- /dev/null
+++ b/leakcanary/leakcanary-core/src/main/java/leakcanary/HeapDumpStorageStrategy.kt
@@ -0,0 +1,139 @@
+package leakcanary
+
+import java.io.File
+import java.util.zip.ZipEntry
+import java.util.zip.ZipOutputStream
+import shark.HeapDiff
+import shark.SharkLog
+
+interface HeapDumpStorageStrategy {
+
+ /**
+ * Deletes heap dumps as soon as we're done traversing them. This is the most disk space
+ * efficient strategy.
+ */
+ class DeleteOnHeapDumpClose(
+ private val deleteFile: (File) -> Unit = { it.delete() }
+ ) : HeapDumpStorageStrategy {
+ override fun onHeapDumpClosed(heapDumpFile: File) {
+ SharkLog.d { "DeleteOnHeapDumpClose: deleting closed heap dump ${heapDumpFile.absolutePath}" }
+ deleteFile(heapDumpFile)
+ }
+ }
+
+ /**
+ * No deletion of heap dump files. This is useful if you intend to open up the heap dumps
+ * directly or re run the analysis no matter the outcome.
+ */
+ object KeepHeapDumps : HeapDumpStorageStrategy
+
+ /**
+ * Keeps the heap dumps until we're done diffing, then delete them only if there are no growing
+ * objects. This is useful if you intend to open up the heap dumps directly or re run
+ * the analysis on failure.
+ */
+ class KeepHeapDumpsOnObjectsGrowing(
+ private val deleteFile: (File) -> Unit = { it.delete() }
+ ) : HeapDumpStorageStrategy {
+ // This assumes the detector instance is always used from the same thread, which seems like a
+ // safe enough assumption for tests.
+ private val heapDumpFiles = mutableListOf<File>()
+
+ override fun onHeapDumped(heapDumpFile: File) {
+ heapDumpFiles += heapDumpFile
+ }
+
+ override fun onHeapDiffResult(result: Result<HeapDiff>) {
+ if (result.isSuccess && !result.getOrThrow().isGrowing) {
+ SharkLog.d {
+ "KeepHeapDumpsOnObjectsGrowing: not growing, deleting heap dumps:" +
+ heapDumpFiles.joinToString(
+ prefix = "\n",
+ separator = "\n"
+ ) { it.absolutePath }
+ }
+ heapDumpFiles.forEach {
+ deleteFile(it)
+ }
+ } else {
+ SharkLog.d {
+ "KeepHeapDumpsOnObjectsGrowing: failure or growing, keeping heap dumps:" +
+ heapDumpFiles.joinToString(
+ prefix = "\n",
+ separator = "\n"
+ ) { it.absolutePath }
+ }
+ }
+ heapDumpFiles.clear()
+ }
+ }
+
+ /**
+ * Keeps the heap dumps until we're done diffing, then on completion creates a zip for each heap
+ * dump if there are growing object, and delete all the source heap dumps.
+ * This is useful if you intend to upload the heap dumps on failure in CI and you
+ * want to keep disk space, network usage and cloud storage low. Zipped heap dumps are typically
+ * 4x smaller so this is worth it, although the trade off is that zipping can add a few seconds
+ * per heap dump to the runtime duration of a test.
+ */
+ class KeepZippedHeapDumpsOnObjectsGrowing(
+ private val deleteFile: (File) -> Unit = { it.delete() }
+ ) : HeapDumpStorageStrategy {
+ // This assumes the detector instance is always used from the same thread, which seems like a
+ // safe enough assumption for tests.
+ private val heapDumpFiles = mutableListOf<File>()
+
+ override fun onHeapDumped(heapDumpFile: File) {
+ heapDumpFiles += heapDumpFile
+ }
+
+ override fun onHeapDiffResult(result: Result<HeapDiff>) {
+ if (result.isFailure || result.getOrThrow().isGrowing) {
+ SharkLog.d {
+ "KeepZippedHeapDumpsOnObjectsGrowing: failure or growing, zipping heap dumps:" +
+ heapDumpFiles.joinToString(
+ prefix = "\n",
+ separator = "\n"
+ ) { it.absolutePath }
+ }
+ heapDumpFiles.forEach {
+ it.zipFile()
+ }
+ } else {
+ SharkLog.d {
+ "KeepZippedHeapDumpsOnObjectsGrowing: not growing, deleting heap dumps:" +
+ heapDumpFiles.joinToString(
+ prefix = "\n",
+ separator = "\n"
+ ) { it.absolutePath }
+ }
+ }
+ heapDumpFiles.forEach {
+ deleteFile(it)
+ }
+ heapDumpFiles.clear()
+ }
+
+ private fun File.zipFile(destination: File = File(parent, "$nameWithoutExtension.zip")): File {
+ ZipOutputStream(destination.outputStream()).use { zipOutputStream ->
+ zipOutputStream.putNextEntry(ZipEntry(name))
+ inputStream().use {
+ it.copyTo(
+ out = zipOutputStream,
+ // 200 KB, an optimal buffer size from experimenting with different buffer sizes for
+ // a 41 MB heap dump on a Pixel 7.
+ // https://publicobject.com/2020/09/14/many-correct-answers/
+ bufferSize = 200_000
+ )
+ }
+ }
+ return destination
+ }
+ }
+
+ fun onHeapDumpClosed(heapDumpFile: File) = Unit
+
+ fun onHeapDumped(heapDumpFile: File) = Unit
+
+ fun onHeapDiffResult(result: Result<HeapDiff>) = Unit
+}
diff --git a/leakcanary/leakcanary-core/src/main/java/leakcanary/ObjectGrowthWarmupHeapDumper.kt b/leakcanary/leakcanary-core/src/main/java/leakcanary/ObjectGrowthWarmupHeapDumper.kt
index 0bc54bd..26423fd 100644
--- a/leakcanary/leakcanary-core/src/main/java/leakcanary/ObjectGrowthWarmupHeapDumper.kt
+++ b/leakcanary/leakcanary-core/src/main/java/leakcanary/ObjectGrowthWarmupHeapDumper.kt
@@ -3,10 +3,10 @@
import java.io.File
import okio.ByteString.Companion.decodeHex
import shark.ByteArraySourceProvider
+import shark.HeapTraversalInput
import shark.HprofHeapGraph.Companion.openHeapGraph
+import shark.InitialState
import shark.ObjectGrowthDetector
-import shark.RepeatingHeapGraphObjectGrowthDetector
-import shark.RepeatingScenarioObjectGrowthDetector
class ObjectGrowthWarmupHeapDumper(
private val objectGrowthDetector: ObjectGrowthDetector,
@@ -25,22 +25,19 @@
}
private fun warmup() {
- val heapDumpsAsHex = listOf({ heapDump1Hex(androidHeap) }, { heapDump2Hex(androidHeap) },
- { heapDump3Hex(androidHeap) })
- val heapDumpsAsHexIterator = heapDumpsAsHex.iterator()
- val warmupDetector = RepeatingScenarioObjectGrowthDetector(
- heapGraphProvider = {
- ByteArraySourceProvider(
- heapDumpsAsHexIterator.next()().decodeHex().toByteArray()
- ).openHeapGraph()
- },
- repeatingHeapGraphDetector = RepeatingHeapGraphObjectGrowthDetector(objectGrowthDetector),
+ val heapDumpsAsHex = listOf(
+ { heapDump1Hex(androidHeap) },
+ { heapDump2Hex(androidHeap) },
+ { heapDump3Hex(androidHeap) }
)
- warmupDetector.findRepeatedlyGrowingObjects(
- maxHeapDumps = heapDumpsAsHex.size,
- scenarioLoopsPerDump = 1,
- roundTripScenario = {}
- )
+ var lastTraversalOutput: HeapTraversalInput = InitialState(1)
+ for (heapDumpAsHex in heapDumpsAsHex) {
+ lastTraversalOutput = ByteArraySourceProvider(
+ heapDumpAsHex().decodeHex().toByteArray()
+ ).openHeapGraph().use { heapGraph ->
+ objectGrowthDetector.findGrowingObjects(heapGraph, lastTraversalOutput)
+ }
+ }
}
@SuppressWarnings("MaxLineLength")
diff --git a/leakcanary/leakcanary-core/src/test/java/leakcanary/DumpingRepeatingScenarioObjectGrowthDetectorTest.kt b/leakcanary/leakcanary-core/src/test/java/leakcanary/DumpingRepeatingScenarioObjectGrowthDetectorTest.kt
new file mode 100644
index 0000000..8a7a125
--- /dev/null
+++ b/leakcanary/leakcanary-core/src/test/java/leakcanary/DumpingRepeatingScenarioObjectGrowthDetectorTest.kt
@@ -0,0 +1,165 @@
+package leakcanary
+
+import java.io.File
+import leakcanary.HeapDumpStorageStrategy.DeleteOnHeapDumpClose
+import leakcanary.HeapDumpStorageStrategy.KeepHeapDumps
+import leakcanary.HeapDumpStorageStrategy.KeepHeapDumpsOnObjectsGrowing
+import leakcanary.HeapDumpStorageStrategy.KeepZippedHeapDumpsOnObjectsGrowing
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import shark.GcRoot
+import shark.GcRootReference
+import shark.JvmObjectGrowthReferenceMatchers
+import shark.ObjectGrowthDetector
+import shark.OpenJdkReferenceReaderFactory
+import shark.dump
+import shark.forJvmHeap
+
+class DumpingRepeatingScenarioObjectGrowthDetectorTest {
+
+ @get:Rule
+ val tempFolder = TemporaryFolder()
+
+ @Test
+ fun `DeleteOnHeapDumpClose deletes heap dump on indexing failure`() {
+ val heapDumpDirectory = tempFolder.newFolder()
+
+ DeleteOnHeapDumpClose().triggerIndexingFailure(heapDumpDirectory)
+
+ assertThat(heapDumpDirectory.listFiles()).isEmpty()
+ }
+
+ @Test
+ fun `DeleteOnHeapDumpClose deletes heap dump on traversal failure`() {
+ val heapDumpDirectory = tempFolder.newFolder()
+
+ DeleteOnHeapDumpClose().triggerTraversalFailure(heapDumpDirectory)
+
+ assertThat(heapDumpDirectory.listFiles()).isEmpty()
+ }
+
+ @Test
+ fun `KeepHeapDumps keeps heap dump on indexing failure`() {
+ val heapDumpDirectory = tempFolder.newFolder()
+
+ KeepHeapDumps.triggerIndexingFailure(heapDumpDirectory)
+
+ assertThat(heapDumpDirectory.listFiles()).containsExactly(
+ File(heapDumpDirectory, "dump-1.hprof")
+ )
+ }
+
+ @Test
+ fun `KeepHeapDumps keeps heap dump on traversal failure`() {
+ val heapDumpDirectory = tempFolder.newFolder()
+
+ KeepHeapDumps.triggerTraversalFailure(heapDumpDirectory)
+
+ assertThat(heapDumpDirectory.listFiles()).containsExactly(
+ File(heapDumpDirectory, "dump-1.hprof")
+ )
+ }
+
+ @Test
+ fun `KeepHeapDumpsOnObjectsGrowing keeps heap dump on indexing failure`() {
+ val heapDumpDirectory = tempFolder.newFolder()
+
+ KeepHeapDumpsOnObjectsGrowing().triggerIndexingFailure(heapDumpDirectory)
+
+ assertThat(heapDumpDirectory.listFiles()).containsExactly(
+ File(heapDumpDirectory, "dump-1.hprof")
+ )
+ }
+
+ @Test
+ fun `KeepHeapDumpsOnObjectsGrowing keeps heap dump on traversal failure`() {
+ val heapDumpDirectory = tempFolder.newFolder()
+
+ KeepHeapDumpsOnObjectsGrowing().triggerTraversalFailure(heapDumpDirectory)
+
+ assertThat(heapDumpDirectory.listFiles()).containsExactly(
+ File(heapDumpDirectory, "dump-1.hprof")
+ )
+ }
+
+ @Test
+ fun `KeepZippedHeapDumpsOnObjectsGrowing keeps zipped heap dump on indexing failure`() {
+ val heapDumpDirectory = tempFolder.newFolder()
+
+ KeepZippedHeapDumpsOnObjectsGrowing().triggerIndexingFailure(heapDumpDirectory)
+
+ assertThat(heapDumpDirectory.listFiles()).containsExactly(
+ File(heapDumpDirectory, "dump-1.zip")
+ )
+ }
+
+ @Test
+ fun `KeepZippedHeapDumpsOnObjectsGrowing keeps zipped heap dump on traversal failure`() {
+ val heapDumpDirectory = tempFolder.newFolder()
+
+ KeepZippedHeapDumpsOnObjectsGrowing().triggerTraversalFailure(heapDumpDirectory)
+
+ assertThat(heapDumpDirectory.listFiles()).containsExactly(
+ File(heapDumpDirectory, "dump-1.zip")
+ )
+ }
+
+ private fun HeapDumpStorageStrategy.triggerIndexingFailure(heapDumpDirectory: File) {
+ val heapDumpFiles = generateSequence(1) {
+ it + 1
+ }.map { File(heapDumpDirectory, "dump-$it.hprof") }.iterator()
+
+ val detector = DumpingRepeatingScenarioObjectGrowthDetector(
+ objectGrowthDetector = ObjectGrowthDetector.forJvmHeap(),
+ heapDumpFileProvider = {
+ heapDumpFiles.next()
+ },
+ heapDumper = {
+ // bad heap dump
+ it.writeText("I like turtles")
+ },
+ heapDumpStorageStrategy = this,
+ )
+
+
+ try {
+ detector.findRepeatedlyGrowingObjects {
+ }
+ } catch (ignored: Throwable) {
+ }
+ }
+
+ private fun HeapDumpStorageStrategy.triggerTraversalFailure(heapDumpDirectory: File) {
+ val unknownObjectId = 42L
+ val heapDumpFiles = generateSequence(1) {
+ it + 1
+ }.map { File(heapDumpDirectory, "dump-$it.hprof") }.iterator()
+
+ val referenceMatchers = JvmObjectGrowthReferenceMatchers.defaults
+ val detector = DumpingRepeatingScenarioObjectGrowthDetector(
+ objectGrowthDetector = ObjectGrowthDetector(
+ gcRootProvider = {
+ // Fake GC Root
+ sequenceOf(GcRootReference(GcRoot.StickyClass(unknownObjectId), false, null))
+ },
+ referenceReaderFactory = OpenJdkReferenceReaderFactory(referenceMatchers)
+ ),
+ heapDumpFileProvider = {
+ heapDumpFiles.next()
+ },
+ heapDumper = {
+ // bad heap dump
+ it.dump { }
+ },
+ heapDumpStorageStrategy = this,
+ )
+
+ assertThatThrownBy {
+ detector.findRepeatedlyGrowingObjects {
+ }
+ }.hasMessageContaining(unknownObjectId.toString())
+ }
+}
diff --git a/leakcanary/leakcanary-jvm-test/src/main/java/leakcanary/RepeatingJvmInProcessScenario.kt b/leakcanary/leakcanary-jvm-test/src/main/java/leakcanary/RepeatingJvmInProcessScenario.kt
index 8b359e1..215bb2b 100644
--- a/leakcanary/leakcanary-jvm-test/src/main/java/leakcanary/RepeatingJvmInProcessScenario.kt
+++ b/leakcanary/leakcanary-jvm-test/src/main/java/leakcanary/RepeatingJvmInProcessScenario.kt
@@ -23,9 +23,9 @@
.withDetectorWarmup(objectGrowthDetector, androidHeap = false),
heapDumpStorageStrategy: HeapDumpStorageStrategy = HeapDumpStorageStrategy.DeleteOnHeapDumpClose(),
): RepeatingScenarioObjectGrowthDetector {
- return repeatingDumpingTestScenario(
+ return DumpingRepeatingScenarioObjectGrowthDetector(
objectGrowthDetector = objectGrowthDetector,
- heapDumpDirectoryProvider = heapDumpDirectoryProvider,
+ heapDumpFileProvider = TestHeapDumpFileProvider(heapDumpDirectoryProvider),
heapDumper = heapDumper,
heapDumpStorageStrategy = heapDumpStorageStrategy,
)
diff --git a/leakcanary/leakcanary-jvm-test/src/test/java/leakcanary/JvmLiveObjectGrowthDetectorTest.kt b/leakcanary/leakcanary-jvm-test/src/test/java/leakcanary/JvmLiveObjectGrowthDetectorTest.kt
index a2d3e9e..3c113d4 100644
--- a/leakcanary/leakcanary-jvm-test/src/test/java/leakcanary/JvmLiveObjectGrowthDetectorTest.kt
+++ b/leakcanary/leakcanary-jvm-test/src/test/java/leakcanary/JvmLiveObjectGrowthDetectorTest.kt
@@ -19,9 +19,6 @@
class CustomLinkedList(var next: CustomLinkedList? = null)
- @get:Rule
- val testFolder = TemporaryFolder()
-
val leakies = mutableListOf<Any>()
val stringLeaks = mutableListOf<String>()
diff --git a/leakcanary/leakcanary-test-core/api/leakcanary-test-core.api b/leakcanary/leakcanary-test-core/api/leakcanary-test-core.api
index 9a70ee2..64902e0 100644
--- a/leakcanary/leakcanary-test-core/api/leakcanary-test-core.api
+++ b/leakcanary/leakcanary-test-core/api/leakcanary-test-core.api
@@ -1,47 +1,3 @@
-public abstract interface class leakcanary/HeapDumpStorageStrategy : leakcanary/DumpingHeapGraphProvider$HeapDumpClosedListener, shark/RepeatingHeapGraphObjectGrowthDetector$CompletionListener {
- public abstract fun onHeapDumpClosed (Ljava/io/File;)V
- public abstract fun onObjectGrowthDetectionComplete (Lshark/HeapDiff;)V
-}
-
-public final class leakcanary/HeapDumpStorageStrategy$DefaultImpls {
- public static fun onHeapDumpClosed (Lleakcanary/HeapDumpStorageStrategy;Ljava/io/File;)V
- public static fun onObjectGrowthDetectionComplete (Lleakcanary/HeapDumpStorageStrategy;Lshark/HeapDiff;)V
-}
-
-public final class leakcanary/HeapDumpStorageStrategy$DeleteOnHeapDumpClose : leakcanary/HeapDumpStorageStrategy {
- public fun <init> ()V
- public fun <init> (Lkotlin/jvm/functions/Function1;)V
- public synthetic fun <init> (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
- public fun onHeapDumpClosed (Ljava/io/File;)V
- public fun onObjectGrowthDetectionComplete (Lshark/HeapDiff;)V
-}
-
-public final class leakcanary/HeapDumpStorageStrategy$KeepHeapDumps : leakcanary/HeapDumpStorageStrategy {
- public static final field INSTANCE Lleakcanary/HeapDumpStorageStrategy$KeepHeapDumps;
- public fun onHeapDumpClosed (Ljava/io/File;)V
- public fun onObjectGrowthDetectionComplete (Lshark/HeapDiff;)V
-}
-
-public final class leakcanary/HeapDumpStorageStrategy$KeepHeapDumpsOnObjectsGrowing : leakcanary/HeapDumpStorageStrategy {
- public fun <init> ()V
- public fun <init> (Lkotlin/jvm/functions/Function1;)V
- public synthetic fun <init> (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
- public fun onHeapDumpClosed (Ljava/io/File;)V
- public fun onObjectGrowthDetectionComplete (Lshark/HeapDiff;)V
-}
-
-public final class leakcanary/HeapDumpStorageStrategy$KeepZippedHeapDumpsOnObjectsGrowing : leakcanary/HeapDumpStorageStrategy {
- public fun <init> ()V
- public fun <init> (Lkotlin/jvm/functions/Function1;)V
- public synthetic fun <init> (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
- public fun onHeapDumpClosed (Ljava/io/File;)V
- public fun onObjectGrowthDetectionComplete (Lshark/HeapDiff;)V
-}
-
-public final class leakcanary/RepeatingScenarioKt {
- public static final fun repeatingDumpingTestScenario (Lshark/HeapDiff$Companion;Lshark/ObjectGrowthDetector;Lleakcanary/HeapDumpDirectoryProvider;Lleakcanary/HeapDumper;Lleakcanary/HeapDumpStorageStrategy;)Lshark/RepeatingScenarioObjectGrowthDetector;
-}
-
public final class leakcanary/TestDescriptionHolder : org/junit/rules/TestRule {
public static final field INSTANCE Lleakcanary/TestDescriptionHolder;
public fun apply (Lorg/junit/runners/model/Statement;Lorg/junit/runner/Description;)Lorg/junit/runners/model/Statement;
@@ -50,6 +6,11 @@
public final fun wrap (Lorg/junit/runners/model/Statement;Lorg/junit/runner/Description;)Lorg/junit/runners/model/Statement;
}
+public final class leakcanary/TestHeapDumpFileProvider : leakcanary/HeapDumpFileProvider {
+ public fun <init> (Lleakcanary/HeapDumpDirectoryProvider;)V
+ public fun newHeapDumpFile ()Ljava/io/File;
+}
+
public abstract interface class leakcanary/TestName {
public abstract fun getClassName ()Ljava/lang/String;
public abstract fun getClassSimpleName ()Ljava/lang/String;
diff --git a/leakcanary/leakcanary-test-core/src/main/java/leakcanary/HeapDumpStorageStrategy.kt b/leakcanary/leakcanary-test-core/src/main/java/leakcanary/HeapDumpStorageStrategy.kt
deleted file mode 100644
index 467aed0..0000000
--- a/leakcanary/leakcanary-test-core/src/main/java/leakcanary/HeapDumpStorageStrategy.kt
+++ /dev/null
@@ -1,107 +0,0 @@
-package leakcanary
-
-import java.io.File
-import java.util.zip.ZipEntry
-import java.util.zip.ZipOutputStream
-import shark.HeapDiff
-import shark.RepeatingHeapGraphObjectGrowthDetector
-
-interface HeapDumpStorageStrategy : DumpingHeapGraphProvider.HeapDumpClosedListener,
- RepeatingHeapGraphObjectGrowthDetector.CompletionListener {
-
- /**
- * Deletes heap dumps as soon as we're done traversing them. This is the most disk space
- * efficient strategy.
- */
- class DeleteOnHeapDumpClose(
- private val deleteFile: (File) -> Unit = { it.delete() }
- ) : HeapDumpStorageStrategy {
- override fun onHeapDumpClosed(heapDumpFile: File) {
- deleteFile(heapDumpFile)
- }
- }
-
- /**
- * No deletion of heap dump files. This is useful if you intend to open up the heap dumps
- * directly or re run the analysis no matter the outcome.
- */
- object KeepHeapDumps : HeapDumpStorageStrategy
-
- /**
- * Keeps the heap dumps until we're done diffing, then delete them only if there are no growing
- * objects. This is useful if you intend to open up the heap dumps directly or re run
- * the analysis on failure.
- */
- class KeepHeapDumpsOnObjectsGrowing(
- private val deleteFile: (File) -> Unit = { it.delete() }
- ) : HeapDumpStorageStrategy {
- // This assumes the detector instance is always used from the same thread, which seems like a
- // safe enough assumption for tests.
- private val closedHeapDumpFiles = mutableListOf<File>()
-
- override fun onHeapDumpClosed(heapDumpFile: File) {
- closedHeapDumpFiles += heapDumpFile
- }
-
- override fun onObjectGrowthDetectionComplete(result: HeapDiff) {
- if (!result.isGrowing) {
- closedHeapDumpFiles.forEach {
- deleteFile(it)
- }
- }
- closedHeapDumpFiles.clear()
- }
- }
-
- /**
- * Keeps the heap dumps until we're done diffing, then on completion creates a zip for each heap
- * dump if there are growing object, and delete all the source heap dumps.
- * This is useful if you intend to upload the heap dumps on failure in CI and you
- * want to keep disk space, network usage and cloud storage low. Zipped heap dumps are typically
- * 4x smaller so this is worth it, although the trade off is that zipping can add a few seconds
- * per heap dump to the runtime duration of a test.
- */
- class KeepZippedHeapDumpsOnObjectsGrowing(
- private val deleteFile: (File) -> Unit = { it.delete() }
- ) : HeapDumpStorageStrategy {
- // This assumes the detector instance is always used from the same thread, which seems like a
- // safe enough assumption for tests.
- private val closedHeapDumpFiles = mutableListOf<File>()
-
- override fun onHeapDumpClosed(heapDumpFile: File) {
- closedHeapDumpFiles += heapDumpFile
- }
-
- override fun onObjectGrowthDetectionComplete(result: HeapDiff) {
- if (result.isGrowing) {
- closedHeapDumpFiles.forEach {
- it.zipFile()
- }
- }
- closedHeapDumpFiles.forEach {
- deleteFile(it)
- }
- closedHeapDumpFiles.clear()
- }
-
- private fun File.zipFile(destination: File = File(parent, "$nameWithoutExtension.zip")): File {
- ZipOutputStream(destination.outputStream()).use { zipOutputStream ->
- zipOutputStream.putNextEntry(ZipEntry(name))
- inputStream().use {
- it.copyTo(
- out = zipOutputStream,
- // 200 KB, an optimal buffer size from experimenting with different buffer sizes for
- // a 41 MB heap dump on a Pixel 7.
- // https://publicobject.com/2020/09/14/many-correct-answers/
- bufferSize = 200_000
- )
- }
- }
- return destination
- }
- }
-
- override fun onHeapDumpClosed(heapDumpFile: File) = Unit
-
- override fun onObjectGrowthDetectionComplete(result: HeapDiff) = Unit
-}
diff --git a/leakcanary/leakcanary-test-core/src/main/java/leakcanary/RepeatingScenario.kt b/leakcanary/leakcanary-test-core/src/main/java/leakcanary/RepeatingScenario.kt
deleted file mode 100644
index 009503a..0000000
--- a/leakcanary/leakcanary-test-core/src/main/java/leakcanary/RepeatingScenario.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package leakcanary
-
-import shark.HeapDiff
-import shark.ObjectGrowthDetector
-import shark.RepeatingHeapGraphObjectGrowthDetector
-import shark.RepeatingScenarioObjectGrowthDetector
-
-/**
- * Creates a [RepeatingScenarioObjectGrowthDetector] suitable for junit based automated tests that
- * can dump the heap.
- *
- * @see [RepeatingScenarioObjectGrowthDetector.findRepeatedlyGrowingObjects]
- */
-fun HeapDiff.Companion.repeatingDumpingTestScenario(
- objectGrowthDetector: ObjectGrowthDetector,
- heapDumpDirectoryProvider: HeapDumpDirectoryProvider,
- heapDumper: HeapDumper,
- heapDumpStorageStrategy: HeapDumpStorageStrategy,
-): RepeatingScenarioObjectGrowthDetector {
- return RepeatingScenarioObjectGrowthDetector(
- heapGraphProvider = DumpingHeapGraphProvider(
- heapDumpFileProvider = DatetimeFormattedHeapDumpFileProvider(
- heapDumpDirectoryProvider = heapDumpDirectoryProvider,
- suffixProvider = {
- TestNameProvider.currentTestName()?.run {
- // JVM test method names can have spaces.
- val escapedMethodName = methodName.replace(' ', '-')
- "_${classSimpleName}-${escapedMethodName}"
- } ?: ""
- }
- ),
- heapDumper = heapDumper,
- heapDumpClosedListener = heapDumpStorageStrategy
- ),
- repeatingHeapGraphDetector = RepeatingHeapGraphObjectGrowthDetector(
- objectGrowthDetector = objectGrowthDetector,
- completionListener = heapDumpStorageStrategy
- ),
- )
-}
-
diff --git a/leakcanary/leakcanary-test-core/src/main/java/leakcanary/TestHeapDumpFileProvider.kt b/leakcanary/leakcanary-test-core/src/main/java/leakcanary/TestHeapDumpFileProvider.kt
new file mode 100644
index 0000000..ff0bc09
--- /dev/null
+++ b/leakcanary/leakcanary-test-core/src/main/java/leakcanary/TestHeapDumpFileProvider.kt
@@ -0,0 +1,19 @@
+package leakcanary
+
+class TestHeapDumpFileProvider(
+ heapDumpDirectoryProvider: HeapDumpDirectoryProvider
+) : HeapDumpFileProvider {
+
+ private val delegate = DatetimeFormattedHeapDumpFileProvider(
+ heapDumpDirectoryProvider = heapDumpDirectoryProvider,
+ suffixProvider = {
+ TestNameProvider.currentTestName()?.run {
+ // JVM test method names can have spaces.
+ val escapedMethodName = methodName.replace(' ', '-')
+ "_${classSimpleName}-${escapedMethodName}"
+ } ?: ""
+ }
+ )
+
+ override fun newHeapDumpFile() = delegate.newHeapDumpFile()
+}
diff --git a/shark/shark-cli/src/main/java/shark/HeapGrowthCommand.kt b/shark/shark-cli/src/main/java/shark/HeapGrowthCommand.kt
index db8db0c..2736fa4 100644
--- a/shark/shark-cli/src/main/java/shark/HeapGrowthCommand.kt
+++ b/shark/shark-cli/src/main/java/shark/HeapGrowthCommand.kt
@@ -87,35 +87,33 @@
"order:\n${hprofFiles.joinToString("\n") { it.name }}"
)
- var previous: ConstantMemoryMetricsDualSourceProvider? = null
- var previousStartTime = 0L.nanoseconds
-
- val heapGraphs = hprofFiles.asSequence().map { file ->
- val openTime = System.nanoTime().nanoseconds
- previous?.let {
- metrics += Metrics(
- it.randomAccessByteReads, it.randomAccessReadCount, openTime - previousStartTime
+ var lastTraversal: HeapTraversalInput = InitialState(scenarioLoopsPerDump)
+ for (hprofFile in hprofFiles) {
+ val start = System.nanoTime().nanoseconds
+ val sourceProvider =
+ ConstantMemoryMetricsDualSourceProvider(FileSourceProvider(hprofFile))
+ val heapTraversal = sourceProvider.openHeapGraph().use { heapGraph ->
+ androidDetector.findGrowingObjects(
+ heapGraph = heapGraph,
+ previousTraversal = lastTraversal,
)
}
- val sourceProvider = ConstantMemoryMetricsDualSourceProvider(FileSourceProvider(file))
- previous = sourceProvider
- previousStartTime = openTime
- sourceProvider.openHeapGraph()
- }
- val detector = RepeatingHeapGraphObjectGrowthDetector(androidDetector)
- val results = detector.findRepeatedlyGrowingObjects(
- scenarioLoopsPerGraph = scenarioLoopsPerDump,
- heapGraphSequence = heapGraphs,
- ).also {
- previous?.let {
- val finishTime = System.nanoTime().nanoseconds
- metrics += Metrics(
- it.randomAccessByteReads, it.randomAccessReadCount, finishTime - previousStartTime
- )
+ val duration = System.nanoTime().nanoseconds - start
+ metrics += Metrics(
+ sourceProvider.randomAccessByteReads, sourceProvider.randomAccessReadCount, duration
+ )
+ lastTraversal = heapTraversal
+ if (heapTraversal is HeapDiff && !heapTraversal.isGrowing) {
+ break
}
}
- echo("Results: $results")
- echo("Found ${results.growingObjects.size} growing objects")
+ val heapDiff = lastTraversal as HeapDiff
+ if (heapDiff.isGrowing) {
+ echo("Results: $heapDiff")
+ echo("Found ${heapDiff.growingObjects.size} growing objects")
+ } else {
+ echo("Results: not growing")
+ }
}
is ProcessSource -> {
@@ -123,10 +121,12 @@
ConsoleReader().readLine("Press ENTER to dump heap when ready to start")
- val firstTraversal = androidDetector.findGrowingObjects(
- heapGraph = source.dumpHeapAndOpenGraph(),
- previousTraversal = InitialState(scenarioLoopsPerDump)
- )
+ val firstTraversal = source.dumpHeapAndOpenGraph().use { heapGraph ->
+ androidDetector.findGrowingObjects(
+ heapGraph = heapGraph,
+ previousTraversal = InitialState(scenarioLoopsPerDump)
+ )
+ }
val nTimes = if (scenarioLoopsPerDump > 1) "$scenarioLoopsPerDump times" else "once"
@@ -151,7 +151,9 @@
while (promptForCommand) {
if (latestTraversal.isGrowing) {
echo("To keep going, go through scenario $nTimes.")
- echo("Then, either press ENTER or enter 'r' to reset and use the last heap dump as the new baseline.")
+ echo(
+ "Then, either press ENTER or enter 'r' to reset and use the last heap dump as the new baseline."
+ )
echo("To quit, enter 'q'.")
val command = consoleReader.readCommand(
@@ -169,8 +171,12 @@
}
}
} else {
- echo("As the last heap dump found 0 growing objects, there's no point continuing with the same heap dump baseline.")
- echo("To keep going, go through scenario $nTimes then press ENTER to use the last heap dump as the NEW baseline.")
+ echo(
+ "As the last heap dump found 0 growing objects, there's no point continuing with the same heap dump baseline."
+ )
+ echo(
+ "To keep going, go through scenario $nTimes then press ENTER to use the last heap dump as the NEW baseline."
+ )
echo("To quit, enter 'q'.")
when (val command = consoleReader.readCommand()) {
"q" -> throw PrintMessage("Quitting.")
diff --git a/shark/shark-graph/api/shark-graph.api b/shark/shark-graph/api/shark-graph.api
index 6b3dd31..bc8adc3 100644
--- a/shark/shark-graph/api/shark-graph.api
+++ b/shark/shark-graph/api/shark-graph.api
@@ -44,14 +44,6 @@
public abstract fun objectExists (J)Z
}
-public abstract interface class shark/HeapGraphProvider {
- public static final field Companion Lshark/HeapGraphProvider$Companion;
- public abstract fun openHeapGraph ()Lshark/CloseableHeapGraph;
-}
-
-public final class shark/HeapGraphProvider$Companion {
-}
-
public abstract class shark/HeapObject {
public static final field Companion Lshark/HeapObject$Companion;
public final fun getAsClass ()Lshark/HeapObject$HeapClass;
diff --git a/shark/shark-graph/src/main/java/shark/HeapGraphProvider.kt b/shark/shark-graph/src/main/java/shark/HeapGraphProvider.kt
deleted file mode 100644
index c0dd691..0000000
--- a/shark/shark-graph/src/main/java/shark/HeapGraphProvider.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package shark
-
-fun interface HeapGraphProvider {
- fun openHeapGraph(): CloseableHeapGraph
-
- /**
- * This allows external modules to add factory methods for implementations of this interface as
- * extension functions of this companion object.
- */
- companion object
-}
diff --git a/shark/shark/api/shark.api b/shark/shark/api/shark.api
index cd43a59..222a67a 100644
--- a/shark/shark/api/shark.api
+++ b/shark/shark/api/shark.api
@@ -644,8 +644,8 @@
public final class shark/ObjectGrowthDetector {
public static final field Companion Lshark/ObjectGrowthDetector$Companion;
public fun <init> (Lshark/GcRootProvider;Lshark/ReferenceReader$Factory;)V
- public final fun findGrowingObjects (Lshark/CloseableHeapGraph;Lshark/HeapTraversalInput;)Lshark/HeapTraversalOutput;
- public static synthetic fun findGrowingObjects$default (Lshark/ObjectGrowthDetector;Lshark/CloseableHeapGraph;Lshark/HeapTraversalInput;ILjava/lang/Object;)Lshark/HeapTraversalOutput;
+ public final fun findGrowingObjects (Lshark/HeapGraph;Lshark/HeapTraversalInput;)Lshark/HeapTraversalOutput;
+ public static synthetic fun findGrowingObjects$default (Lshark/ObjectGrowthDetector;Lshark/HeapGraph;Lshark/HeapTraversalInput;ILjava/lang/Object;)Lshark/HeapTraversalOutput;
}
public final class shark/ObjectGrowthDetector$Companion {
@@ -930,27 +930,20 @@
public abstract fun createFor (Lshark/HeapGraph;)Lshark/ReferenceReader;
}
-public final class shark/RepeatingHeapGraphObjectGrowthDetector {
- public fun <init> (Lshark/ObjectGrowthDetector;Lshark/RepeatingHeapGraphObjectGrowthDetector$CompletionListener;)V
- public synthetic fun <init> (Lshark/ObjectGrowthDetector;Lshark/RepeatingHeapGraphObjectGrowthDetector$CompletionListener;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
- public final fun findRepeatedlyGrowingObjects (ILkotlin/sequences/Sequence;)Lshark/HeapDiff;
- public static synthetic fun findRepeatedlyGrowingObjects$default (Lshark/RepeatingHeapGraphObjectGrowthDetector;ILkotlin/sequences/Sequence;ILjava/lang/Object;)Lshark/HeapDiff;
-}
-
-public abstract interface class shark/RepeatingHeapGraphObjectGrowthDetector$CompletionListener {
- public abstract fun onObjectGrowthDetectionComplete (Lshark/HeapDiff;)V
-}
-
-public final class shark/RepeatingScenarioObjectGrowthDetector {
+public abstract interface class shark/RepeatingScenarioObjectGrowthDetector {
public static final field Companion Lshark/RepeatingScenarioObjectGrowthDetector$Companion;
public static final field DEFAULT_MAX_HEAP_DUMPS I
public static final field DEFAULT_SCENARIO_LOOPS_PER_DUMP I
- public fun <init> (Lshark/HeapGraphProvider;Lshark/RepeatingHeapGraphObjectGrowthDetector;)V
- public final fun findRepeatedlyGrowingObjects (IILkotlin/jvm/functions/Function0;)Lshark/HeapDiff;
- public static synthetic fun findRepeatedlyGrowingObjects$default (Lshark/RepeatingScenarioObjectGrowthDetector;IILkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lshark/HeapDiff;
+ public abstract fun findRepeatedlyGrowingObjects (IILkotlin/jvm/functions/Function0;)Lshark/HeapDiff;
}
public final class shark/RepeatingScenarioObjectGrowthDetector$Companion {
+ public static final field DEFAULT_MAX_HEAP_DUMPS I
+ public static final field DEFAULT_SCENARIO_LOOPS_PER_DUMP I
+}
+
+public final class shark/RepeatingScenarioObjectGrowthDetector$DefaultImpls {
+ public static synthetic fun findRepeatedlyGrowingObjects$default (Lshark/RepeatingScenarioObjectGrowthDetector;IILkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lshark/HeapDiff;
}
public final class shark/Retained {
diff --git a/shark/shark/src/main/java/shark/ObjectGrowthDetector.kt b/shark/shark/src/main/java/shark/ObjectGrowthDetector.kt
index 5105f66..532d694 100644
--- a/shark/shark/src/main/java/shark/ObjectGrowthDetector.kt
+++ b/shark/shark/src/main/java/shark/ObjectGrowthDetector.kt
@@ -26,7 +26,7 @@
) {
fun findGrowingObjects(
- heapGraph: CloseableHeapGraph,
+ heapGraph: HeapGraph,
previousTraversal: HeapTraversalInput = InitialState(),
): HeapTraversalOutput {
check(previousTraversal !is HeapDiff || previousTraversal.isGrowing) {
@@ -38,20 +38,10 @@
// visit more than that but this limits the number of early array growths.
val estimatedVisitedObjects = (heapGraph.instanceCount / 2).coerceAtLeast(4)
val state = TraversalState(estimatedVisitedObjects = estimatedVisitedObjects)
- return heapGraph.use {
- state.traverseHeapDiffingShortestPaths(
- heapGraph,
- previousTraversal
- )
- }.also { output ->
- if (output is HeapDiff) {
- val scenarioCount = output.traversalCount * output.scenarioLoopsPerGraph
- SharkLog.d {
- "After $scenarioCount scenario iterations and ${output.traversalCount} heap dumps, " +
- "${output.growingObjects.size} growing nodes:\n" + output.growingObjects
- }
- }
- }
+ return state.traverseHeapDiffingShortestPaths(
+ heapGraph,
+ previousTraversal
+ )
}
// data class to be a properly implemented key.
@@ -94,7 +84,7 @@
@Suppress("ComplexMethod")
private fun TraversalState.traverseHeapDiffingShortestPaths(
- graph: CloseableHeapGraph,
+ graph: HeapGraph,
previousTraversal: HeapTraversalInput
): HeapTraversalOutput {
val previousTree = when (previousTraversal) {
@@ -402,7 +392,7 @@
private fun TraversalState.enqueueRoots(
previousTree: ShortestPathObjectNode?,
- heapGraph: CloseableHeapGraph
+ heapGraph: HeapGraph
) {
val previousTreeRootMap = previousTree?.let { tree ->
tree.children.associateBy { it.name }
diff --git a/shark/shark/src/main/java/shark/RepeatingHeapGraphObjectGrowthDetector.kt b/shark/shark/src/main/java/shark/RepeatingHeapGraphObjectGrowthDetector.kt
deleted file mode 100644
index a2d5ad5..0000000
--- a/shark/shark/src/main/java/shark/RepeatingHeapGraphObjectGrowthDetector.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-package shark
-
-import shark.RepeatingHeapGraphObjectGrowthDetector.CompletionListener
-
-/**
- * @see findRepeatedlyGrowingObjects
- */
-class RepeatingHeapGraphObjectGrowthDetector(
- private val objectGrowthDetector: ObjectGrowthDetector,
- private val completionListener: CompletionListener = CompletionListener { }
-) {
-
- fun interface CompletionListener {
- fun onObjectGrowthDetectionComplete(result: HeapDiff)
- }
-
- /**
- * Detects object growth by iterating through [heapGraphSequence] repeatedly until no object
- * growth is detected or the sequence ends. Returns the [HeapDiff] for the last
- * iteration. You can check [HeapDiff.isGrowing] and
- * [HeapDiff.growingObjects] to report object growth.
- */
- fun findRepeatedlyGrowingObjects(
- scenarioLoopsPerGraph: Int = InitialState.DEFAULT_SCENARIO_LOOPS_PER_GRAPH,
- heapGraphSequence: Sequence<CloseableHeapGraph>,
- ): HeapDiff {
- var lastTraversal: HeapTraversalInput = InitialState(scenarioLoopsPerGraph)
- for (heapGraph in heapGraphSequence) {
- val output = objectGrowthDetector.findGrowingObjects(
- heapGraph = heapGraph,
- previousTraversal = lastTraversal,
- )
- if (output is HeapDiff && !output.isGrowing) {
- completionListener.onObjectGrowthDetectionComplete(output)
- return output
- }
- lastTraversal = output
- }
- check(lastTraversal is HeapDiff) {
- "Final output should be a HeapGrowth, traversalCount ${lastTraversal.traversalCount - 1} " +
- "should be >= 2. Output: $lastTraversal"
- }
- completionListener.onObjectGrowthDetectionComplete(lastTraversal)
- return lastTraversal
- }
-}
diff --git a/shark/shark/src/main/java/shark/RepeatingScenarioObjectGrowthDetector.kt b/shark/shark/src/main/java/shark/RepeatingScenarioObjectGrowthDetector.kt
index f6f01bf..d5ee723 100644
--- a/shark/shark/src/main/java/shark/RepeatingScenarioObjectGrowthDetector.kt
+++ b/shark/shark/src/main/java/shark/RepeatingScenarioObjectGrowthDetector.kt
@@ -3,14 +3,11 @@
/**
* @see [findRepeatedlyGrowingObjects]
*/
-class RepeatingScenarioObjectGrowthDetector(
- private val heapGraphProvider: HeapGraphProvider,
- private val repeatingHeapGraphDetector: RepeatingHeapGraphObjectGrowthDetector,
-) {
+interface RepeatingScenarioObjectGrowthDetector {
/**
* Detects object growth by iterating through [roundTripScenario] repeatedly and dumping the heap
- * every `scenarioLoopsPerDump` until no object growth is detected or we reach `maxHeapDumps`.
+ * every [scenarioLoopsPerDump] until no object growth is detected or we reach [maxHeapDumps].
* Returns the [HeapDiff] for the last iteration. You can check
* [HeapDiff.isGrowing] and [HeapDiff.growingObjects] to report object growth.
*
@@ -23,27 +20,7 @@
maxHeapDumps: Int = DEFAULT_MAX_HEAP_DUMPS,
scenarioLoopsPerDump: Int = DEFAULT_SCENARIO_LOOPS_PER_DUMP,
roundTripScenario: () -> Unit
- ): HeapDiff {
- val heapGraphSequence = dumpHeapOnNext(maxHeapDumps, scenarioLoopsPerDump, roundTripScenario)
- return repeatingHeapGraphDetector.findRepeatedlyGrowingObjects(
- scenarioLoopsPerGraph = scenarioLoopsPerDump,
- heapGraphSequence = heapGraphSequence,
- )
- }
-
- private fun dumpHeapOnNext(
- maxHeapDumps: Int,
- scenarioLoopsPerDump: Int,
- repeatedScenario: () -> Unit,
- ): Sequence<CloseableHeapGraph> {
- val heapDumps = (1..maxHeapDumps).asSequence().map {
- repeat(scenarioLoopsPerDump) {
- repeatedScenario()
- }
- heapGraphProvider.openHeapGraph()
- }
- return heapDumps
- }
+ ): HeapDiff
companion object {
const val DEFAULT_MAX_HEAP_DUMPS = 5
diff --git a/shark/shark/src/test/java/shark/ObjectGrowthDetectorTest.kt b/shark/shark/src/test/java/shark/ObjectGrowthDetectorTest.kt
index d7d6e41..c4df0ee 100644
--- a/shark/shark/src/test/java/shark/ObjectGrowthDetectorTest.kt
+++ b/shark/shark/src/test/java/shark/ObjectGrowthDetectorTest.kt
@@ -99,7 +99,7 @@
}
)
- val heapTraversal = detector.findRepeatedlyGrowingObjects(dumps)
+ val heapTraversal = detector.findRepeatedlyGrowingObjects(dumps)
val growingObject = heapTraversal.growingObjects.single()
assertThat(growingObject.retainedIncrease.objectCount).isEqualTo(1)
@@ -122,7 +122,7 @@
}
)
- val heapTraversal = detector.findRepeatedlyGrowingObjects(dumps)
+ val heapTraversal = detector.findRepeatedlyGrowingObjects(dumps)
val growingObject = heapTraversal.growingObjects.single()
assertThat(growingObject.retainedIncrease.objectCount).isEqualTo(1)
@@ -130,7 +130,6 @@
assertThat(growingObject.retainedIncrease.heapSize).isEqualTo(expectedRetainedSizeIncrease)
}
-
@Test
fun `detect growth of custom linked list`() {
val detector = ObjectGrowthDetector.forJvmHeap().listRepeatingHeapGraph()
@@ -433,11 +432,15 @@
val detector = ObjectGrowthDetector.forJvmHeap().listRepeatingHeapGraph()
val dumps = listOf(
dump {
- val pairClass = clazz("Pair", fields = listOf(
+ val pairClass = clazz(
+ "Pair", fields = listOf(
"first" to ValueHolder.ReferenceHolder::class,
"second" to ValueHolder.ReferenceHolder::class,
- ))
- val growingClass = clazz("GrowingClass", fields = listOf("growingField" to ValueHolder.ReferenceHolder::class))
+ )
+ )
+ val growingClass = clazz(
+ "GrowingClass", fields = listOf("growingField" to ValueHolder.ReferenceHolder::class)
+ )
val pair = instance(pairClass, listOf(instance(objectClassId), instance(objectClassId)))
clazz(
"ClassWithStatics",
@@ -449,11 +452,15 @@
)
},
dump {
- val pairClass = clazz("Pair", fields = listOf(
+ val pairClass = clazz(
+ "Pair", fields = listOf(
"first" to ValueHolder.ReferenceHolder::class,
"second" to ValueHolder.ReferenceHolder::class,
- ))
- val growingClass = clazz("GrowingClass", fields = listOf("growingField" to ValueHolder.ReferenceHolder::class))
+ )
+ )
+ val growingClass = clazz(
+ "GrowingClass", fields = listOf("growingField" to ValueHolder.ReferenceHolder::class)
+ )
val pair1 = instance(pairClass, listOf(instance(objectClassId), instance(objectClassId)))
val pair2 = instance(pairClass, listOf(instance(objectClassId), instance(objectClassId)))
clazz(
@@ -475,22 +482,23 @@
}
class ListRepeatingHeapGraphObjectGrowthDetector(
- objectGrowthDetector: ObjectGrowthDetector
+ private val objectGrowthDetector: ObjectGrowthDetector
) {
- private val delegate = RepeatingHeapGraphObjectGrowthDetector(objectGrowthDetector)
fun findRepeatedlyGrowingObjects(
- heapGraphs: List<CloseableHeapGraph>,
+ heapGraphs: List<HeapGraph>,
scenarioLoopsPerGraph: Int = InitialState.DEFAULT_SCENARIO_LOOPS_PER_GRAPH,
): HeapDiff {
- return delegate.findRepeatedlyGrowingObjects(
- scenarioLoopsPerGraph = scenarioLoopsPerGraph,
- heapGraphSequence = heapGraphs.asSequence()
- ).apply {
- check(traversalCount == heapGraphs.size) {
- "Expected traversalCount $traversalCount to be equal to heapGraphs size ${heapGraphs.size} for $this"
+ var previousTraversal: HeapTraversalInput = InitialState(scenarioLoopsPerGraph)
+ for (heapGraph in heapGraphs) {
+ previousTraversal = objectGrowthDetector.findGrowingObjects(heapGraph, previousTraversal)
+ if (previousTraversal is HeapDiff && !previousTraversal.isGrowing) {
+ check(previousTraversal.traversalCount == heapGraphs.size) {
+ "Expected to go through all ${heapGraphs.size} heap dumps, stopped at ${previousTraversal.traversalCount}"
+ }
}
}
+ return previousTraversal as HeapDiff
}
}