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
     }
   }