diff --git a/leakcanary/leakcanary-android-core/api/leakcanary-android-core.api b/leakcanary/leakcanary-android-core/api/leakcanary-android-core.api index ab4954bba0..d1a1c19c81 100644 --- a/leakcanary/leakcanary-android-core/api/leakcanary-android-core.api +++ b/leakcanary/leakcanary-android-core/api/leakcanary-android-core.api @@ -64,10 +64,6 @@ public final class leakcanary/EventListener$Event$HeapDumpFailed : leakcanary/Ev public final fun getWillRetryLater ()Z } -public abstract interface class leakcanary/HeapDumper { - public abstract fun dumpHeap (Ljava/io/File;)V -} - public final class leakcanary/LazyForwardingEventListener : leakcanary/EventListener { public fun (Lkotlin/jvm/functions/Function0;)V public fun onEvent (Lleakcanary/EventListener$Event;)V diff --git a/leakcanary/leakcanary-android-core/build.gradle b/leakcanary/leakcanary-android-core/build.gradle index 568ce49c5b..0c9c92ec98 100644 --- a/leakcanary/leakcanary-android-core/build.gradle +++ b/leakcanary/leakcanary-android-core/build.gradle @@ -8,6 +8,7 @@ dependencies { api projects.shark.sharkAndroid api projects.objectWatcher.objectWatcherAndroidCore api projects.objectWatcher.objectWatcherAndroidAndroidx + api projects.leakcanary.leakcanaryCore implementation libs.kotlin.stdlib // Optional dependency diff --git a/leakcanary/leakcanary-android-instrumentation/api/leakcanary-android-instrumentation.api b/leakcanary/leakcanary-android-instrumentation/api/leakcanary-android-instrumentation.api index 6984d4eeda..6c55848d39 100644 --- a/leakcanary/leakcanary-android-instrumentation/api/leakcanary-android-instrumentation.api +++ b/leakcanary/leakcanary-android-instrumentation/api/leakcanary-android-instrumentation.api @@ -19,6 +19,12 @@ public final class leakcanary/AndroidDetectLeaksInterceptor : leakcanary/DetectL public fun waitUntilReadyForHeapAnalysis ()Lleakcanary/HeapAnalysisDecision; } +public final class leakcanary/AndroidLiveObjectGrowthDetector { + public static final field INSTANCE Lleakcanary/AndroidLiveObjectGrowthDetector; + public final fun create (IILleakcanary/HeapDumpFileProvider;Lleakcanary/HeapDumper;Lshark/RepeatedObjectGrowthDetector;)Lshark/LiveObjectGrowthDetector; + public static synthetic fun create$default (Lleakcanary/AndroidLiveObjectGrowthDetector;IILleakcanary/HeapDumpFileProvider;Lleakcanary/HeapDumper;Lshark/RepeatedObjectGrowthDetector;ILjava/lang/Object;)Lshark/LiveObjectGrowthDetector; +} + public final class leakcanary/DetectLeaksAfterTestSuccess : org/junit/rules/TestRule { public static final field Companion Lleakcanary/DetectLeaksAfterTestSuccess$Companion; public fun ()V diff --git a/leakcanary/leakcanary-android-instrumentation/build.gradle b/leakcanary/leakcanary-android-instrumentation/build.gradle index 4d991c88c3..16c7a58031 100644 --- a/leakcanary/leakcanary-android-instrumentation/build.gradle +++ b/leakcanary/leakcanary-android-instrumentation/build.gradle @@ -6,6 +6,7 @@ plugins { dependencies { api projects.leakcanary.leakcanaryAndroidCore + api projects.shark.sharkAndroid implementation libs.androidX.test.runner implementation libs.kotlin.stdlib diff --git a/leakcanary/leakcanary-android-instrumentation/src/main/java/leakcanary/AndroidDetectLeaksAssert.kt b/leakcanary/leakcanary-android-instrumentation/src/main/java/leakcanary/AndroidDetectLeaksAssert.kt index 52d5aa2399..7ca924906c 100644 --- a/leakcanary/leakcanary-android-instrumentation/src/main/java/leakcanary/AndroidDetectLeaksAssert.kt +++ b/leakcanary/leakcanary-android-instrumentation/src/main/java/leakcanary/AndroidDetectLeaksAssert.kt @@ -1,9 +1,10 @@ package leakcanary import android.os.SystemClock +import androidx.test.platform.app.InstrumentationRegistry +import java.io.File import leakcanary.HeapAnalysisDecision.NoHeapAnalysis import leakcanary.internal.InstrumentationHeapAnalyzer -import leakcanary.internal.InstrumentationHeapDumpFileProvider import leakcanary.internal.RetryingHeapAnalyzer import leakcanary.internal.friendly.checkNotMainThread import leakcanary.internal.friendly.measureDurationMillis @@ -57,7 +58,15 @@ class AndroidDetectLeaksAssert( } } - val heapDumpFile = InstrumentationHeapDumpFileProvider().newHeapDumpFile() + val heapDumpFileProvider = leakcanary.HeapDumpFileProvider.dateFormatted( + directory = File( + InstrumentationRegistry.getInstrumentation().targetContext.filesDir, + "instrumentation_tests" + ), + prefix = "instrumentation_tests" + ) + + val heapDumpFile = heapDumpFileProvider.newHeapDumpFile() val config = LeakCanary.config diff --git a/leakcanary/leakcanary-android-instrumentation/src/main/java/leakcanary/AndroidLiveObjectGrowthDetector.kt b/leakcanary/leakcanary-android-instrumentation/src/main/java/leakcanary/AndroidLiveObjectGrowthDetector.kt new file mode 100644 index 0000000000..9f1ecca104 --- /dev/null +++ b/leakcanary/leakcanary-android-instrumentation/src/main/java/leakcanary/AndroidLiveObjectGrowthDetector.kt @@ -0,0 +1,35 @@ +package leakcanary + +import androidx.test.platform.app.InstrumentationRegistry +import java.io.File +import shark.HeapDumpingObjectGrowthDetector +import shark.LiveObjectGrowthDetector +import shark.RepeatedObjectGrowthDetector + +// TODO This name is wrong, and create method is wrong. +object AndroidLiveObjectGrowthDetector { + + fun create( + maxHeapDumps: Int = 5, + scenarioLoopsPerDump: Int = 5, + heapDumpFileProvider: HeapDumpFileProvider = HeapDumpFileProvider.dateFormatted( + directory = File( + InstrumentationRegistry.getInstrumentation().targetContext.filesDir, + "heap-growth-hprof" + ), + prefix = "heap-growth-" + ), + heapDumper: HeapDumper = AndroidDebugHeapDumper, + objectRepeatedGrowthDetector: RepeatedObjectGrowthDetector + ): LiveObjectGrowthDetector { + val heapGraphProvider = + DumpingDeletingOnCloseHeapGraphProvider(heapDumpFileProvider, heapDumper) + + return HeapDumpingObjectGrowthDetector( + maxHeapDumps = maxHeapDumps, + heapGraphProvider = heapGraphProvider, + scenarioLoopsPerDump = scenarioLoopsPerDump, + detector = objectRepeatedGrowthDetector + ) + } +} diff --git a/leakcanary/leakcanary-android-instrumentation/src/main/java/leakcanary/internal/InstrumentationHeapDumpFileProvider.kt b/leakcanary/leakcanary-android-instrumentation/src/main/java/leakcanary/internal/InstrumentationHeapDumpFileProvider.kt deleted file mode 100644 index b1a1f0527c..0000000000 --- a/leakcanary/leakcanary-android-instrumentation/src/main/java/leakcanary/internal/InstrumentationHeapDumpFileProvider.kt +++ /dev/null @@ -1,26 +0,0 @@ -package leakcanary.internal - -import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation -import java.io.File -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - -/** - * Provides unique file names for each heap dump in instrumentation tests. - */ -internal class InstrumentationHeapDumpFileProvider( - private val heapDumpDirectory: File = getInstrumentation().targetContext.filesDir -) { - - /** - * Returns a file for where the heap should be dumped. - */ - fun newHeapDumpFile(): File { - // File name is unique as analysis may run several times per test - val fileName = - SimpleDateFormat("'instrumentation_tests_'yyyy-MM-dd_HH-mm-ss_SSS'.hprof'", Locale.US) - .format(Date()) - return File(heapDumpDirectory, fileName) - } -} diff --git a/leakcanary/leakcanary-core/api/leakcanary-core.api b/leakcanary/leakcanary-core/api/leakcanary-core.api new file mode 100644 index 0000000000..4bb100e0ff --- /dev/null +++ b/leakcanary/leakcanary-core/api/leakcanary-core.api @@ -0,0 +1,30 @@ +public final class leakcanary/DateFormatHeapDumpFileProvider : leakcanary/HeapDumpFileProvider { + public static final field Companion Lleakcanary/DateFormatHeapDumpFileProvider$Companion; + public static final field TIME_PATTERN Ljava/lang/String; + public fun (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun newHeapDumpFile ()Ljava/io/File; +} + +public final class leakcanary/DateFormatHeapDumpFileProvider$Companion { +} + +public final class leakcanary/DumpingDeletingOnCloseHeapGraphProvider : shark/HeapGraphProvider { + public fun (Lleakcanary/HeapDumpFileProvider;Lleakcanary/HeapDumper;)V + public fun openHeapGraph ()Lshark/CloseableHeapGraph; +} + +public abstract interface class leakcanary/HeapDumpFileProvider { + public static final field Companion Lleakcanary/HeapDumpFileProvider$Companion; + public abstract fun newHeapDumpFile ()Ljava/io/File; +} + +public final class leakcanary/HeapDumpFileProvider$Companion { + public final fun dateFormatted (Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Lleakcanary/HeapDumpFileProvider; + public static synthetic fun dateFormatted$default (Lleakcanary/HeapDumpFileProvider$Companion;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lleakcanary/HeapDumpFileProvider; +} + +public abstract interface class leakcanary/HeapDumper { + public abstract fun dumpHeap (Ljava/io/File;)V +} + diff --git a/leakcanary/leakcanary-core/build.gradle b/leakcanary/leakcanary-core/build.gradle new file mode 100644 index 0000000000..b0c3bba09b --- /dev/null +++ b/leakcanary/leakcanary-core/build.gradle @@ -0,0 +1,12 @@ +plugins { + id("org.jetbrains.kotlin.jvm") + id("com.vanniktech.maven.publish") +} + +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 + +dependencies { + api projects.shark.shark + +} diff --git a/leakcanary/leakcanary-core/gradle.properties b/leakcanary/leakcanary-core/gradle.properties new file mode 100644 index 0000000000..63d67e8a33 --- /dev/null +++ b/leakcanary/leakcanary-core/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=leakcanary-core +POM_NAME=LeakCanary Core +POM_PACKAGING=jar diff --git a/leakcanary/leakcanary-core/src/main/java/leakcanary/DateFormatHeapDumpFileProvider.kt b/leakcanary/leakcanary-core/src/main/java/leakcanary/DateFormatHeapDumpFileProvider.kt new file mode 100644 index 0000000000..f8cfc4dd1f --- /dev/null +++ b/leakcanary/leakcanary-core/src/main/java/leakcanary/DateFormatHeapDumpFileProvider.kt @@ -0,0 +1,39 @@ +package leakcanary + +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class DateFormatHeapDumpFileProvider( + private val heapDumpDirectoryProvider: () -> File, + private val dateProvider: () -> Date = { Date() }, + prefix: String = "", + suffix: String = "" +) : HeapDumpFileProvider { + + private val dateFormatPattern = + "${escape(prefix)}$TIME_PATTERN${escape("$suffix.hprof")}" + + private val timeFormatThreadLocal = object : ThreadLocal() { + // Lint is drunk and thinks we use the pattern 'u' + @Suppress("NewApi") + override fun initialValue() = + SimpleDateFormat(dateFormatPattern, Locale.US) + } + + override fun newHeapDumpFile(): File { + val heapDumpDirectory = heapDumpDirectoryProvider() + val date = dateProvider() + val fileName = timeFormatThreadLocal.get()!!.format(date) + return File(heapDumpDirectory, fileName) + } + + private fun escape(string: String) = if (string != "") { + "'$string'" + } else "" + + companion object { + const val TIME_PATTERN = "yyyy-MM-dd_HH-mm-ss_SSS" + } +} diff --git a/leakcanary/leakcanary-core/src/main/java/leakcanary/DumpingDeletingOnCloseHeapGraphProvider.kt b/leakcanary/leakcanary-core/src/main/java/leakcanary/DumpingDeletingOnCloseHeapGraphProvider.kt new file mode 100644 index 0000000000..45552bdee3 --- /dev/null +++ b/leakcanary/leakcanary-core/src/main/java/leakcanary/DumpingDeletingOnCloseHeapGraphProvider.kt @@ -0,0 +1,25 @@ +package leakcanary + +import shark.CloseableHeapGraph +import shark.HeapGraphProvider +import shark.HprofHeapGraph.Companion.openHeapGraph + +class DumpingDeletingOnCloseHeapGraphProvider( + private val heapDumpFileProvider: HeapDumpFileProvider, + private val heapDumper: HeapDumper +) : HeapGraphProvider { + override fun openHeapGraph(): CloseableHeapGraph { + val heapDumpFile = heapDumpFileProvider.newHeapDumpFile() + heapDumper.dumpHeap(heapDumpFile) + check(heapDumpFile.exists()) { + "Expected file to exist after heap dump: ${heapDumpFile.absolutePath}" + } + val realGraph = heapDumpFile.openHeapGraph() + return object : CloseableHeapGraph by realGraph { + override fun close() { + realGraph.close() + heapDumpFile.delete() + } + } + } +} diff --git a/leakcanary/leakcanary-core/src/main/java/leakcanary/HeapDumpFileProvider.kt b/leakcanary/leakcanary-core/src/main/java/leakcanary/HeapDumpFileProvider.kt new file mode 100644 index 0000000000..6d5ecb7606 --- /dev/null +++ b/leakcanary/leakcanary-core/src/main/java/leakcanary/HeapDumpFileProvider.kt @@ -0,0 +1,35 @@ +package leakcanary + +import java.io.File +import java.util.Date + +fun interface HeapDumpFileProvider { + + /** + * Returns a [File] that can be passed to a [HeapDumper] to dump the heap. + */ + fun newHeapDumpFile(): File + + companion object { + fun dateFormatted( + directory: File, + prefix: String = "", + suffix: String = "", + dateProvider: () -> Date = { Date() }, + ): HeapDumpFileProvider { + return DateFormatHeapDumpFileProvider( + heapDumpDirectoryProvider = { + directory.apply { + mkdirs() + check(exists()) { + "Expected heap dump folder to exist: $absolutePath" + } + } + }, + dateProvider = dateProvider, + prefix = prefix, + suffix = suffix + ) + } + } +} diff --git a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/HeapDumper.kt b/leakcanary/leakcanary-core/src/main/java/leakcanary/HeapDumper.kt similarity index 100% rename from leakcanary/leakcanary-android-core/src/main/java/leakcanary/HeapDumper.kt rename to leakcanary/leakcanary-core/src/main/java/leakcanary/HeapDumper.kt diff --git a/leakcanary/leakcanary-jvm-test/api/leakcanary-jvm-test.api b/leakcanary/leakcanary-jvm-test/api/leakcanary-jvm-test.api new file mode 100644 index 0000000000..2b2f67a036 --- /dev/null +++ b/leakcanary/leakcanary-jvm-test/api/leakcanary-jvm-test.api @@ -0,0 +1,16 @@ +public final class leakcanary/HotSpotHeapDumper : leakcanary/HeapDumper { + public static final field INSTANCE Lleakcanary/HotSpotHeapDumper; + public fun dumpHeap (Ljava/io/File;)V +} + +public final class leakcanary/JvmLiveObjectGrowthDetector { + public static final field INSTANCE Lleakcanary/JvmLiveObjectGrowthDetector; + public final fun create (IILleakcanary/HeapDumpFileProvider;Lleakcanary/HeapDumper;Lshark/RepeatedObjectGrowthDetector;)Lshark/LiveObjectGrowthDetector; + public static synthetic fun create$default (Lleakcanary/JvmLiveObjectGrowthDetector;IILleakcanary/HeapDumpFileProvider;Lleakcanary/HeapDumper;Lshark/RepeatedObjectGrowthDetector;ILjava/lang/Object;)Lshark/LiveObjectGrowthDetector; +} + +public final class leakcanary/TempHeapDumpFileProvider : leakcanary/HeapDumpFileProvider { + public static final field INSTANCE Lleakcanary/TempHeapDumpFileProvider; + public fun newHeapDumpFile ()Ljava/io/File; +} + diff --git a/shark/shark-heap-growth/build.gradle b/leakcanary/leakcanary-jvm-test/build.gradle similarity index 69% rename from shark/shark-heap-growth/build.gradle rename to leakcanary/leakcanary-jvm-test/build.gradle index 03dee06693..7fcc007833 100644 --- a/shark/shark-heap-growth/build.gradle +++ b/leakcanary/leakcanary-jvm-test/build.gradle @@ -7,11 +7,9 @@ sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 dependencies { + api projects.leakcanary.leakcanaryCore api projects.shark.shark - api projects.shark.sharkGraph testImplementation libs.assertjCore testImplementation libs.junit - testImplementation projects.shark.sharkTest - testImplementation projects.shark.sharkHprofTest } diff --git a/leakcanary/leakcanary-jvm-test/gradle.properties b/leakcanary/leakcanary-jvm-test/gradle.properties new file mode 100644 index 0000000000..36485e9717 --- /dev/null +++ b/leakcanary/leakcanary-jvm-test/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=leakcanary-jvm-test +POM_NAME=LeakCanary Jvm Test +POM_PACKAGING=jar diff --git a/leakcanary/leakcanary-jvm-test/src/main/java/leakcanary/HotSpotHeapDumper.kt b/leakcanary/leakcanary-jvm-test/src/main/java/leakcanary/HotSpotHeapDumper.kt new file mode 100644 index 0000000000..926b800d25 --- /dev/null +++ b/leakcanary/leakcanary-jvm-test/src/main/java/leakcanary/HotSpotHeapDumper.kt @@ -0,0 +1,21 @@ +package leakcanary + +import com.sun.management.HotSpotDiagnosticMXBean +import java.io.File +import java.lang.management.ManagementFactory + +object HotSpotHeapDumper : HeapDumper { + private val hotspotMBean: HotSpotDiagnosticMXBean by lazy { + val mBeanServer = ManagementFactory.getPlatformMBeanServer() + ManagementFactory.newPlatformMXBeanProxy( + mBeanServer, + "com.sun.management:type=HotSpotDiagnostic", + HotSpotDiagnosticMXBean::class.java + ) + } + + override fun dumpHeap(heapDumpFile: File) { + val live = true + hotspotMBean.dumpHeap(heapDumpFile.absolutePath, live) + } +} diff --git a/leakcanary/leakcanary-jvm-test/src/main/java/leakcanary/JvmLiveObjectGrowthDetector.kt b/leakcanary/leakcanary-jvm-test/src/main/java/leakcanary/JvmLiveObjectGrowthDetector.kt new file mode 100644 index 0000000000..7dd6fee3b8 --- /dev/null +++ b/leakcanary/leakcanary-jvm-test/src/main/java/leakcanary/JvmLiveObjectGrowthDetector.kt @@ -0,0 +1,26 @@ +package leakcanary + +import shark.HeapDumpingObjectGrowthDetector +import shark.LiveObjectGrowthDetector +import shark.RepeatedObjectGrowthDetector + +// TODO This name is wrong, and create method is wrong. +object JvmLiveObjectGrowthDetector { + + fun create( + maxHeapDumps: Int = 5, + scenarioLoopsPerDump: Int = 5, + heapDumpFileProvider: HeapDumpFileProvider = TempHeapDumpFileProvider, + heapDumper: HeapDumper = HotSpotHeapDumper, + repeatedObjectGrowthDetector: RepeatedObjectGrowthDetector + ): LiveObjectGrowthDetector { + val heapGraphProvider = + DumpingDeletingOnCloseHeapGraphProvider(heapDumpFileProvider, heapDumper) + return HeapDumpingObjectGrowthDetector( + maxHeapDumps = maxHeapDumps, + heapGraphProvider = heapGraphProvider, + scenarioLoopsPerDump = scenarioLoopsPerDump, + detector = repeatedObjectGrowthDetector + ) + } +} diff --git a/leakcanary/leakcanary-jvm-test/src/main/java/leakcanary/TempHeapDumpFileProvider.kt b/leakcanary/leakcanary-jvm-test/src/main/java/leakcanary/TempHeapDumpFileProvider.kt new file mode 100644 index 0000000000..f4cb41f31d --- /dev/null +++ b/leakcanary/leakcanary-jvm-test/src/main/java/leakcanary/TempHeapDumpFileProvider.kt @@ -0,0 +1,13 @@ +package leakcanary + +import java.io.File + +object TempHeapDumpFileProvider : HeapDumpFileProvider { + override fun newHeapDumpFile(): File { + val heapDumpFile = File.createTempFile("heap-growth", ".hprof", null) + check(heapDumpFile.delete()) { + "Could not delete $heapDumpFile, needs to not exist for heap dump" + } + return heapDumpFile + } +} diff --git a/leakcanary/leakcanary-jvm-test/src/test/java/leakcanary/JvmHeapGrowthDetectorConfigTest.kt b/leakcanary/leakcanary-jvm-test/src/test/java/leakcanary/JvmHeapGrowthDetectorConfigTest.kt new file mode 100644 index 0000000000..d5aa90fbae --- /dev/null +++ b/leakcanary/leakcanary-jvm-test/src/test/java/leakcanary/JvmHeapGrowthDetectorConfigTest.kt @@ -0,0 +1,25 @@ +package leakcanary + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import shark.RepeatedObjectGrowthDetectorJvmFactory + +class JvmHeapGrowthDetectorConfigTest { + + class Leaky + + private val leakies = mutableListOf() + + @Test + fun `leaky increase leads to heap growth`() { + val detector = JvmLiveObjectGrowthDetector.create( + repeatedObjectGrowthDetector = RepeatedObjectGrowthDetectorJvmFactory.create() + ) + + val growingNodes = detector.findRepeatedlyGrowingObjects { + leakies += Leaky() + } + + assertThat(growingNodes).hasSize(1) + } +} diff --git a/settings.gradle b/settings.gradle index ea066d0972..e8303b8dd5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,7 +8,9 @@ include ':leakcanary:leakcanary-android-utils' include ':leakcanary:leakcanary-app' include ':leakcanary:leakcanary-app-aidl' include ':leakcanary:leakcanary-app-service' +include ':leakcanary:leakcanary-core' include ':leakcanary:leakcanary-deobfuscation-gradle-plugin' +include ':leakcanary:leakcanary-jvm-test' include ':object-watcher:object-watcher' include ':object-watcher:object-watcher-android' include ':object-watcher:object-watcher-android-androidx' @@ -22,7 +24,6 @@ include ':shark:shark' include ':shark:shark-android' include ':shark:shark-cli' include ':shark:shark-graph' -include ':shark:shark-heap-growth' include ':shark:shark-hprof' include ':shark:shark-hprof-test' include ':shark:shark-log' diff --git a/shark/shark-android/api/shark-android.api b/shark/shark-android/api/shark-android.api index a64e4b2de7..62bc2ebe38 100644 --- a/shark/shark-android/api/shark-android.api +++ b/shark/shark-android/api/shark-android.api @@ -15,6 +15,24 @@ public final class shark/AndroidExtensionsKt { public static final fun getIdentityHashCode (Lshark/HeapObject$HeapInstance;)Ljava/lang/Integer; } +public abstract class shark/AndroidHeapGrowthIgnoredReferences : java/lang/Enum { + public static final field ANDROID_DEFAULTS Lshark/AndroidHeapGrowthIgnoredReferences; + public static final field COMPOSE_TEST_CONTEXT_STATES Lshark/AndroidHeapGrowthIgnoredReferences; + public static final field Companion Lshark/AndroidHeapGrowthIgnoredReferences$Companion; + public static final field HEAP_TRAVERSAL Lshark/AndroidHeapGrowthIgnoredReferences; + public static final field RESOURCES_THEME_REFS Lshark/AndroidHeapGrowthIgnoredReferences; + public static final field STRICT_MODE_VIOLATION_TIME Lshark/AndroidHeapGrowthIgnoredReferences; + public static final field VIEW_ROOT_IMPL_W_VIEW_ANCESTOR Lshark/AndroidHeapGrowthIgnoredReferences; + public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public static fun valueOf (Ljava/lang/String;)Lshark/AndroidHeapGrowthIgnoredReferences; + public static fun values ()[Lshark/AndroidHeapGrowthIgnoredReferences; +} + +public final class shark/AndroidHeapGrowthIgnoredReferences$Companion { + public final fun buildKnownReferences (Ljava/util/Set;)Ljava/util/List; + public final fun getDefaults ()Ljava/util/List; +} + public final class shark/AndroidMetadataExtractor : shark/MetadataExtractor { public static final field INSTANCE Lshark/AndroidMetadataExtractor; public fun extractMetadata (Lshark/HeapGraph;)Ljava/util/Map; @@ -215,3 +233,9 @@ public final class shark/AndroidServices { public final fun getAliveAndroidServiceObjectIds (Lshark/HeapGraph;)Ljava/util/List; } +public final class shark/RepeatedObjectGrowthDetectorAndroidFactory { + public static final field INSTANCE Lshark/RepeatedObjectGrowthDetectorAndroidFactory; + public final fun create (Ljava/util/List;)Lshark/RepeatedObjectGrowthDetector; + public static synthetic fun create$default (Lshark/RepeatedObjectGrowthDetectorAndroidFactory;Ljava/util/List;ILjava/lang/Object;)Lshark/RepeatedObjectGrowthDetector; +} + diff --git a/shark/shark-android/build.gradle b/shark/shark-android/build.gradle index 5bc2c0a8d3..61103cb413 100644 --- a/shark/shark-android/build.gradle +++ b/shark/shark-android/build.gradle @@ -20,3 +20,9 @@ dependencies { testImplementation projects.shark.sharkTest testImplementation projects.shark.sharkHprofTest } + +test { + minHeapSize = "128m" // initial heap size + maxHeapSize = "4g" // maximum heap size +// jvmArgs '-XX:MaxPermSize=256m' // mem argument for the test JVM +} diff --git a/shark/shark-android/src/main/java/shark/AndroidHeapGrowthIgnoredReferences.kt b/shark/shark-android/src/main/java/shark/AndroidHeapGrowthIgnoredReferences.kt new file mode 100644 index 0000000000..47ff6f46d1 --- /dev/null +++ b/shark/shark-android/src/main/java/shark/AndroidHeapGrowthIgnoredReferences.kt @@ -0,0 +1,74 @@ +package shark + +import java.util.EnumSet +import shark.ReferencePattern.InstanceFieldPattern +import shark.ReferencePattern.StaticFieldPattern + +// TODO Maybe change back to using ref matchers? +// Deprioritizing can still be useful. +enum class AndroidHeapGrowthIgnoredReferences { + + ANDROID_DEFAULTS { + override fun add(patterns: MutableList) { + patterns += AndroidReferenceMatchers.appDefaults.filterIsInstance() + .map { it.pattern } + } + }, + + HEAP_TRAVERSAL { + override fun add(patterns: MutableList) { + patterns += HeapTraversal.ignoredReferences.map { it.pattern } + } + }, + + STRICT_MODE_VIOLATION_TIME { + override fun add(patterns: MutableList) { + // https://cs.android.com/android/_/android/platform/frameworks/base/+/6985fb39f07294fb979b14ba0ebabfd2fea06d34 + patterns += StaticFieldPattern("android.os.StrictMode", "sLastVmViolationTime") + } + }, + + COMPOSE_TEST_CONTEXT_STATES { + override fun add(patterns: MutableList) { + // TestContext.states has unbounded growth + // https://issuetracker.google.com/issues/319693080 + patterns += InstanceFieldPattern("androidx.compose.ui.test.TestContext", "states") + } + }, + + RESOURCES_THEME_REFS { + override fun add(patterns: MutableList) { + // Every time a new theme is generated, a weak reference is added to that list. That list is cleared at growing thresholds, and + // only the cleared weak refs are removed. + // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/res/Resources.java;l=182;drc=c4f2605b0cbde5813bf4030d9209c62cd0839f81 + patterns += InstanceFieldPattern("android.content.res.Resources", "mThemeRefs") + } + }, + + VIEW_ROOT_IMPL_W_VIEW_ANCESTOR { + override fun add(patterns: MutableList) { + // Stub allowing the system server process to talk back to a view root impl. Cleared when the GC runs + // in the system server process and then the receiving app. + // https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/view/ViewRootImpl.java;l=10538;drc=803ac6538fd26e39942e51930a0a3c7d8a0e8e06 + patterns += InstanceFieldPattern("android.view.ViewRootImpl\$W", "mViewAncestor") + } + }, + + ; + + internal abstract fun add(patterns: MutableList) + + companion object { + + val defaults: List + get() = buildKnownReferences(EnumSet.allOf(AndroidHeapGrowthIgnoredReferences::class.java)) + + fun buildKnownReferences(referenceMatchers: Set): List { + val resultSet = mutableListOf() + referenceMatchers.forEach { + it.add(resultSet) + } + return resultSet.map { IgnoredReferenceMatcher(it) } + } + } +} diff --git a/shark/shark-android/src/main/java/shark/RepeatedObjectGrowthDetectorAndroidFactory.kt b/shark/shark-android/src/main/java/shark/RepeatedObjectGrowthDetectorAndroidFactory.kt new file mode 100644 index 0000000000..84d7bb1bf0 --- /dev/null +++ b/shark/shark-android/src/main/java/shark/RepeatedObjectGrowthDetectorAndroidFactory.kt @@ -0,0 +1,12 @@ +package shark + +object RepeatedObjectGrowthDetectorAndroidFactory { + fun create(referenceMatchers: List = AndroidHeapGrowthIgnoredReferences.defaults): RepeatedObjectGrowthDetector { + return HeapGraphSequenceObjectGrowthDetector( + HeapGraphObjectGrowthDetector( + gcRootProvider = MatchingGcRootProvider(referenceMatchers), + referenceReaderFactory = AndroidReferenceReaderFactory(referenceMatchers) + ) + ) + } +} diff --git a/shark/shark-cli/src/main/java/shark/HeapGrowthCommand.kt b/shark/shark-cli/src/main/java/shark/HeapGrowthCommand.kt new file mode 100644 index 0000000000..003ea9e4c3 --- /dev/null +++ b/shark/shark-cli/src/main/java/shark/HeapGrowthCommand.kt @@ -0,0 +1,81 @@ +package shark + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.CliktError +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.validate +import com.github.ajalt.clikt.parameters.types.int +import jline.console.ConsoleReader +import shark.DumpProcessCommand.Companion.dumpHeap +import shark.HprofHeapGraph.Companion.openHeapGraph +import shark.SharkCliCommand.Companion.sharkCliParams +import shark.SharkCliCommand.HeapDumpSource.HprofDirectorySource +import shark.SharkCliCommand.HeapDumpSource.HprofFileSource +import shark.SharkCliCommand.HeapDumpSource.ProcessSource + +class HeapGrowthCommand : CliktCommand( + name = "heap-growth", + help = "Detect heap growth" +) { + private val scenarioLoopsPerDump by option( + "--loops", "-l", + help = "The number of scenario iteration in between each heap dump." + ).int().default(1).validate { if (it <= 0) fail("$it is not greater than 0") } + + private val maxHeapDumps by option( + "--max", "-m", + help = "The max number of heap dumps to perform live before returning the results." + ).int().default(5).validate { if (it <= 1) fail("$it is not greater than 1") } + + override fun run() { + val params = context.sharkCliParams + + val detector = RepeatedObjectGrowthDetectorAndroidFactory.create() + + val results = when (val source = params.source) { + is HprofFileSource -> throw CliktError( + "$commandName requires passing in a directory containing more than one hprof files." + ) + + is HprofDirectorySource -> { + val hprofFiles = source.hprofFiles.sortedBy { it.name } + if (hprofFiles.size == 1) { + throw CliktError( + "$commandName requires passing in a directory containing more than one hprof " + + "files, could only find ${hprofFiles.first().name} in " + + source.directory.absolutePath + ) + } + echo( + "Detecting heap growth by going analyzing the following heap dumps in this " + + "order:\n${hprofFiles.joinToString("\n") { it.name }}" + ) + + val heapGraphs = hprofFiles.asSequence().map { it.openHeapGraph() } + + detector.findRepeatedlyGrowingObjects(heapGraphs, scenarioLoopsPerDump) + } + + is ProcessSource -> { + echo("Detecting heap growth live") + + val liveDetector = HeapDumpingObjectGrowthDetector( + maxHeapDumps = maxHeapDumps, + heapGraphProvider = { + dumpHeap(source.processName, source.deviceId).openHeapGraph() + }, + scenarioLoopsPerDump = scenarioLoopsPerDump, + detector = detector + ) + + val nTimes = if (scenarioLoopsPerDump > 1) "$scenarioLoopsPerDump times" else "once" + + liveDetector.findRepeatedlyGrowingObjects { + ConsoleReader().readLine("Go through scenario $nTimes then press ENTER to dump heap") + } + } + } + echo("Results:\n" + results.joinToString("\n")) + } +} diff --git a/shark/shark-cli/src/main/java/shark/Main.kt b/shark/shark-cli/src/main/java/shark/Main.kt index 88cf53c531..929252222d 100644 --- a/shark/shark-cli/src/main/java/shark/Main.kt +++ b/shark/shark-cli/src/main/java/shark/Main.kt @@ -9,5 +9,6 @@ fun main(args: Array) = Neo4JCommand(), DumpProcessCommand(), StripHprofCommand(), - DeobfuscateHprofCommand() + DeobfuscateHprofCommand(), + HeapGrowthCommand() ).main(args) diff --git a/shark/shark-cli/src/main/java/shark/SharkCliCommand.kt b/shark/shark-cli/src/main/java/shark/SharkCliCommand.kt index 527368514f..3518a2cea3 100644 --- a/shark/shark-cli/src/main/java/shark/SharkCliCommand.kt +++ b/shark/shark-cli/src/main/java/shark/SharkCliCommand.kt @@ -17,9 +17,11 @@ import shark.SharkCliCommand.HeapDumpSource.HprofFileSource import shark.SharkCliCommand.HeapDumpSource.ProcessSource import shark.SharkLog.Logger import java.io.File +import java.io.FileFilter import java.io.PrintWriter import java.io.StringWriter import java.util.Properties +import shark.SharkCliCommand.HeapDumpSource.HprofDirectorySource class SharkCliCommand : CliktCommand( name = "shark-cli", @@ -64,9 +66,11 @@ class SharkCliCommand : CliktCommand( help = "provide additional details as to what shark-cli is doing" ).flag("--no-verbose") - private val heapDumpFile by option("--hprof", "-h", help = "path to a .hprof file").file( + private val heapDumpFile by option( + "--hprof", "-h", help = "path to a .hprof file or a folder containing hprof files" + ).file( exists = true, - folderOkay = false, + folderOkay = true, readable = true ) @@ -81,6 +85,11 @@ class SharkCliCommand : CliktCommand( sealed class HeapDumpSource { class HprofFileSource(val file: File) : HeapDumpSource() + class HprofDirectorySource(val directory: File) : HeapDumpSource() { + val hprofFiles: List + get() = directory.listFiles(FileFilter { it.extension == "hprof" })?.toList() ?: emptyList() + } + class ProcessSource( val processName: String, val deviceId: String? @@ -99,10 +108,18 @@ class SharkCliCommand : CliktCommand( obfuscationMappingPath = obfuscationMappingPath ) } else if (heapDumpFile != null) { - context.sharkCliParams = CommandParams( - source = HprofFileSource(heapDumpFile!!), - obfuscationMappingPath = obfuscationMappingPath - ) + val file = heapDumpFile!! + if (file.isDirectory) { + context.sharkCliParams = CommandParams( + source = HprofDirectorySource(file), + obfuscationMappingPath = obfuscationMappingPath + ) + } else { + context.sharkCliParams = CommandParams( + source = HprofFileSource(file), + obfuscationMappingPath = obfuscationMappingPath + ) + } } else { throw UsageError("Must provide one of --process, --hprof") } @@ -154,6 +171,17 @@ class SharkCliCommand : CliktCommand( fun CliktCommand.retrieveHeapDumpFile(params: CommandParams): File { return when (val source = params.source) { is HprofFileSource -> source.file + is HprofDirectorySource -> { + val hprofFiles = source.hprofFiles + if (hprofFiles.size != 1) { + throw CliktError( + "Directory ${source.directory.absolutePath} should have exactly one hprof " + + "file, not ${hprofFiles.size}: ${hprofFiles.map { it.name }}" + ) + } + hprofFiles.single() + } + is ProcessSource -> dumpHeap(source.processName, source.deviceId) } } diff --git a/shark/shark-heap-growth/api/shark-heap-growth.api b/shark/shark-heap-growth/api/shark-heap-growth.api deleted file mode 100644 index 00e0937b8a..0000000000 --- a/shark/shark-heap-growth/api/shark-heap-growth.api +++ /dev/null @@ -1,70 +0,0 @@ -public final class shark/DiffingHeapGrowthDetector { - public fun (Lshark/ReferenceReader$Factory;Lshark/GcRootProvider;)V - public final fun detectHeapGrowth (Lshark/DiffingHeapGrowthDetector$HeapDumpAfterLoopingScenario;Lshark/InputHeapTraversal;)Lshark/HeapTraversal; - public static synthetic fun detectHeapGrowth$default (Lshark/DiffingHeapGrowthDetector;Lshark/DiffingHeapGrowthDetector$HeapDumpAfterLoopingScenario;Lshark/InputHeapTraversal;ILjava/lang/Object;)Lshark/HeapTraversal; -} - -public final class shark/DiffingHeapGrowthDetector$HeapDumpAfterLoopingScenario { - public fun (Lshark/CloseableHeapGraph;I)V - public final fun component1 ()Lshark/CloseableHeapGraph; - public final fun component2 ()I - public final fun copy (Lshark/CloseableHeapGraph;I)Lshark/DiffingHeapGrowthDetector$HeapDumpAfterLoopingScenario; - public static synthetic fun copy$default (Lshark/DiffingHeapGrowthDetector$HeapDumpAfterLoopingScenario;Lshark/CloseableHeapGraph;IILjava/lang/Object;)Lshark/DiffingHeapGrowthDetector$HeapDumpAfterLoopingScenario; - public fun equals (Ljava/lang/Object;)Z - public final fun getHeapGraph ()Lshark/CloseableHeapGraph; - public final fun getScenarioLoopCount ()I - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public abstract interface class shark/HeapTraversal : shark/InputHeapTraversal { - public static final field Companion Lshark/HeapTraversal$Companion; - public abstract fun getShortestPathTree ()Lshark/ShortestPathNode; -} - -public final class shark/HeapTraversal$Companion { - public final fun getIgnoredReferences ()Ljava/util/List; -} - -public final class shark/HeapTraversalWithDiff : shark/HeapTraversal { - public fun (Lshark/ShortestPathNode;Ljava/util/List;)V - public final fun getGrowingNodes ()Ljava/util/List; - public fun getShortestPathTree ()Lshark/ShortestPathNode; - public fun toString ()Ljava/lang/String; -} - -public final class shark/InitialHeapTraversal : shark/HeapTraversal { - public fun (Lshark/ShortestPathNode;)V - public fun getShortestPathTree ()Lshark/ShortestPathNode; -} - -public abstract interface class shark/InputHeapTraversal { -} - -public final class shark/LiveHeapGrowthDetector { - public fun (ILshark/HeapGraphProvider;ILshark/LoopingHeapGrowthDetector;)V - public final fun detectRepeatedHeapGrowth (Lkotlin/jvm/functions/Function0;)Lshark/HeapTraversalWithDiff; -} - -public final class shark/LoopingHeapGrowthDetector { - public fun (Lshark/DiffingHeapGrowthDetector;)V - public final fun detectRepeatedHeapGrowth (Lkotlin/sequences/Sequence;)Lshark/HeapTraversalWithDiff; -} - -public final class shark/NoHeapTraversalYet : shark/InputHeapTraversal { - public static final field INSTANCE Lshark/NoHeapTraversalYet; -} - -public final class shark/ShortestPathNode { - public fun (Ljava/lang/String;Lshark/ShortestPathNode;Z)V - public final fun getChildren ()Ljava/util/List; - public final fun getChildrenObjectCount ()I - public final fun getChildrenObjectCountIncrease ()I - public final fun getNodeAndEdgeName ()Ljava/lang/String; - public final fun getParent ()Lshark/ShortestPathNode; - public final fun getSelfObjectCount ()I - public final fun getSelfObjectCountIncrease ()I - public final fun pathFromRootAsString ()Ljava/lang/String; - public fun toString ()Ljava/lang/String; -} - diff --git a/shark/shark-heap-growth/gradle.properties b/shark/shark-heap-growth/gradle.properties deleted file mode 100644 index f9141b9498..0000000000 --- a/shark/shark-heap-growth/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -POM_ARTIFACT_ID=shark-heap-growth -POM_NAME=Shark Heap Growth -POM_PACKAGING=jar diff --git a/shark/shark-heap-growth/src/main/java/shark/LoopingHeapGrowthDetector.kt b/shark/shark-heap-growth/src/main/java/shark/LoopingHeapGrowthDetector.kt deleted file mode 100644 index 12df6f4540..0000000000 --- a/shark/shark-heap-growth/src/main/java/shark/LoopingHeapGrowthDetector.kt +++ /dev/null @@ -1,34 +0,0 @@ -package shark - -import shark.DiffingHeapGrowthDetector.HeapDumpAfterLoopingScenario - -class LoopingHeapGrowthDetector( - private val heapGrowthDetector: DiffingHeapGrowthDetector -) { - fun detectRepeatedHeapGrowth( - heapDumps: Sequence - ): HeapTraversalWithDiff { - var i = 1 - var lastDiffResult: InputHeapTraversal = NoHeapTraversalYet - for (heapDump in heapDumps) { - val diffResult = - heapGrowthDetector.detectHeapGrowth(heapDump, lastDiffResult) - if (diffResult is HeapTraversalWithDiff) { - val iterationCount = i * heapDump.scenarioLoopCount - SharkLog.d { - "After $iterationCount (+ ${heapDump.scenarioLoopCount}) iterations and heap dump $i: ${diffResult.growingNodes.size} growing nodes" - } - if (diffResult.growingNodes.isEmpty()) { - return diffResult - } - } - lastDiffResult = diffResult - i++ - } - val finalDiffResult = lastDiffResult - check(finalDiffResult is HeapTraversalWithDiff) { - "finalDiffResult $finalDiffResult should be a HeapDiff as i ${i - 1} should be >= 2" - } - return finalDiffResult - } -} diff --git a/shark/shark/api/shark.api b/shark/shark/api/shark.api index cb0d6781b4..ad4cff1a50 100644 --- a/shark/shark/api/shark.api +++ b/shark/shark/api/shark.api @@ -215,12 +215,52 @@ public final class shark/HeapAnalyzer { public static synthetic fun analyze$default (Lshark/HeapAnalyzer;Ljava/io/File;Lshark/LeakingObjectFinder;Ljava/util/List;ZLjava/util/List;Lshark/MetadataExtractor;Lshark/ProguardMapping;ILjava/lang/Object;)Lshark/HeapAnalysis; } +public final class shark/HeapDumpingObjectGrowthDetector : shark/LiveObjectGrowthDetector { + public fun (ILshark/HeapGraphProvider;ILshark/RepeatedObjectGrowthDetector;)V + public fun findRepeatedlyGrowingObjects (Lkotlin/jvm/functions/Function0;)Ljava/util/List; +} + +public final class shark/HeapGraphObjectGrowthDetector { + public fun (Lshark/GcRootProvider;Lshark/ReferenceReader$Factory;)V + public final fun findGrowingObjects (Lshark/CloseableHeapGraph;ILshark/InputHeapTraversal;)Lshark/HeapTraversal; + public static synthetic fun findGrowingObjects$default (Lshark/HeapGraphObjectGrowthDetector;Lshark/CloseableHeapGraph;ILshark/InputHeapTraversal;ILjava/lang/Object;)Lshark/HeapTraversal; +} + +public final class shark/HeapGraphSequenceObjectGrowthDetector : shark/RepeatedObjectGrowthDetector { + public fun (Lshark/HeapGraphObjectGrowthDetector;)V + public fun findRepeatedlyGrowingObjects (Lkotlin/sequences/Sequence;I)Ljava/util/List; +} + +public abstract interface class shark/HeapTraversal : shark/InputHeapTraversal { + public static final field Companion Lshark/HeapTraversal$Companion; + public abstract fun getShortestPathTree ()Lshark/ShortestPathObjectNode; +} + +public final class shark/HeapTraversal$Companion { + public final fun getIgnoredReferences ()Ljava/util/List; +} + +public final class shark/HeapTraversalWithDiff : shark/HeapTraversal { + public fun (Lshark/ShortestPathObjectNode;Ljava/util/List;)V + public final fun getGrowingNodes ()Ljava/util/List; + public fun getShortestPathTree ()Lshark/ShortestPathObjectNode; + public fun toString ()Ljava/lang/String; +} + public final class shark/IgnoredReferenceMatcher : shark/ReferenceMatcher { public fun (Lshark/ReferencePattern;)V public fun getPattern ()Lshark/ReferencePattern; public fun toString ()Ljava/lang/String; } +public final class shark/InitialHeapTraversal : shark/HeapTraversal { + public fun (Lshark/ShortestPathObjectNode;)V + public fun getShortestPathTree ()Lshark/ShortestPathObjectNode; +} + +public abstract interface class shark/InputHeapTraversal { +} + public final class shark/JavaLocalReferenceReader : shark/ChainingInstanceReferenceReader$VirtualInstanceReferenceReader { public fun (Lshark/HeapGraph;Ljava/util/List;)V public final fun getGraph ()Lshark/HeapGraph; @@ -456,6 +496,26 @@ public final class shark/LibraryLeakReferenceMatcher : shark/ReferenceMatcher { public fun toString ()Ljava/lang/String; } +public abstract interface class shark/LiveObjectGrowthDetector { + public abstract fun findRepeatedlyGrowingObjects (Lkotlin/jvm/functions/Function0;)Ljava/util/List; +} + +public final class shark/LiveObjectGrowthDetector$Config { + public fun (IILshark/HeapGraphProvider;)V + public final fun component1 ()I + public final fun component2 ()I + public final fun component3 ()Lshark/HeapGraphProvider; + public final fun copy (IILshark/HeapGraphProvider;)Lshark/LiveObjectGrowthDetector$Config; + public static synthetic fun copy$default (Lshark/LiveObjectGrowthDetector$Config;IILshark/HeapGraphProvider;ILjava/lang/Object;)Lshark/LiveObjectGrowthDetector$Config; + public final fun create (Lshark/RepeatedObjectGrowthDetector;)Lshark/LiveObjectGrowthDetector; + public fun equals (Ljava/lang/Object;)Z + public final fun getHeapGraphProvider ()Lshark/HeapGraphProvider; + public final fun getMaxHeapDumps ()I + public final fun getScenarioLoopsPerDump ()I + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class shark/MatchingGcRootProvider : shark/GcRootProvider { public fun (Ljava/util/List;)V public fun provideGcRoots (Lshark/HeapGraph;)Lkotlin/sequences/Sequence; @@ -470,6 +530,10 @@ public final class shark/MetadataExtractor$Companion { public final fun getNO_OP ()Lshark/MetadataExtractor; } +public final class shark/NoHeapTraversalYet : shark/InputHeapTraversal { + public static final field INSTANCE Lshark/NoHeapTraversalYet; +} + public final class shark/ObjectDominators { public fun ()V public final fun buildDominatorTree (Lshark/HeapGraph;Ljava/util/List;)Ljava/util/Map; @@ -577,6 +641,11 @@ public abstract class shark/OpenJdkInstanceRefReaders : java/lang/Enum, shark/Ch public static fun values ()[Lshark/OpenJdkInstanceRefReaders; } +public final class shark/OpenJdkReferenceReaderFactory : shark/ReferenceReader$Factory { + public fun (Ljava/util/List;)V + public fun createFor (Lshark/HeapGraph;)Lshark/ReferenceReader; +} + public final class shark/PathFindingResults { public fun (Ljava/util/List;Lshark/DominatorTree;)V public final fun getDominatorTree ()Lshark/DominatorTree; @@ -748,6 +817,16 @@ public abstract interface class shark/ReferenceReader$Factory { public abstract fun createFor (Lshark/HeapGraph;)Lshark/ReferenceReader; } +public abstract interface class shark/RepeatedObjectGrowthDetector { + public abstract fun findRepeatedlyGrowingObjects (Lkotlin/sequences/Sequence;I)Ljava/util/List; +} + +public final class shark/RepeatedObjectGrowthDetectorJvmFactory { + public static final field INSTANCE Lshark/RepeatedObjectGrowthDetectorJvmFactory; + public final fun create (Ljava/util/List;)Lshark/RepeatedObjectGrowthDetector; + public static synthetic fun create$default (Lshark/RepeatedObjectGrowthDetectorJvmFactory;Ljava/util/List;ILjava/lang/Object;)Lshark/RepeatedObjectGrowthDetector; +} + public abstract interface class shark/ShortestPathFinder { public abstract fun findShortestPathsFromGcRoots (Ljava/util/Set;)Lshark/PathFindingResults; } @@ -756,6 +835,19 @@ public abstract interface class shark/ShortestPathFinder$Factory { public abstract fun createFor (Lshark/HeapGraph;)Lshark/ShortestPathFinder; } +public final class shark/ShortestPathObjectNode { + public fun (Ljava/lang/String;Lshark/ShortestPathObjectNode;Z)V + public final fun getChildren ()Ljava/util/List; + public final fun getChildrenObjectCount ()I + public final fun getChildrenObjectCountIncrease ()I + public final fun getNodeAndEdgeName ()Ljava/lang/String; + public final fun getParent ()Lshark/ShortestPathObjectNode; + public final fun getSelfObjectCount ()I + public final fun getSelfObjectCountIncrease ()I + public final fun pathFromRootAsString ()Ljava/lang/String; + public fun toString ()Ljava/lang/String; +} + public final class shark/VirtualizingMatchingReferenceReaderFactory : shark/ReferenceReader$Factory { public fun (Ljava/util/List;Lshark/ChainingInstanceReferenceReader$VirtualInstanceReferenceReader$ChainFactory;)V public fun createFor (Lshark/HeapGraph;)Lshark/ReferenceReader; diff --git a/shark/shark-heap-growth/src/main/java/shark/LiveHeapGrowthDetector.kt b/shark/shark/src/main/java/shark/HeapDumpingObjectGrowthDetector.kt similarity index 51% rename from shark/shark-heap-growth/src/main/java/shark/LiveHeapGrowthDetector.kt rename to shark/shark/src/main/java/shark/HeapDumpingObjectGrowthDetector.kt index 81f830ecff..8f1cf2072b 100644 --- a/shark/shark-heap-growth/src/main/java/shark/LiveHeapGrowthDetector.kt +++ b/shark/shark/src/main/java/shark/HeapDumpingObjectGrowthDetector.kt @@ -1,13 +1,11 @@ package shark -import shark.DiffingHeapGrowthDetector.HeapDumpAfterLoopingScenario - -class LiveHeapGrowthDetector( +class HeapDumpingObjectGrowthDetector( private val maxHeapDumps: Int, private val heapGraphProvider: HeapGraphProvider, private val scenarioLoopsPerDump: Int, - private val detector: LoopingHeapGrowthDetector -) { + private val detector: RepeatedObjectGrowthDetector +) : LiveObjectGrowthDetector { init { check(maxHeapDumps >= 2) { @@ -18,20 +16,19 @@ class LiveHeapGrowthDetector( } } - fun detectRepeatedHeapGrowth(repeatedScenario: () -> Unit): HeapTraversalWithDiff { - val heapDumps = dumpHeapRepeated(repeatedScenario) - return detector.detectRepeatedHeapGrowth(heapDumps) + override fun findRepeatedlyGrowingObjects(roundTripScenario: () -> Unit): List { + val heapDumpSequence = dumpHeapOnNext(roundTripScenario) + return detector.findRepeatedlyGrowingObjects(heapDumpSequence, scenarioLoopsPerDump) } - private fun dumpHeapRepeated( + private fun dumpHeapOnNext( repeatedScenario: () -> Unit, - ): Sequence { - + ): Sequence { val heapDumps = (1..maxHeapDumps).asSequence().map { repeat(scenarioLoopsPerDump) { repeatedScenario() } - HeapDumpAfterLoopingScenario(heapGraphProvider.openHeapGraph(), scenarioLoopsPerDump) + heapGraphProvider.openHeapGraph() } return heapDumps } diff --git a/shark/shark-heap-growth/src/main/java/shark/DiffingHeapGrowthDetector.kt b/shark/shark/src/main/java/shark/HeapGraphObjectGrowthDetector.kt similarity index 92% rename from shark/shark-heap-growth/src/main/java/shark/DiffingHeapGrowthDetector.kt rename to shark/shark/src/main/java/shark/HeapGraphObjectGrowthDetector.kt index 5b35ac0d08..4a53a788f3 100644 --- a/shark/shark-heap-growth/src/main/java/shark/DiffingHeapGrowthDetector.kt +++ b/shark/shark/src/main/java/shark/HeapGraphObjectGrowthDetector.kt @@ -9,25 +9,22 @@ import shark.HeapObject.HeapInstance import shark.HeapObject.HeapObjectArray import shark.HeapObject.HeapPrimitiveArray import shark.ReferenceLocationType.ARRAY_ENTRY +import shark.ReferenceReader.Factory import shark.internal.hppc.LongScatterSet -class DiffingHeapGrowthDetector( - private val referenceReaderFactory: ReferenceReader.Factory, +class HeapGraphObjectGrowthDetector( private val gcRootProvider: GcRootProvider, + private val referenceReaderFactory: Factory, ) { - data class HeapDumpAfterLoopingScenario( - val heapGraph: CloseableHeapGraph, - val scenarioLoopCount: Int - ) - - fun detectHeapGrowth( - heapDump: HeapDumpAfterLoopingScenario, + fun findGrowingObjects( + heapGraph: CloseableHeapGraph, + scenarioLoops: Int, previousTraversal: InputHeapTraversal = NoHeapTraversalYet, ): HeapTraversal { val state = TraversalState() - return heapDump.heapGraph.use { - state.traverseHeapDiffingShortestPaths(heapDump, previousTraversal) + return heapGraph.use { + state.traverseHeapDiffingShortestPaths(heapGraph, scenarioLoops, previousTraversal) } } @@ -44,7 +41,7 @@ class DiffingHeapGrowthDetector( val visitedSet = LongScatterSet() - val tree = ShortestPathNode("root", null, newNode = false).apply { + val tree = ShortestPathObjectNode("root", null, newNode = false).apply { selfObjectCount = 1 } val queuesNotEmpty: Boolean @@ -53,7 +50,8 @@ class DiffingHeapGrowthDetector( @Suppress("ComplexMethod") private fun TraversalState.traverseHeapDiffingShortestPaths( - heapDump: HeapDumpAfterLoopingScenario, + graph: CloseableHeapGraph, + detectedGrowth: Int, previousTraversal: InputHeapTraversal, ): HeapTraversal { @@ -67,9 +65,6 @@ class DiffingHeapGrowthDetector( is HeapTraversal -> previousTraversal.shortestPathTree } - val detectedGrowth = heapDump.scenarioLoopCount - - val graph = heapDump.heapGraph val objectReferenceReader = referenceReaderFactory.createFor(graph) val roots = graph.groupRoots() @@ -256,7 +251,7 @@ class DiffingHeapGrowthDetector( } private fun TraversalState.enqueueRoots( - previousTree: ShortestPathNode?, + previousTree: ShortestPathObjectNode?, roots: Map>> ) { val previousTreeRootMap = previousTree?.let { tree -> @@ -282,8 +277,8 @@ class DiffingHeapGrowthDetector( } private fun TraversalState.enqueue( - parentPathNode: ShortestPathNode, - previousPathNode: ShortestPathNode?, + parentPathNode: ShortestPathObjectNode, + previousPathNode: ShortestPathObjectNode?, objectIds: List, nodeAndEdgeName: String, isLowPriority: Boolean, @@ -304,7 +299,7 @@ class DiffingHeapGrowthDetector( } val shortestPathNode = - ShortestPathNode(nodeAndEdgeName, parentPathNode, newNode = previousPathNode == null) + ShortestPathObjectNode(nodeAndEdgeName, parentPathNode, newNode = previousPathNode == null) val node = Node( objectIds = filteredObjectIds, @@ -322,7 +317,7 @@ class DiffingHeapGrowthDetector( private data class Node( // All objects that you can reach through paths that all resolves to the same structure. val objectIds: Set, - val shortestPathNode: ShortestPathNode, - val previousPathNode: ShortestPathNode? + val shortestPathNode: ShortestPathObjectNode, + val previousPathNode: ShortestPathObjectNode? ) } diff --git a/shark/shark/src/main/java/shark/HeapGraphSequenceObjectGrowthDetector.kt b/shark/shark/src/main/java/shark/HeapGraphSequenceObjectGrowthDetector.kt new file mode 100644 index 0000000000..813c1029f2 --- /dev/null +++ b/shark/shark/src/main/java/shark/HeapGraphSequenceObjectGrowthDetector.kt @@ -0,0 +1,36 @@ +package shark + +class HeapGraphSequenceObjectGrowthDetector( + private val heapGrowthDetector: HeapGraphObjectGrowthDetector +) : RepeatedObjectGrowthDetector { + + override fun findRepeatedlyGrowingObjects( + heapGraphs: Sequence, + scenarioLoopsPerGraph: Int + ): GrowingObjectNodes { + var i = 1 + var lastDiffResult: InputHeapTraversal = NoHeapTraversalYet + for (heapGraph in heapGraphs) { + val diffResult = + heapGrowthDetector.findGrowingObjects( + heapGraph, scenarioLoopsPerGraph, lastDiffResult + ) + if (diffResult is HeapTraversalWithDiff) { + val iterationCount = i * scenarioLoopsPerGraph + SharkLog.d { + "After $iterationCount (+ $scenarioLoopsPerGraph) iterations and heap dump $i: ${diffResult.growingNodes.size} growing nodes" + } + if (diffResult.growingNodes.isEmpty()) { + return emptyList() + } + } + lastDiffResult = diffResult + i++ + } + val finalDiffResult = lastDiffResult + check(finalDiffResult is HeapTraversalWithDiff) { + "finalDiffResult $finalDiffResult should be a HeapDiff as i ${i - 1} should be >= 2" + } + return finalDiffResult.growingNodes + } +} diff --git a/shark/shark-heap-growth/src/main/java/shark/HeapTraversal.kt b/shark/shark/src/main/java/shark/HeapTraversal.kt similarity index 83% rename from shark/shark-heap-growth/src/main/java/shark/HeapTraversal.kt rename to shark/shark/src/main/java/shark/HeapTraversal.kt index a0d9f3e369..b2828dee9c 100644 --- a/shark/shark-heap-growth/src/main/java/shark/HeapTraversal.kt +++ b/shark/shark/src/main/java/shark/HeapTraversal.kt @@ -13,7 +13,7 @@ sealed interface HeapTraversal : InputHeapTraversal { * - Path element names are determined using the edge name to reach them (e.g. field name) and * the object class name. */ - val shortestPathTree: ShortestPathNode + val shortestPathTree: ShortestPathObjectNode companion object { @@ -24,7 +24,7 @@ sealed interface HeapTraversal : InputHeapTraversal { */ val ignoredReferences: List get() { - val shortestPathNodeClass = ShortestPathNode::class.java + val shortestPathNodeClass = ShortestPathObjectNode::class.java return shortestPathNodeClass.declaredFields.map { classField -> IgnoredReferenceMatcher(InstanceFieldPattern(shortestPathNodeClass.name, classField.name)) } @@ -33,16 +33,16 @@ sealed interface HeapTraversal : InputHeapTraversal { } class InitialHeapTraversal constructor( - override val shortestPathTree: ShortestPathNode + override val shortestPathTree: ShortestPathObjectNode ) : HeapTraversal class HeapTraversalWithDiff( - override val shortestPathTree: ShortestPathNode, + override val shortestPathTree: ShortestPathObjectNode, /** * Nodes that already existed in the previous traversal, still exist in this * [shortestPathTree], and have grown compared to the previous traversal. */ - val growingNodes: List + val growingNodes: List ) : HeapTraversal { override fun toString(): String { return "HeapTraversalWithDiff(growingNodes=\n$growingNodes" diff --git a/shark/shark/src/main/java/shark/LiveObjectGrowthDetector.kt b/shark/shark/src/main/java/shark/LiveObjectGrowthDetector.kt new file mode 100644 index 0000000000..ab2e8564d5 --- /dev/null +++ b/shark/shark/src/main/java/shark/LiveObjectGrowthDetector.kt @@ -0,0 +1,21 @@ +package shark + +interface LiveObjectGrowthDetector { + + fun findRepeatedlyGrowingObjects(roundTripScenario: () -> Unit): GrowingObjectNodes + + data class Config( + val maxHeapDumps: Int, + val scenarioLoopsPerDump: Int, + val heapGraphProvider: HeapGraphProvider, + ) { + fun create(objectRepeatedGrowthDetector: RepeatedObjectGrowthDetector): LiveObjectGrowthDetector { + return HeapDumpingObjectGrowthDetector( + maxHeapDumps = maxHeapDumps, + heapGraphProvider = heapGraphProvider, + scenarioLoopsPerDump = scenarioLoopsPerDump, + detector = objectRepeatedGrowthDetector + ) + } + } +} diff --git a/shark/shark/src/main/java/shark/OpenJdkReferenceReaderFactory.kt b/shark/shark/src/main/java/shark/OpenJdkReferenceReaderFactory.kt new file mode 100644 index 0000000000..c263007893 --- /dev/null +++ b/shark/shark/src/main/java/shark/OpenJdkReferenceReaderFactory.kt @@ -0,0 +1,21 @@ +package shark + +class OpenJdkReferenceReaderFactory( + private val referenceMatchers: List +) : ReferenceReader.Factory { + + private val virtualizingFactory = VirtualizingMatchingReferenceReaderFactory( + referenceMatchers = referenceMatchers, + virtualRefReadersFactory = { graph -> + listOf( + JavaLocalReferenceReader(graph, referenceMatchers), + ) + + OpenJdkInstanceRefReaders.values().mapNotNull { it.create(graph) } + + ApacheHarmonyInstanceRefReaders.values().mapNotNull { it.create(graph) } + } + ) + + override fun createFor(heapGraph: HeapGraph): ReferenceReader { + return virtualizingFactory.createFor(heapGraph) + } +} diff --git a/shark/shark/src/main/java/shark/RepeatedObjectGrowthDetector.kt b/shark/shark/src/main/java/shark/RepeatedObjectGrowthDetector.kt new file mode 100644 index 0000000000..2135152c5f --- /dev/null +++ b/shark/shark/src/main/java/shark/RepeatedObjectGrowthDetector.kt @@ -0,0 +1,9 @@ +package shark + +interface RepeatedObjectGrowthDetector { + + fun findRepeatedlyGrowingObjects( + heapGraphs: Sequence, + scenarioLoopsPerGraph: Int + ): GrowingObjectNodes +} diff --git a/shark/shark/src/main/java/shark/RepeatedObjectGrowthDetectorJvmFactory.kt b/shark/shark/src/main/java/shark/RepeatedObjectGrowthDetectorJvmFactory.kt new file mode 100644 index 0000000000..0fe6c34fe5 --- /dev/null +++ b/shark/shark/src/main/java/shark/RepeatedObjectGrowthDetectorJvmFactory.kt @@ -0,0 +1,15 @@ +package shark + +object RepeatedObjectGrowthDetectorJvmFactory { + fun create( + referenceMatchers: List = JdkReferenceMatchers.defaults + + HeapTraversal.ignoredReferences + ): RepeatedObjectGrowthDetector { + return HeapGraphSequenceObjectGrowthDetector( + HeapGraphObjectGrowthDetector( + gcRootProvider = MatchingGcRootProvider(referenceMatchers), + referenceReaderFactory = OpenJdkReferenceReaderFactory(referenceMatchers) + ) + ) + } +} diff --git a/shark/shark-heap-growth/src/main/java/shark/ShortestPathNode.kt b/shark/shark/src/main/java/shark/ShortestPathObjectNode.kt similarity index 81% rename from shark/shark-heap-growth/src/main/java/shark/ShortestPathNode.kt rename to shark/shark/src/main/java/shark/ShortestPathObjectNode.kt index 1d8d54c23b..88fbff9bb0 100644 --- a/shark/shark-heap-growth/src/main/java/shark/ShortestPathNode.kt +++ b/shark/shark/src/main/java/shark/ShortestPathObjectNode.kt @@ -1,13 +1,15 @@ package shark -class ShortestPathNode( +typealias GrowingObjectNodes = List + +class ShortestPathObjectNode( val nodeAndEdgeName: String, - val parent: ShortestPathNode?, + val parent: ShortestPathObjectNode?, internal val newNode: Boolean ) { @Suppress("VariableNaming") - internal val _children = mutableListOf() - val children: List + internal val _children = mutableListOf() + val children: List get() = _children var selfObjectCount = 0 @@ -26,15 +28,15 @@ class ShortestPathNode( override fun toString() = pathFromRootAsString() fun pathFromRootAsString(): String { - val pathFromRoot = mutableListOf() - var unwindingNode: ShortestPathNode? = this + val pathFromRoot = mutableListOf() + var unwindingNode: ShortestPathObjectNode? = this while (unwindingNode != null) { pathFromRoot.add(0, unwindingNode) unwindingNode = unwindingNode.parent } val pathAfterRoot = pathFromRoot.drop(1) val result = StringBuilder() - result.append("┬───").appendLine() + result.append("\n┬───").appendLine() pathAfterRoot.forEachIndexed { index, pathNode -> if (index == 0) { result.append("│ ") diff --git a/shark/shark-heap-growth/src/test/java/shark/HeapGrowthDetectorFakeDumpTest.kt b/shark/shark/src/test/java/shark/HeapGraphObjectGrowthDetectorTest.kt similarity index 77% rename from shark/shark-heap-growth/src/test/java/shark/HeapGrowthDetectorFakeDumpTest.kt rename to shark/shark/src/test/java/shark/HeapGraphObjectGrowthDetectorTest.kt index dfdc542231..923189e638 100644 --- a/shark/shark-heap-growth/src/test/java/shark/HeapGrowthDetectorFakeDumpTest.kt +++ b/shark/shark/src/test/java/shark/HeapGraphObjectGrowthDetectorTest.kt @@ -2,18 +2,18 @@ package shark import org.assertj.core.api.Assertions.assertThat import org.junit.Test -import shark.DiffingHeapGrowthDetector.HeapDumpAfterLoopingScenario import shark.HprofHeapGraph.Companion.openHeapGraph -class HeapGrowthDetectorFakeDumpTest { +class HeapGraphObjectGrowthDetectorTest { @Test fun `first traversal returns InitialHeapTraversal`() { val detector = newSimpleDetector() - val heapTraversal = detector.detectHeapGrowth( - heapDump = dump { + val heapTraversal = detector.findGrowingObjects( + dump { }, + 1, previousTraversal = NoHeapTraversalYet ) @@ -23,13 +23,15 @@ class HeapGrowthDetectorFakeDumpTest { @Test fun `second traversal returns HeapTraversalWithDiff`() { val detector = newSimpleDetector() - val first = detector.detectHeapGrowth( - heapDump = emptyHeapDump(), + val first = detector.findGrowingObjects( + emptyHeapDump(), + 1, previousTraversal = NoHeapTraversalYet ) - val secondTraversal = detector.detectHeapGrowth( - heapDump = emptyHeapDump(), + val secondTraversal = detector.findGrowingObjects( + emptyHeapDump(), + 1, previousTraversal = first ) @@ -42,10 +44,10 @@ class HeapGrowthDetectorFakeDumpTest { val dumps = listOf( dump { classWithStringsInStaticField("Hi") - }, + } to 1, dump { classWithStringsInStaticField("Hi") - } + } to 1 ) val traversal = detector.detectHeapGrowth(dumps) @@ -60,10 +62,10 @@ class HeapGrowthDetectorFakeDumpTest { val dumps = listOf( dump { classWithStringsInStaticField("Hi") - }, + } to 1, dump { classWithStringsInStaticField("Bonjour") - } + } to 1 ) val traversal = detector.detectHeapGrowth(dumps) @@ -77,10 +79,10 @@ class HeapGrowthDetectorFakeDumpTest { val dumps = listOf( dump { classWithStringsInStaticField("Hello") - }, + } to 1, dump { classWithStringsInStaticField("Hello", "World!") - } + } to 1 ) val traversal = detector.detectHeapGrowth(dumps) @@ -94,10 +96,10 @@ class HeapGrowthDetectorFakeDumpTest { val dumps = listOf( dump { classWithStringsInStaticField("Hello") - }, - dump(2) { + } to 1, + dump { classWithStringsInStaticField("Hello", "World!") - } + } to 2 ) val traversal = detector.detectHeapGrowth(dumps) @@ -117,7 +119,7 @@ class HeapGrowthDetectorFakeDumpTest { val strings = (1..stringCount).toList().map { "Hi $it" }.toTypedArray() dump { classWithStringsInStaticField(*strings) - } + } to 1 } val traversal = detector.detectHeapGrowth(dumps) @@ -130,11 +132,11 @@ class HeapGrowthDetectorFakeDumpTest { assertThat(growingNode.children).hasSize(1) } - private fun DiffingHeapGrowthDetector.detectHeapGrowth(heapDumps: List): HeapTraversalWithDiff { - return heapDumps.fold( + private fun HeapGraphObjectGrowthDetector.detectHeapGrowth(heapDumps: List>): HeapTraversalWithDiff { + return heapDumps.fold, InputHeapTraversal>( initial = NoHeapTraversalYet - ) { previous, dump -> - detectHeapGrowth(dump, previous) + ) { previous, (graph, count) -> + findGrowingObjects(graph, count, previous) } as HeapTraversalWithDiff } @@ -148,16 +150,12 @@ class HeapGrowthDetectorFakeDumpTest { private fun emptyHeapDump() = dump {} private fun dump( - scenarioLoopCount: Int = 1, block: HprofWriterHelper.() -> Unit - ): HeapDumpAfterLoopingScenario { - return HeapDumpAfterLoopingScenario( - heapGraph = dump(HprofHeader(), block).openHeapGraph(), - scenarioLoopCount = scenarioLoopCount - ) + ): CloseableHeapGraph { + return dump(HprofHeader(), block).openHeapGraph() } - private fun newSimpleDetector(): DiffingHeapGrowthDetector { + private fun newSimpleDetector(): HeapGraphObjectGrowthDetector { val referenceReaderFactory = ActualMatchingReferenceReaderFactory( referenceMatchers = emptyList() ) @@ -170,6 +168,6 @@ class HeapGrowthDetectorFakeDumpTest { ) } } - return DiffingHeapGrowthDetector(referenceReaderFactory, gcRootProvider) + return HeapGraphObjectGrowthDetector(gcRootProvider, referenceReaderFactory) } } diff --git a/shark/shark-heap-growth/src/test/java/shark/HeapGrowthDetectorJvmTest.kt b/shark/shark/src/test/java/shark/LiveObjectGrowthDetectorTest.kt similarity index 69% rename from shark/shark-heap-growth/src/test/java/shark/HeapGrowthDetectorJvmTest.kt rename to shark/shark/src/test/java/shark/LiveObjectGrowthDetectorTest.kt index 1b56b95507..9813cbe286 100644 --- a/shark/shark-heap-growth/src/test/java/shark/HeapGrowthDetectorJvmTest.kt +++ b/shark/shark/src/test/java/shark/LiveObjectGrowthDetectorTest.kt @@ -7,7 +7,7 @@ import org.junit.Test import org.junit.rules.TemporaryFolder import shark.HprofHeapGraph.Companion.openHeapGraph -class HeapGrowthDetectorJvmTest { +class LiveObjectGrowthDetectorTest { class Leaky @@ -36,20 +36,20 @@ class HeapGrowthDetectorJvmTest { val emptyScenario = {} - val heapTraversal = detector.detectRepeatedHeapGrowth(emptyScenario) + val growingNodes = detector.findRepeatedlyGrowingObjects(emptyScenario) - assertThat(heapTraversal.growingNodes).isEmpty() + assertThat(growingNodes).isEmpty() } @Test fun `leaky increase leads to heap growth`() { val detector = simpleDetector().live() - val heapTraversal = detector.detectRepeatedHeapGrowth { + val growingNodes = detector.findRepeatedlyGrowingObjects { leakies += Leaky() } - assertThat(heapTraversal.growingNodes).isNotEmpty + assertThat(growingNodes).isNotEmpty } @Test @@ -57,11 +57,11 @@ class HeapGrowthDetectorJvmTest { val detector = simpleDetector().live() var index = 0 - val heapTraversal = detector.detectRepeatedHeapGrowth { + val growingNodes = detector.findRepeatedlyGrowingObjects { stringLeaks += "Yo ${++index}" } - assertThat(heapTraversal.growingNodes).isNotEmpty + assertThat(growingNodes).isNotEmpty } @Test @@ -71,13 +71,13 @@ class HeapGrowthDetectorJvmTest { val detector = simpleDetector().live(maxHeapDumps = maxHeapDumps) var index = 0 - val heapTraversal = detector.detectRepeatedHeapGrowth { + val growingNodes = detector.findRepeatedlyGrowingObjects { if (++index < stopLeakingIndex) { leakies += Leaky() } } - assertThat(heapTraversal.growingNodes).isEmpty() + assertThat(growingNodes).isEmpty() } @Test @@ -85,13 +85,13 @@ class HeapGrowthDetectorJvmTest { val scenarioLoopsPerDump = 5 val detector = simpleDetector().live(scenarioLoopsPerDump = scenarioLoopsPerDump) - val heapTraversal = detector.detectRepeatedHeapGrowth { + val growingNodes = detector.findRepeatedlyGrowingObjects { leakies += Leaky() } - assertThat(heapTraversal.growingNodes).hasSize(1) + assertThat(growingNodes).hasSize(1) - val growingNode = heapTraversal.growingNodes.first() + val growingNode = growingNodes.first() assertThat(growingNode.childrenObjectCountIncrease).isEqualTo(scenarioLoopsPerDump) } @@ -99,27 +99,27 @@ class HeapGrowthDetectorJvmTest { fun `custom leaky linked list leads to heap growth`() { val detector = simpleDetector().live() - val heapTraversal = detector.detectRepeatedHeapGrowth { + val growingNodes = detector.findRepeatedlyGrowingObjects { customLeakyLinkedList = CustomLinkedList(customLeakyLinkedList) } - assertThat(heapTraversal.growingNodes).isNotEmpty + assertThat(growingNodes).isNotEmpty } @Test fun `custom leaky linked list reports descendant to root as flattened collection`() { val detector = simpleDetector().live() - val heapTraversal = detector.detectRepeatedHeapGrowth { + val growingNodes = detector.findRepeatedlyGrowingObjects { customLeakyLinkedList = CustomLinkedList(customLeakyLinkedList) customLeakyLinkedList = CustomLinkedList(customLeakyLinkedList) customLeakyLinkedList = CustomLinkedList(customLeakyLinkedList) customLeakyLinkedList = CustomLinkedList(customLeakyLinkedList) } - assertThat(heapTraversal.growingNodes).hasSize(1) + assertThat(growingNodes).hasSize(1) - val growingNode = heapTraversal.growingNodes.first() + val growingNode = growingNodes.first() assertThat(growingNode.children.size).isEqualTo(1) assertThat(growingNode.childrenObjectCountIncrease).isEqualTo(4) } @@ -128,13 +128,13 @@ class HeapGrowthDetectorJvmTest { fun `growth along shared sub paths reported as single growth of shortest path`() { val detector = simpleDetector().live() - val heapTraversal = detector.detectRepeatedHeapGrowth { + val growingNodes = detector.findRepeatedlyGrowingObjects { multiLeakies += MultiLeaky() } - assertThat(heapTraversal.growingNodes).hasSize(1) + assertThat(growingNodes).hasSize(1) - val growingNode = heapTraversal.growingNodes.first() + val growingNode = growingNodes.first() assertThat(growingNode.nodeAndEdgeName).contains("ArrayList") } @@ -143,11 +143,11 @@ class HeapGrowthDetectorJvmTest { val detector = openJdkDetector().live() var index = 0 - val heapTraversal = detector.detectRepeatedHeapGrowth { + val growingNodes = detector.findRepeatedlyGrowingObjects { leakyHashMap["key${++index}"] = Leaky() } - val growingNode = heapTraversal.growingNodes.first() + val growingNode = growingNodes.first() assertThat(growingNode.nodeAndEdgeName).contains("leakyHashMap") } @@ -155,29 +155,29 @@ class HeapGrowthDetectorJvmTest { fun `OpenJdk ArrayList virtualized as array`() { val detector = openJdkDetector().live() - val heapTraversal = detector.detectRepeatedHeapGrowth { + val growingNodes = detector.findRepeatedlyGrowingObjects { leakies += Leaky() } - val growingNode = heapTraversal.growingNodes.first() + val growingNode = growingNodes.first() assertThat(growingNode.nodeAndEdgeName).contains("leakies") } - private fun DiffingHeapGrowthDetector.live( + private fun HeapGraphObjectGrowthDetector.live( scenarioLoopsPerDump: Int = 1, maxHeapDumps: Int = 5 - ): LiveHeapGrowthDetector { - return LiveHeapGrowthDetector(maxHeapDumps, ::dumpHeapGraph, scenarioLoopsPerDump, LoopingHeapGrowthDetector(this)) + ): HeapDumpingObjectGrowthDetector { + return HeapDumpingObjectGrowthDetector(maxHeapDumps, ::dumpHeapGraph, scenarioLoopsPerDump, HeapGraphSequenceObjectGrowthDetector(this)) } - private fun simpleDetector(): DiffingHeapGrowthDetector { + private fun simpleDetector(): HeapGraphObjectGrowthDetector { val referenceMatchers = JdkReferenceMatchers.defaults + HeapTraversal.ignoredReferences val referenceReaderFactory = ActualMatchingReferenceReaderFactory(referenceMatchers) val gcRootProvider = MatchingGcRootProvider(referenceMatchers) - return DiffingHeapGrowthDetector(referenceReaderFactory, gcRootProvider) + return HeapGraphObjectGrowthDetector(gcRootProvider, referenceReaderFactory) } - private fun openJdkDetector(): DiffingHeapGrowthDetector { + private fun openJdkDetector(): HeapGraphObjectGrowthDetector { val referenceMatchers = JdkReferenceMatchers.defaults + HeapTraversal.ignoredReferences val referenceReaderFactory = VirtualizingMatchingReferenceReaderFactory( @@ -191,7 +191,7 @@ class HeapGrowthDetectorJvmTest { ) val gcRootProvider = MatchingGcRootProvider(referenceMatchers) - return DiffingHeapGrowthDetector(referenceReaderFactory, gcRootProvider) + return HeapGraphObjectGrowthDetector(gcRootProvider, referenceReaderFactory) } private fun dumpHeapGraph(): CloseableHeapGraph {