Merge pull request #2676 from square/py/improve_heap_diff
Iterating on heap growth based on friction / feedback from trying to integrate it inside Square.
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 8e58e6d..43265b8 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -41,6 +41,7 @@
# and they'll automatically resolve to higher version without having to necessarily resort to a
# resolution strategy.
androidX-fragment = { module = "androidx.fragment:fragment", version = "1.0.0" }
+androidX-multidex = { module = "androidx.multidex:multidex", version = "2.0.1" }
# Exposed transitively, avoid increasing
androidX-startup = { module = "androidx.startup:startup-runtime", version = "1.0.0" }
androidX-test-core = { module = "androidx.test:core", version = "1.4.0" }
@@ -55,6 +56,7 @@
androidX-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version = "2.2.0" }
androidX-work-runtime = { module = "androidx.work:work-runtime", version.ref = "workManager" }
androidX-work-multiprocess = { module = "androidx.work:work-multiprocess", version.ref = "workManager" }
+androidX-collections = { module = "androidx.collection:collection-ktx", version = "1.4.0" }
androidSupport = { module = "com.android.support:support-v4", version = "28.0.0" }
assertjCore = { module = "org.assertj:assertj-core", version = "3.9.1" }
diff --git a/leakcanary/leakcanary-android-instrumentation/build.gradle b/leakcanary/leakcanary-android-instrumentation/build.gradle
index 16c7a58..5bbf189 100644
--- a/leakcanary/leakcanary-android-instrumentation/build.gradle
+++ b/leakcanary/leakcanary-android-instrumentation/build.gradle
@@ -15,6 +15,7 @@
androidTestImplementation projects.objectWatcher.objectWatcherAndroid
// Plumber auto installer for running tests
androidTestImplementation projects.plumber.plumberAndroid
+ androidTestImplementation libs.androidX.multidex
androidTestImplementation libs.androidX.test.core
androidTestImplementation libs.androidX.test.espresso
androidTestImplementation libs.androidX.test.rules
@@ -28,6 +29,7 @@
targetSdk versions.compileSdk
minSdk versions.minSdk
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ multiDexEnabled true
}
buildFeatures.buildConfig = false
namespace 'com.squareup.leakcanary.instrumentation'
diff --git a/leakcanary/leakcanary-android-instrumentation/src/androidTest/AndroidManifest.xml b/leakcanary/leakcanary-android-instrumentation/src/androidTest/AndroidManifest.xml
index 2743ca7..700c2fc 100644
--- a/leakcanary/leakcanary-android-instrumentation/src/androidTest/AndroidManifest.xml
+++ b/leakcanary/leakcanary-android-instrumentation/src/androidTest/AndroidManifest.xml
@@ -16,7 +16,9 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
- <application>
+ <application
+ android:name="androidx.multidex.MultiDexApplication"
+ >
<activity android:name="leakcanary.TestActivity"/>
</application>
</manifest>
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 1b3f5b8..fc65e2d 100644
--- a/leakcanary/leakcanary-android-test/src/main/java/leakcanary/RepeatingAndroidInProcessScenario.kt
+++ b/leakcanary/leakcanary-android-test/src/main/java/leakcanary/RepeatingAndroidInProcessScenario.kt
@@ -1,8 +1,8 @@
package leakcanary
-import shark.RepeatingScenarioObjectGrowthDetector
import shark.HeapGraphProvider
import shark.ObjectGrowthDetector
+import shark.RepeatingScenarioObjectGrowthDetector
import shark.repeatingScenario
/**
@@ -23,7 +23,8 @@
return repeatingScenario(
heapGraphProvider = HeapGraphProvider.dumpingAndDeleting(
heapDumper = HeapDumper.forAndroidInProcess()
- .withGc(gcTrigger = GcTrigger.inProcess()),
+ .withGc(gcTrigger = GcTrigger.inProcess())
+ .withDetectorWarmup(this),
heapDumpFileProvider = HeapDumpFileProvider.tempFile()
),
maxHeapDumps = maxHeapDumps,
diff --git a/leakcanary/leakcanary-core/api/leakcanary-core.api b/leakcanary/leakcanary-core/api/leakcanary-core.api
index 78f756d..af0bddc 100644
--- a/leakcanary/leakcanary-core/api/leakcanary-core.api
+++ b/leakcanary/leakcanary-core/api/leakcanary-core.api
@@ -49,6 +49,19 @@
public static synthetic fun withGc$default (Lleakcanary/HeapDumper;Lleakcanary/GcTrigger;ILjava/lang/Object;)Lleakcanary/HeapDumper;
}
+public final class leakcanary/ObjectGrowthWarmupHeapDumper : leakcanary/HeapDumper {
+ public static final field Companion Lleakcanary/ObjectGrowthWarmupHeapDumper$Companion;
+ public fun <init> (Lshark/ObjectGrowthDetector;Lleakcanary/HeapDumper;)V
+ public fun dumpHeap (Ljava/io/File;)V
+}
+
+public final class leakcanary/ObjectGrowthWarmupHeapDumper$Companion {
+}
+
+public final class leakcanary/ObjectGrowthWarmupHeapDumperKt {
+ public static final fun withDetectorWarmup (Lleakcanary/HeapDumper;Lshark/ObjectGrowthDetector;)Lleakcanary/HeapDumper;
+}
+
public final class leakcanary/TempHeapDumpFileProvider : leakcanary/HeapDumpFileProvider {
public static final field INSTANCE Lleakcanary/TempHeapDumpFileProvider;
public fun newHeapDumpFile ()Ljava/io/File;
diff --git a/leakcanary/leakcanary-core/build.gradle b/leakcanary/leakcanary-core/build.gradle
index 41ae014..35ba8a2 100644
--- a/leakcanary/leakcanary-core/build.gradle
+++ b/leakcanary/leakcanary-core/build.gradle
@@ -9,4 +9,9 @@
dependencies {
api projects.leakcanary.leakcanaryGc
api projects.shark.shark
+ implementation libs.okio2
+
+ testImplementation libs.assertjCore
+ testImplementation libs.junit
+ testImplementation projects.shark.sharkHprofTest
}
diff --git a/leakcanary/leakcanary-core/src/main/java/leakcanary/ObjectGrowthWarmupHeapDumper.kt b/leakcanary/leakcanary-core/src/main/java/leakcanary/ObjectGrowthWarmupHeapDumper.kt
new file mode 100644
index 0000000..ee184cb
--- /dev/null
+++ b/leakcanary/leakcanary-core/src/main/java/leakcanary/ObjectGrowthWarmupHeapDumper.kt
@@ -0,0 +1,59 @@
+package leakcanary
+
+import java.io.File
+import shark.ByteArraySourceProvider
+import shark.ObjectGrowthDetector
+import shark.repeatingScenario
+import okio.ByteString.Companion.decodeHex
+import shark.HprofHeapGraph.Companion.openHeapGraph
+
+class ObjectGrowthWarmupHeapDumper(
+ private val objectGrowthDetector: ObjectGrowthDetector,
+ private val delegate: HeapDumper
+) : HeapDumper {
+
+ private var warm = false
+
+ override fun dumpHeap(heapDumpFile: File) {
+ if (!warm) {
+ warmup()
+ warm = true
+ }
+ delegate.dumpHeap(heapDumpFile)
+ }
+
+ private fun warmup() {
+ val heapDumpsAsHex = listOf({ heapDump1Hex() }, { heapDump2Hex() }, { heapDump3Hex() })
+ val heapDumpsAsHexIterator = heapDumpsAsHex.iterator()
+ val warmupDetector = objectGrowthDetector.repeatingScenario(
+ heapGraphProvider = {
+ ByteArraySourceProvider(
+ heapDumpsAsHexIterator.next()().decodeHex().toByteArray()
+ ).openHeapGraph()
+ },
+ maxHeapDumps = heapDumpsAsHex.size,
+ scenarioLoopsPerDump = 1
+ )
+ warmupDetector.findRepeatedlyGrowingObjects {}
+ }
+
+ @SuppressWarnings("MaxLineLength")
+ companion object {
+ // Header:
+ // 4a4156412050524f46494c4520312e302e33 is the header version string
+ // then 00 is the string separator
+ // 00000004 is the identifier byte size, 4 bytes
+ // 0b501e7e ca55e77e (obsolete cassette) is a cool heap dump timestamp.
+ internal fun heapDump1Hex() =
+ "4a4156412050524f46494c4520312e302e3300000000040b501e7eca55e77e01000000000000001b000000016a6176612e6c616e672e7265662e5265666572656e63650200000000000000100000000100000002000000010000000101000000000000000c000000037265666572656e74010000000000000014000000046a6176612e6c616e672e4f626a656374020000000000000010000000010000000500000001000000040c000000000000006520000000050000000100000000000000000000000000000000000000000000000000000000000000000000050000000520000000020000000100000005000000000000000000000000000000000000000000000004000000000001000000030205000000022c000000000000000001000000000000001f000000066a6176612e6c616e672e7265662e5765616b5265666572656e6365020000000000000010000000010000000700000001000000060c00000000000000302000000007000000010000000200000000000000000000000000000000000000000000000400000000000005000000072c0000000000000000010000000000000021000000086c65616b63616e6172792e4b657965645765616b5265666572656e6365020000000000000010000000010000000900000001000000080100000000000000180000000a6865617044756d70557074696d654d696c6c69730100000000000000070000000b6b65790100000000000000080000000c6e616d650100000000000000150000000d7761746368557074696d654d696c6c69730100000000000000180000000e72657461696e6564557074696d654d696c6c69730c00000000000000622000000009000000010000000700000000000000000000000000000000000000000000001c000000010000000a0b000000000000753000040000000b020000000c020000000d0b0000000e0b0500000009210000000f0000000100000005000000002c0000000000000000010000000000000016000000106a6176612e6c616e672e4f626a6563745b5d020000000000000010000000010000001100000001000000100c000000000000004520000000110000000100000005000000000000000000000000000000000000000000000000000000000000050000001122000000120000000100000001000000110000000f2c000000000000000001000000000000000a00000013486f6c64657202000000000000001000000001000000140000000100000013010000000000000008000000156c6973740c00000000000000392000000014000000010000000500000000000000000000000000000000000000000000000000000001000000150200000012000005000000142c0000000000000000"
+
+ internal fun heapDump2Hex() =
+ "4a4156412050524f46494c4520312e302e3300000000040b501e7eca55e77e01000000000000001b000000016a6176612e6c616e672e7265662e5265666572656e63650200000000000000100000000100000002000000010000000101000000000000000c000000037265666572656e74010000000000000014000000046a6176612e6c616e672e4f626a656374020000000000000010000000010000000500000001000000040c000000000000006520000000050000000100000000000000000000000000000000000000000000000000000000000000000000050000000520000000020000000100000005000000000000000000000000000000000000000000000004000000000001000000030205000000022c000000000000000001000000000000001f000000066a6176612e6c616e672e7265662e5765616b5265666572656e6365020000000000000010000000010000000700000001000000060c00000000000000302000000007000000010000000200000000000000000000000000000000000000000000000400000000000005000000072c0000000000000000010000000000000021000000086c65616b63616e6172792e4b657965645765616b5265666572656e6365020000000000000010000000010000000900000001000000080100000000000000180000000a6865617044756d70557074696d654d696c6c69730100000000000000070000000b6b65790100000000000000080000000c6e616d650100000000000000150000000d7761746368557074696d654d696c6c69730100000000000000180000000e72657461696e6564557074696d654d696c6c69730c00000000000000732000000009000000010000000700000000000000000000000000000000000000000000001c000000010000000a0b000000000000753000040000000b020000000c020000000d0b0000000e0b0500000009210000000f00000001000000050000000021000000100000000100000005000000002c0000000000000000010000000000000016000000116a6176612e6c616e672e4f626a6563745b5d020000000000000010000000010000001200000001000000110c000000000000004920000000120000000100000005000000000000000000000000000000000000000000000000000000000000050000001222000000130000000100000002000000120000000f000000102c000000000000000001000000000000000a00000014486f6c64657202000000000000001000000001000000150000000100000014010000000000000008000000166c6973740c00000000000000392000000015000000010000000500000000000000000000000000000000000000000000000000000001000000160200000013000005000000152c0000000000000000"
+
+ internal fun heapDump3Hex() =
+ "4a4156412050524f46494c4520312e302e3300000000040b501e7eca55e77e01000000000000001b000000016a6176612e6c616e672e7265662e5265666572656e63650200000000000000100000000100000002000000010000000101000000000000000c000000037265666572656e74010000000000000014000000046a6176612e6c616e672e4f626a656374020000000000000010000000010000000500000001000000040c000000000000006520000000050000000100000000000000000000000000000000000000000000000000000000000000000000050000000520000000020000000100000005000000000000000000000000000000000000000000000004000000000001000000030205000000022c000000000000000001000000000000001f000000066a6176612e6c616e672e7265662e5765616b5265666572656e6365020000000000000010000000010000000700000001000000060c00000000000000302000000007000000010000000200000000000000000000000000000000000000000000000400000000000005000000072c0000000000000000010000000000000021000000086c65616b63616e6172792e4b657965645765616b5265666572656e6365020000000000000010000000010000000900000001000000080100000000000000180000000a6865617044756d70557074696d654d696c6c69730100000000000000070000000b6b65790100000000000000080000000c6e616d650100000000000000150000000d7761746368557074696d654d696c6c69730100000000000000180000000e72657461696e6564557074696d654d696c6c69730c00000000000000842000000009000000010000000700000000000000000000000000000000000000000000001c000000010000000a0b000000000000753000040000000b020000000c020000000d0b0000000e0b0500000009210000000f000000010000000500000000210000001000000001000000050000000021000000110000000100000005000000002c0000000000000000010000000000000016000000126a6176612e6c616e672e4f626a6563745b5d020000000000000010000000010000001300000001000000120c000000000000004d20000000130000000100000005000000000000000000000000000000000000000000000000000000000000050000001322000000140000000100000003000000130000000f00000010000000112c000000000000000001000000000000000a00000015486f6c64657202000000000000001000000001000000160000000100000015010000000000000008000000176c6973740c00000000000000392000000016000000010000000500000000000000000000000000000000000000000000000000000001000000170200000014000005000000162c0000000000000000"
+ }
+}
+
+fun HeapDumper.withDetectorWarmup(objectGrowthDetector: ObjectGrowthDetector): HeapDumper =
+ ObjectGrowthWarmupHeapDumper(objectGrowthDetector, this)
diff --git a/leakcanary/leakcanary-core/src/test/java/leakcanary/ObjectGrowthWarmupHeapDumperTest.kt b/leakcanary/leakcanary-core/src/test/java/leakcanary/ObjectGrowthWarmupHeapDumperTest.kt
new file mode 100644
index 0000000..002d0da
--- /dev/null
+++ b/leakcanary/leakcanary-core/src/test/java/leakcanary/ObjectGrowthWarmupHeapDumperTest.kt
@@ -0,0 +1,48 @@
+package leakcanary
+
+import okio.ByteString.Companion.decodeHex
+import okio.ByteString.Companion.toByteString
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import shark.HprofHeader
+import shark.dumpToBytes
+
+class ObjectGrowthWarmupHeapDumperTest {
+
+ @Test fun `heap dump 1 as hex constant matches generated heap dump hex`() {
+ assertThat(ObjectGrowthWarmupHeapDumper.heapDump1Hex()).isEqualTo(dumpGrowingListHeapAsHex(1))
+ }
+
+ @Test fun `heap dump 2 as hex constant matches generated heap dump hex`() {
+ assertThat(ObjectGrowthWarmupHeapDumper.heapDump2Hex()).isEqualTo(dumpGrowingListHeapAsHex(2))
+ }
+
+ @Test fun `heap dump 3 as hex constant matches generated heap dump hex`() {
+ assertThat(ObjectGrowthWarmupHeapDumper.heapDump3Hex()).isEqualTo(dumpGrowingListHeapAsHex(3))
+ }
+
+ private fun dumpGrowingListHeapAsHex(listItemCount: Int): String {
+ val heapDumpTimestamp = ("0b501e7e" + "ca55e77e").decodeHex().toByteArray().toLong()
+ return dumpToBytes(hprofHeader = HprofHeader(heapDumpTimestamp = heapDumpTimestamp)) {
+ "Holder" clazz {
+ val refs = (1..listItemCount).map {
+ instance(objectClassId)
+ }.toTypedArray()
+ staticField["list"] = objectArray(*refs)
+ }
+ }.toByteString().hex()
+ }
+
+ private fun ByteArray.toLong(): Long {
+ check(size == 8)
+ var pos = 0
+ return (this[pos++].toLong() and 0xffL shl 56
+ or (this[pos++].toLong() and 0xffL shl 48)
+ or (this[pos++].toLong() and 0xffL shl 40)
+ or (this[pos++].toLong() and 0xffL shl 32)
+ or (this[pos++].toLong() and 0xffL shl 24)
+ or (this[pos++].toLong() and 0xffL shl 16)
+ or (this[pos++].toLong() and 0xffL shl 8)
+ or (this[pos].toLong() and 0xffL))
+ }
+}
diff --git a/leakcanary/leakcanary-jvm-test/build.gradle b/leakcanary/leakcanary-jvm-test/build.gradle
index 7fcc007..5226407 100644
--- a/leakcanary/leakcanary-jvm-test/build.gradle
+++ b/leakcanary/leakcanary-jvm-test/build.gradle
@@ -12,4 +12,5 @@
testImplementation libs.assertjCore
testImplementation libs.junit
+ testImplementation projects.shark.sharkHprofTest
}
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 45fb828..9dede19 100644
--- a/leakcanary/leakcanary-jvm-test/src/main/java/leakcanary/RepeatingJvmInProcessScenario.kt
+++ b/leakcanary/leakcanary-jvm-test/src/main/java/leakcanary/RepeatingJvmInProcessScenario.kt
@@ -23,7 +23,8 @@
return repeatingScenario(
heapGraphProvider = HeapGraphProvider.dumpingAndDeleting(
heapDumper = HeapDumper.forJvmInProcess()
- .withGc(gcTrigger = GcTrigger.inProcess()),
+ .withGc(gcTrigger = GcTrigger.inProcess())
+ .withDetectorWarmup(this),
heapDumpFileProvider = HeapDumpFileProvider.tempFile()
),
maxHeapDumps = maxHeapDumps,
diff --git a/leakcanary/leakcanary-jvm-test/src/test/java/leakcanary/JvmHeapGrowthDetectorConfigTest.kt b/leakcanary/leakcanary-jvm-test/src/test/java/leakcanary/JvmHeapGrowthDetectorConfigTest.kt
index e6b614c..142c5d7 100644
--- a/leakcanary/leakcanary-jvm-test/src/test/java/leakcanary/JvmHeapGrowthDetectorConfigTest.kt
+++ b/leakcanary/leakcanary-jvm-test/src/test/java/leakcanary/JvmHeapGrowthDetectorConfigTest.kt
@@ -2,9 +2,7 @@
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
-import shark.HeapGraphProvider
import shark.ObjectGrowthDetector
-import shark.repeatingScenario
import shark.forJvmHeap
class JvmHeapGrowthDetectorConfigTest {
@@ -15,7 +13,10 @@
@Test
fun `leaky increase leads to heap growth`() {
- val detector = ObjectGrowthDetector.forJvmHeap().repeatingJvmInProcessScenario()
+ val detector = ObjectGrowthDetector.forJvmHeap().repeatingJvmInProcessScenario(
+ maxHeapDumps = 2,
+ scenarioLoopsPerDump = 10
+ )
val growingNodes = detector.findRepeatedlyGrowingObjects {
leakies += Leaky()
diff --git a/leakcanary/leakcanary-jvm-test/src/test/java/leakcanary/JvmLiveObjectGrowthDetectorTest.kt b/leakcanary/leakcanary-jvm-test/src/test/java/leakcanary/JvmLiveObjectGrowthDetectorTest.kt
new file mode 100644
index 0000000..9ac88ac
--- /dev/null
+++ b/leakcanary/leakcanary-jvm-test/src/test/java/leakcanary/JvmLiveObjectGrowthDetectorTest.kt
@@ -0,0 +1,197 @@
+package leakcanary
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import shark.ActualMatchingReferenceReaderFactory
+import shark.JvmObjectGrowthReferenceMatchers
+import shark.MatchingGcRootProvider
+import shark.ObjectGrowthDetector
+import shark.forJvmHeap
+
+class JvmLiveObjectGrowthDetectorTest {
+
+ class MultiLeaky {
+ val leaky = Any() to Any()
+ }
+
+ class CustomLinkedList(var next: CustomLinkedList? = null)
+
+ @get:Rule
+ val testFolder = TemporaryFolder()
+
+ val leakies = mutableListOf<Any>()
+
+ val stringLeaks = mutableListOf<String>()
+
+ var customLeakyLinkedList = CustomLinkedList()
+
+ val leakyHashMap = HashMap<String, Any>()
+
+ val multiLeakies = mutableListOf<MultiLeaky>()
+
+ @Test
+ fun `empty scenario leads to no heap growth`() {
+ val detector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs()
+ .repeatingJvmInProcessScenario(scenarioLoopsPerDump = 1)
+
+ val emptyScenario = {}
+
+ val heapTraversal = detector.findRepeatedlyGrowingObjects(roundTripScenario = emptyScenario)
+
+ assertThat(heapTraversal.growingObjects).isEmpty()
+ }
+
+ @Test
+ fun `leaky increase leads to heap growth`() {
+ val detector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs()
+ .repeatingJvmInProcessScenario(scenarioLoopsPerDump = 1)
+
+ val heapTraversal = detector.findRepeatedlyGrowingObjects {
+ leakies += Any()
+ }
+
+ assertThat(heapTraversal.growingObjects).hasSize(1)
+ }
+
+ @Test
+ fun `string leak increase leads to heap growth`() {
+ val detector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs()
+ .repeatingJvmInProcessScenario(scenarioLoopsPerDump = 1)
+
+ var index = 0
+ val heapTraversal = detector.findRepeatedlyGrowingObjects {
+ stringLeaks += "Yo ${++index}"
+ }
+
+ assertThat(heapTraversal.growingObjects).hasSize(1)
+ }
+
+ @Test
+ fun `leak increase that ends leads to no heap growth`() {
+ val maxHeapDumps = 10
+ val stopLeakingIndex = maxHeapDumps / 2
+ val detector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs()
+ .repeatingJvmInProcessScenario(
+ maxHeapDumps = maxHeapDumps,
+ scenarioLoopsPerDump = 1
+ )
+
+ var index = 0
+ val heapTraversal = detector.findRepeatedlyGrowingObjects {
+ if (++index < stopLeakingIndex) {
+ leakies += Any()
+ }
+ }
+
+ assertThat(heapTraversal.growingObjects).isEmpty()
+ }
+
+ @Test
+ fun `multiple leaky scenarios per dump leads to heap growth`() {
+ val scenarioLoopsPerDump = 5
+ val detector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs()
+ .repeatingJvmInProcessScenario(scenarioLoopsPerDump = scenarioLoopsPerDump)
+
+ val heapTraversal = detector.findRepeatedlyGrowingObjects {
+ leakies += Any()
+ }
+
+ val growingObject = heapTraversal.growingObjects.single()
+ val growingChild = growingObject.growingChildren.single()
+ assertThat(growingChild.objectCountIncrease).isEqualTo(scenarioLoopsPerDump)
+ }
+
+ @Test
+ fun `detect growth of custom linked list`() {
+ val detector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs()
+ .repeatingJvmInProcessScenario(scenarioLoopsPerDump = 1)
+
+ val heapTraversal = detector.findRepeatedlyGrowingObjects {
+ customLeakyLinkedList = CustomLinkedList(customLeakyLinkedList)
+ }
+
+ assertThat(heapTraversal.growingObjects).hasSize(1)
+ }
+
+ @Test
+ fun `custom leaky linked list reports descendant to root as flattened collection`() {
+ val detector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs()
+ .repeatingJvmInProcessScenario(scenarioLoopsPerDump = 1)
+
+ val heapTraversal = detector.findRepeatedlyGrowingObjects {
+ customLeakyLinkedList = CustomLinkedList(customLeakyLinkedList)
+ customLeakyLinkedList = CustomLinkedList(customLeakyLinkedList)
+ customLeakyLinkedList = CustomLinkedList(customLeakyLinkedList)
+ customLeakyLinkedList = CustomLinkedList(customLeakyLinkedList)
+ }
+
+ val growingObject = heapTraversal.growingObjects.single()
+ val growingChild = growingObject.growingChildren.single()
+ assertThat(growingChild.objectCountIncrease).isEqualTo(4)
+ }
+
+ @Test
+ fun `growth along shared sub paths reported as single growth of shortest path`() {
+ val detector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs()
+ .repeatingJvmInProcessScenario(scenarioLoopsPerDump = 1)
+
+ val heapTraversal = detector.findRepeatedlyGrowingObjects {
+ multiLeakies += MultiLeaky()
+ }
+
+ val growingObject = heapTraversal.growingObjects.single()
+ assertThat(growingObject.name).contains("ArrayList")
+ }
+
+ @Test
+ fun `OpenJdk HashMap without synthetic refs shows internal table array growing`() {
+ val detector = ObjectGrowthDetector.forJvmHeapNoSyntheticRefs()
+ .repeatingJvmInProcessScenario(scenarioLoopsPerDump = 1)
+
+ var index = 0
+ val heapTraversal = detector.findRepeatedlyGrowingObjects {
+ leakyHashMap["key${++index}"] = Any()
+ }
+
+ val growingObject = heapTraversal.growingObjects.single()
+ assertThat(growingObject.name).startsWith("INSTANCE_FIELD HashMap.table")
+ }
+
+ @Test
+ fun `OpenJdk HashMap with synthetic refs shows itself growing`() {
+ val detector = ObjectGrowthDetector.forJvmHeap()
+ .repeatingJvmInProcessScenario(scenarioLoopsPerDump = 1)
+
+ var index = 0
+ val heapTraversal = detector.findRepeatedlyGrowingObjects {
+ leakyHashMap["key${++index}"] = Any()
+ }
+
+ val growingObject = heapTraversal.growingObjects.single()
+ assertThat(growingObject.name)
+ .startsWith("INSTANCE_FIELD JvmLiveObjectGrowthDetectorTest.leakyHashMap")
+ }
+
+ @Test
+ fun `OpenJdk ArrayList virtualized as array`() {
+ val detector = ObjectGrowthDetector.forJvmHeap()
+ .repeatingJvmInProcessScenario(scenarioLoopsPerDump = 1)
+
+ val heapTraversal = detector.findRepeatedlyGrowingObjects {
+ leakies += Any()
+ }
+
+ val growingObject = heapTraversal.growingObjects.single()
+ assertThat(growingObject.name).contains("leakies")
+ }
+
+ private fun ObjectGrowthDetector.Companion.forJvmHeapNoSyntheticRefs(): ObjectGrowthDetector {
+ val referenceMatchers = JvmObjectGrowthReferenceMatchers.defaults
+ return ObjectGrowthDetector(
+ gcRootProvider = MatchingGcRootProvider(JvmObjectGrowthReferenceMatchers.defaults),
+ referenceReaderFactory = ActualMatchingReferenceReaderFactory(referenceMatchers)
+ )
+ }
+}
diff --git a/shark/shark-cli/src/main/java/shark/HeapGrowthCommand.kt b/shark/shark-cli/src/main/java/shark/HeapGrowthCommand.kt
index daa512c..0d4ff1e 100644
--- a/shark/shark-cli/src/main/java/shark/HeapGrowthCommand.kt
+++ b/shark/shark-cli/src/main/java/shark/HeapGrowthCommand.kt
@@ -106,7 +106,7 @@
.repeatingHeapGraph()
val results = detector.findRepeatedlyGrowingObjects(
heapGraphSequence = heapGraphs,
- initialState = InitialState(scenarioLoopsPerDump, hprofFiles.size),
+ initialState = InitialState(scenarioLoopsPerDump),
).also {
previous?.let {
val finishTime = System.nanoTime().nanoseconds
@@ -173,8 +173,7 @@
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'.")
- val command = consoleReader.readCommand()
- when (command) {
+ when (val command = consoleReader.readCommand()) {
"q" -> throw PrintMessage("Quitting.")
"" -> {
promptForCommand = false
@@ -190,7 +189,7 @@
val nextInputTraversal = if (reset) {
FirstHeapTraversal(
shortestPathTree = latestTraversal.shortestPathTree.copyResettingAsInitialTree(),
- previousTraversal = InitialState(latestTraversal.scenarioLoopsPerGraph, null)
+ previousTraversal = InitialState(latestTraversal.scenarioLoopsPerGraph)
)
} else {
latestTraversal
diff --git a/shark/shark-graph/build.gradle b/shark/shark-graph/build.gradle
index 7aa4e6c..ab83749 100644
--- a/shark/shark-graph/build.gradle
+++ b/shark/shark-graph/build.gradle
@@ -8,6 +8,7 @@
dependencies {
api projects.shark.sharkHprof
+ api libs.androidX.collections
implementation libs.kotlin.stdlib
implementation libs.okio2
diff --git a/shark/shark-hprof-test/src/main/kotlin/shark/HprofWriterHelper.kt b/shark/shark-hprof-test/src/main/kotlin/shark/HprofWriterHelper.kt
index 8198732..e475b66 100644
--- a/shark/shark-hprof-test/src/main/kotlin/shark/HprofWriterHelper.kt
+++ b/shark/shark-hprof-test/src/main/kotlin/shark/HprofWriterHelper.kt
@@ -55,26 +55,41 @@
private val classDumps = mutableMapOf<Long, ClassDumpRecord>()
- private val objectClassId = clazz(superclassId = 0, className = "java.lang.Object")
- private val objectArrayClassId = arrayClass("java.lang.Object")
- private val stringClassId = clazz(
- className = "java.lang.String", fields = listOf(
- "value" to ReferenceHolder::class,
- "count" to IntHolder::class
- )
- )
-
- private val referenceClassId = clazz(
- className = "java.lang.ref.Reference",
- fields = listOf(
- "referent" to ReferenceHolder::class
+ val objectClassId by lazy {
+ clazz(
+ superclassId = 0,
+ className = "java.lang.Object"
)
- )
+ }
+ private val objectArrayClassId by lazy {
+ arrayClass("java.lang.Object")
+ }
- private val weakReferenceClassId = clazz(
- className = "java.lang.ref.WeakReference",
- superclassId = referenceClassId
- )
+ private val stringClassId by lazy {
+ clazz(
+ className = "java.lang.String",
+ fields = listOf(
+ "value" to ReferenceHolder::class,
+ "count" to IntHolder::class
+ )
+ )
+ }
+
+ private val referenceClassId by lazy {
+ clazz(
+ className = "java.lang.ref.Reference",
+ fields = listOf(
+ "referent" to ReferenceHolder::class
+ )
+ )
+ }
+
+ private val weakReferenceClassId by lazy {
+ clazz(
+ className = "java.lang.ref.WeakReference",
+ superclassId = referenceClassId
+ )
+ }
private val keyedWeakReferenceClassId = clazz(
superclassId = weakReferenceClassId,
className = "leakcanary.KeyedWeakReference",
diff --git a/shark/shark/api/shark.api b/shark/shark/api/shark.api
index c75f40e..c6577c1 100644
--- a/shark/shark/api/shark.api
+++ b/shark/shark/api/shark.api
@@ -73,14 +73,11 @@
}
public final class shark/ByteSize : java/lang/Comparable {
- public static final field BYTES_PER_GB J
- public static final field BYTES_PER_KB J
- public static final field BYTES_PER_MB J
- public static final field Companion Lshark/ByteSize$Companion;
public static final synthetic fun box-impl (J)Lshark/ByteSize;
public synthetic fun compareTo (Ljava/lang/Object;)I
public fun compareTo-rK2stxE (J)I
public static fun compareTo-rK2stxE (JJ)I
+ public static fun constructor-impl (J)J
public static final fun div-BWD4q2E (JJ)J
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (JLjava/lang/Object;)Z
@@ -99,16 +96,19 @@
public final synthetic fun unbox-impl ()J
}
-public final class shark/ByteSize$Companion {
- public final fun getBytes-1A9dbZA (I)J
- public final fun getBytes-1A9dbZA (J)J
- public final fun getGigabytes-1A9dbZA (I)J
- public final fun getGigabytes-1A9dbZA (J)J
- public final fun getKilobytes-1A9dbZA (I)J
- public final fun getKilobytes-1A9dbZA (J)J
- public final fun getMegabytes-1A9dbZA (I)J
- public final fun getMegabytes-1A9dbZA (J)J
- public final fun getZERO-UyN4wxk ()J
+public final class shark/ByteSizeKt {
+ public static final field BYTES_PER_GB J
+ public static final field BYTES_PER_KB J
+ public static final field BYTES_PER_MB J
+ public static final fun getBytes (I)J
+ public static final fun getBytes (J)J
+ public static final fun getGigabytes (I)J
+ public static final fun getGigabytes (J)J
+ public static final fun getKilobytes (I)J
+ public static final fun getKilobytes (J)J
+ public static final fun getMegabytes (I)J
+ public static final fun getMegabytes (J)J
+ public static final fun getZERO_BYTES ()J
}
public final class shark/ChainingInstanceReferenceReader : shark/ReferenceReader {
@@ -141,8 +141,9 @@
public fun <init> (I)V
public synthetic fun <init> (IILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun buildFullDominatorTree (Lshark/DominatorTree$ObjectSizeCalculator;)Ljava/util/Map;
- public final fun computeRetainedSizes (Ljava/util/Set;Lshark/DominatorTree$ObjectSizeCalculator;)Ljava/util/Map;
+ public final fun computeRetainedSizes (Landroidx/collection/LongSet;Lshark/DominatorTree$ObjectSizeCalculator;)Landroidx/collection/LongLongMap;
public final fun contains (J)Z
+ public final fun get (J)J
public final fun updateDominated (JJ)Z
public final fun updateDominatedAsRoot (J)Z
}
@@ -173,7 +174,6 @@
public final class shark/FirstHeapTraversal : shark/HeapTraversalOutput {
public fun <init> (Lshark/ShortestPathObjectNode;Lshark/InitialState;)V
- public fun getHeapGraphCount ()Ljava/lang/Integer;
public fun getScenarioLoopsPerGraph ()I
public fun getShortestPathTree ()Lshark/ShortestPathObjectNode;
public fun getTraversalCount ()I
@@ -282,7 +282,6 @@
public final class shark/HeapGrowthTraversal : shark/HeapTraversalOutput {
public fun <init> (ILshark/ShortestPathObjectNode;Ljava/util/List;Lshark/HeapTraversalInput;)V
public final fun getGrowingObjects ()Ljava/util/List;
- public fun getHeapGraphCount ()Ljava/lang/Integer;
public fun getScenarioLoopsPerGraph ()I
public fun getShortestPathTree ()Lshark/ShortestPathObjectNode;
public fun getTraversalCount ()I
@@ -291,7 +290,6 @@
}
public abstract interface class shark/HeapTraversalInput {
- public abstract fun getHeapGraphCount ()Ljava/lang/Integer;
public abstract fun getScenarioLoopsPerGraph ()I
public abstract fun getTraversalCount ()I
}
@@ -314,14 +312,18 @@
}
public final class shark/InitialState : shark/HeapTraversalInput {
+ public static final field Companion Lshark/InitialState$Companion;
+ public static final field DEFAULT_SCENARIO_LOOPS_PER_GRAPH I
public fun <init> ()V
- public fun <init> (ILjava/lang/Integer;)V
- public synthetic fun <init> (ILjava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
- public fun getHeapGraphCount ()Ljava/lang/Integer;
+ public fun <init> (I)V
+ public synthetic fun <init> (IILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun getScenarioLoopsPerGraph ()I
public fun getTraversalCount ()I
}
+public final class shark/InitialState$Companion {
+}
+
public final class shark/JavaLocalReferenceReader : shark/ChainingInstanceReferenceReader$VirtualInstanceReferenceReader {
public fun <init> (Lshark/HeapGraph;Ljava/util/List;)V
public final fun getGraph ()Lshark/HeapGraph;
@@ -357,6 +359,20 @@
public static synthetic fun forJvmHeap$default (Lshark/ObjectGrowthDetector$Companion;Ljava/util/List;ILjava/lang/Object;)Lshark/ObjectGrowthDetector;
}
+public abstract class shark/JvmObjectGrowthReferenceMatchers : java/lang/Enum, shark/ReferenceMatcher$ListBuilder {
+ public static final field Companion Lshark/JvmObjectGrowthReferenceMatchers$Companion;
+ public static final field HEAP_TRAVERSAL Lshark/JvmObjectGrowthReferenceMatchers;
+ public static final field JVM_LEAK_DETECTION_IGNORED_MATCHERS Lshark/JvmObjectGrowthReferenceMatchers;
+ public static final field PARALLEL_LOCK_MAP Lshark/JvmObjectGrowthReferenceMatchers;
+ public synthetic fun <init> (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public static fun valueOf (Ljava/lang/String;)Lshark/JvmObjectGrowthReferenceMatchers;
+ public static fun values ()[Lshark/JvmObjectGrowthReferenceMatchers;
+}
+
+public final class shark/JvmObjectGrowthReferenceMatchers$Companion {
+ public final fun getDefaults ()Ljava/util/List;
+}
+
public final class shark/KeyedWeakReferenceFinder : shark/LeakingObjectFinder {
public static final field INSTANCE Lshark/KeyedWeakReferenceFinder;
public fun findLeakingObjectIds (Lshark/HeapGraph;)Ljava/util/Set;
@@ -937,6 +953,30 @@
public static synthetic fun repeatingScenario$default (Lshark/ObjectGrowthDetector;Lshark/HeapGraphProvider;IIILjava/lang/Object;)Lshark/RepeatingScenarioObjectGrowthDetector;
}
+public final class shark/Retained {
+ public final field packedValue J
+ public static final synthetic fun box-impl (J)Lshark/Retained;
+ public static fun constructor-impl (J)J
+ public fun equals (Ljava/lang/Object;)Z
+ public static fun equals-impl (JLjava/lang/Object;)Z
+ public static final fun equals-impl0 (JJ)Z
+ public static final fun getHeapSize-UyN4wxk (J)J
+ public static final fun getObjectCount-impl (J)I
+ public fun hashCode ()I
+ public static fun hashCode-impl (J)I
+ public static final fun isUnknown-impl (J)Z
+ public static final fun isZero-impl (J)Z
+ public fun toString ()Ljava/lang/String;
+ public static fun toString-impl (J)Ljava/lang/String;
+ public final synthetic fun unbox-impl ()J
+}
+
+public final class shark/RetainedKt {
+ public static final fun Retained-5mcd9r4 (JI)J
+ public static final fun getUNKNOWN_RETAINED ()J
+ public static final fun getZERO_RETAINED ()J
+}
+
public abstract interface class shark/ShortestPathFinder {
public abstract fun findShortestPathsFromGcRoots (Ljava/util/Set;)Lshark/PathFindingResults;
}
@@ -946,27 +986,30 @@
}
public final class shark/ShortestPathObjectNode {
- public fun <init> (Ljava/lang/String;Lshark/ShortestPathObjectNode;Z)V
+ public fun <init> (Ljava/lang/String;Lshark/ShortestPathObjectNode;)V
public final fun copyResettingAsInitialTree ()Lshark/ShortestPathObjectNode;
public final fun getChildren ()Ljava/util/List;
- public final fun getChildrenObjectCount ()I
- public final fun getChildrenObjectCountIncrease ()I
+ public final fun getGrowingChildren ()Ljava/util/List;
public final fun getName ()Ljava/lang/String;
public final fun getParent ()Lshark/ShortestPathObjectNode;
- public final fun getRetained ()Lshark/ShortestPathObjectNode$Retained;
- public final fun getRetainedIncrease ()Lshark/ShortestPathObjectNode$Retained;
- public final fun getRetainedIncreaseOrNull ()Lshark/ShortestPathObjectNode$Retained;
- public final fun getRetainedOrNull ()Lshark/ShortestPathObjectNode$Retained;
+ public final fun getRetained-bh0qJVg ()J
+ public final fun getRetainedIncrease-bh0qJVg ()J
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/ShortestPathObjectNode$Retained {
- public synthetic fun <init> (JILkotlin/jvm/internal/DefaultConstructorMarker;)V
- public final fun getHeapSize-UyN4wxk ()J
- public final fun getObjectCount ()I
+public final class shark/ShortestPathObjectNode$GrowingChildNode {
+ public fun <init> (Lshark/ShortestPathObjectNode;I)V
+ public final fun component1 ()Lshark/ShortestPathObjectNode;
+ public final fun component2 ()I
+ public final fun copy (Lshark/ShortestPathObjectNode;I)Lshark/ShortestPathObjectNode$GrowingChildNode;
+ public static synthetic fun copy$default (Lshark/ShortestPathObjectNode$GrowingChildNode;Lshark/ShortestPathObjectNode;IILjava/lang/Object;)Lshark/ShortestPathObjectNode$GrowingChildNode;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getChild ()Lshark/ShortestPathObjectNode;
+ public final fun getObjectCountIncrease ()I
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
}
public final class shark/VirtualizingMatchingReferenceReaderFactory : shark/ReferenceReader$Factory {
@@ -974,6 +1017,12 @@
public fun createFor (Lshark/HeapGraph;)Lshark/ReferenceReader;
}
+public final class shark/internal/IntIntPairUtilsKt {
+ public static final fun getUnpackAsFirstInt (J)I
+ public static final fun getUnpackAsSecondInt (J)I
+ public static final fun packedWith (II)J
+}
+
public final class shark/internal/InternalSharkCollectionsHelper {
public static final field INSTANCE Lshark/internal/InternalSharkCollectionsHelper;
public final fun arrayListValues (Lshark/HeapObject$HeapInstance;)Lkotlin/sequences/Sequence;
diff --git a/shark/shark/build.gradle b/shark/shark/build.gradle
index 3e9461c..84310f2 100644
--- a/shark/shark/build.gradle
+++ b/shark/shark/build.gradle
@@ -15,7 +15,6 @@
testImplementation libs.assertjCore
testImplementation libs.junit
- testImplementation libs.okio2
testImplementation projects.shark.sharkTest
testImplementation projects.shark.sharkHprofTest
}
diff --git a/shark/shark/src/main/java/shark/ByteSize.kt b/shark/shark/src/main/java/shark/ByteSize.kt
index 0732107..57cd689 100644
--- a/shark/shark/src/main/java/shark/ByteSize.kt
+++ b/shark/shark/src/main/java/shark/ByteSize.kt
@@ -3,18 +3,20 @@
/**
* Inspired by https://github.com/saket/file-size as well as Kotlin's Duration API.
*/
+// DO NOT ADD A COMPANION OBJECT: a value class is supposed to be lightweight and its usage inlined
+// into few instructions. After adding a companion object, call sites get a lot more instructions.
@JvmInline
-value class ByteSize private constructor(
+value class ByteSize constructor(
val inWholeBytes: Long
) : Comparable<ByteSize> {
- val inWholeKilobytes: Long
+ inline val inWholeKilobytes: Long
get() = inWholeBytes / BYTES_PER_KB
- val inWholeMegabytes: Long
+ inline val inWholeMegabytes: Long
get() = inWholeBytes / BYTES_PER_MB
- val inWholeGigabytes: Long
+ inline val inWholeGigabytes: Long
get() = inWholeBytes / BYTES_PER_GB
override fun toString(): String {
@@ -28,33 +30,31 @@
override operator fun compareTo(other: ByteSize) = inWholeBytes.compareTo(other.inWholeBytes)
- operator fun plus(other: ByteSize): ByteSize =
+ inline operator fun plus(other: ByteSize): ByteSize =
ByteSize(inWholeBytes = inWholeBytes + other.inWholeBytes)
- operator fun minus(other: ByteSize): ByteSize =
+ inline operator fun minus(other: ByteSize): ByteSize =
ByteSize(inWholeBytes = inWholeBytes - other.inWholeBytes)
- operator fun times(other: ByteSize): ByteSize =
+ inline operator fun times(other: ByteSize): ByteSize =
ByteSize(inWholeBytes * other.inWholeBytes)
- operator fun div(other: ByteSize): ByteSize =
+ inline operator fun div(other: ByteSize): ByteSize =
ByteSize(inWholeBytes / other.inWholeBytes)
-
- companion object {
- const val BYTES_PER_KB: Long = 1_000L
- const val BYTES_PER_MB: Long = 1_000L * BYTES_PER_KB
- const val BYTES_PER_GB: Long = 1_000L * BYTES_PER_MB
-
- val ZERO: ByteSize = ByteSize(0L)
-
- val Long.bytes get() = ByteSize(this)
- val Long.kilobytes get() = ByteSize(this * BYTES_PER_KB)
- val Long.megabytes get() = ByteSize(this * BYTES_PER_MB)
- val Long.gigabytes get() = ByteSize(this * BYTES_PER_GB)
-
- val Int.bytes get() = ByteSize(toLong())
- val Int.kilobytes get() = ByteSize(this * BYTES_PER_KB)
- val Int.megabytes get() = ByteSize(this * BYTES_PER_MB)
- val Int.gigabytes get() = ByteSize(this * BYTES_PER_GB)
- }
}
+
+const val BYTES_PER_KB: Long = 1_000L
+const val BYTES_PER_MB: Long = 1_000L * BYTES_PER_KB
+const val BYTES_PER_GB: Long = 1_000L * BYTES_PER_MB
+
+val ZERO_BYTES: ByteSize = ByteSize(0L)
+
+inline val Long.bytes get() = ByteSize(this)
+inline val Long.kilobytes get() = ByteSize(this * BYTES_PER_KB)
+inline val Long.megabytes get() = ByteSize(this * BYTES_PER_MB)
+inline val Long.gigabytes get() = ByteSize(this * BYTES_PER_GB)
+
+inline val Int.bytes get() = ByteSize(toLong())
+inline val Int.kilobytes get() = ByteSize(this * BYTES_PER_KB)
+inline val Int.megabytes get() = ByteSize(this * BYTES_PER_MB)
+inline val Int.gigabytes get() = ByteSize(this * BYTES_PER_GB)
diff --git a/shark/shark/src/main/java/shark/ClassReferenceReader.kt b/shark/shark/src/main/java/shark/ClassReferenceReader.kt
index 1a0a88c..a415687 100644
--- a/shark/shark/src/main/java/shark/ClassReferenceReader.kt
+++ b/shark/shark/src/main/java/shark/ClassReferenceReader.kt
@@ -32,13 +32,20 @@
override fun read(source: HeapClass): Sequence<Reference> {
val ignoredStaticFields = staticFieldNameByClassName[source.name] ?: emptyMap()
- return source.readStaticFields().mapNotNull { staticField ->
+ return source.readStaticFields().mapNotNull { staticField ->
// not non null: no null + no primitives.
if (!staticField.value.isNonNullReference) {
return@mapNotNull null
}
val fieldName = staticField.name
- if (fieldName == "\$staticOverhead" || fieldName == "\$classOverhead") {
+ if (
+ // Android noise
+ fieldName == "\$staticOverhead" ||
+ // Android noise
+ fieldName == "\$classOverhead" ||
+ // JVM noise
+ fieldName == "<resolved_references>"
+ ) {
return@mapNotNull null
}
diff --git a/shark/shark/src/main/java/shark/DominatorTree.kt b/shark/shark/src/main/java/shark/DominatorTree.kt
index c918297..e8f7c28 100644
--- a/shark/shark/src/main/java/shark/DominatorTree.kt
+++ b/shark/shark/src/main/java/shark/DominatorTree.kt
@@ -1,10 +1,18 @@
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "CANNOT_OVERRIDE_INVISIBLE_MEMBER")
+
package shark
+import androidx.collection.LongLongMap
+import androidx.collection.LongSet
+import androidx.collection.MutableLongLongMap
+import androidx.collection.MutableLongSet
import shark.ObjectDominators.DominatorNode
import shark.internal.hppc.LongLongScatterMap
import shark.internal.hppc.LongLongScatterMap.ForEachCallback
import shark.internal.hppc.LongScatterSet
+import shark.internal.packedWith
+import shark.internal.unpackAsFirstInt
+import shark.internal.unpackAsSecondInt
class DominatorTree(expectedElements: Int = 4) {
@@ -23,6 +31,12 @@
operator fun contains(objectId: Long): Boolean = dominated.containsKey(objectId)
/**
+ * Returns the dominator object id or [ValueHolder.NULL_REFERENCE] if [dominatedObjectId] is the
+ * root dominator.
+ */
+ operator fun get(dominatedObjectId: Long) = dominated[dominatedObjectId]
+
+ /**
* Records that [objectId] is a root.
*/
fun updateDominatedAsRoot(objectId: Long): Boolean {
@@ -106,7 +120,8 @@
fun buildFullDominatorTree(objectSizeCalculator: ObjectSizeCalculator): Map<Long, DominatorNode> {
val dominators = mutableMapOf<Long, MutableDominatorNode>()
- dominated.forEach(ForEachCallback {key, value ->
+ // Reverse the dominated map to have dominators ids as keys and list of dominated as values
+ dominated.forEach(ForEachCallback { key, value ->
// create entry for dominated
dominators.getOrPut(key) {
MutableDominatorNode()
@@ -118,7 +133,12 @@
}.dominated += key
})
- val allReachableObjectIds = dominators.keys.toSet() - ValueHolder.NULL_REFERENCE
+ val allReachableObjectIds = MutableLongSet(dominators.size)
+ dominators.forEach { (key, _) ->
+ if (key != ValueHolder.NULL_REFERENCE) {
+ allReachableObjectIds += key
+ }
+ }
val retainedSizes = computeRetainedSizes(allReachableObjectIds) { objectId ->
val shallowSize = objectSizeCalculator.computeSize(objectId)
@@ -128,7 +148,9 @@
dominators.forEach { (objectId, node) ->
if (objectId != ValueHolder.NULL_REFERENCE) {
- val (retainedSize, retainedCount) = retainedSizes.getValue(objectId)
+ val retainedPacked = retainedSizes[objectId]
+ val retainedSize = retainedPacked.unpackAsFirstInt
+ val retainedCount = retainedPacked.unpackAsSecondInt
node.retainedSize = retainedSize
node.retainedCount = retainedCount
}
@@ -157,12 +179,12 @@
* @return a map of object id to retained size.
*/
fun computeRetainedSizes(
- retainedObjectIds: Set<Long>,
+ retainedObjectIds: LongSet,
objectSizeCalculator: ObjectSizeCalculator
- ): Map<Long, Pair<Int, Int>> {
- val nodeRetainedSizes = mutableMapOf<Long, Pair<Int, Int>>()
+ ): LongLongMap {
+ val nodeRetainedSizes = MutableLongLongMap(retainedObjectIds.size)
retainedObjectIds.forEach { objectId ->
- nodeRetainedSizes[objectId] = 0 to 0
+ nodeRetainedSizes[objectId] = 0 packedWith 0
}
dominated.forEach(object : ForEachCallback {
@@ -174,9 +196,15 @@
var instanceSize = -1
// If the entry is a node, add its size to nodeRetainedSizes
- nodeRetainedSizes[key]?.let { (currentRetainedSize, currentRetainedCount) ->
+
+ val missing = -1 packedWith -1
+ val packedRetained = nodeRetainedSizes.getOrDefault(key, missing)
+ if (packedRetained != missing) {
+ val currentRetainedSize = packedRetained.unpackAsFirstInt
+ val currentRetainedCount = packedRetained.unpackAsSecondInt
instanceSize = objectSizeCalculator.computeSize(key)
- nodeRetainedSizes[key] = currentRetainedSize + instanceSize to currentRetainedCount + 1
+ nodeRetainedSizes[key] =
+ (currentRetainedSize + instanceSize) packedWith currentRetainedCount + 1
}
if (value != ValueHolder.NULL_REFERENCE) {
@@ -195,11 +223,11 @@
instanceSize = objectSizeCalculator.computeSize(key)
}
// Update retained size for that node
- val (currentRetainedSize, currentRetainedCount) = nodeRetainedSizes.getValue(
- dominator
- )
+ val dominatorRetained = nodeRetainedSizes[dominator]
+ val currentRetainedSize = dominatorRetained.unpackAsFirstInt
+ val currentRetainedCount = dominatorRetained.unpackAsSecondInt
nodeRetainedSizes[dominator] =
- (currentRetainedSize + instanceSize) to currentRetainedCount + 1
+ (currentRetainedSize + instanceSize) packedWith (currentRetainedCount + 1)
dominatedByNextNode.clear()
} else {
dominatedByNextNode += dominator
@@ -218,3 +246,4 @@
return nodeRetainedSizes
}
}
+
diff --git a/shark/shark/src/main/java/shark/HeapTraversal.kt b/shark/shark/src/main/java/shark/HeapTraversal.kt
index 5bca9e1..8050f6a 100644
--- a/shark/shark/src/main/java/shark/HeapTraversal.kt
+++ b/shark/shark/src/main/java/shark/HeapTraversal.kt
@@ -11,16 +11,10 @@
* growing at least [scenarioLoopsPerGraph] times since the previous traversal.
*/
val scenarioLoopsPerGraph: Int
-
- /**
- * The expected max number of traversals, or null if unknown.
- */
- val heapGraphCount: Int?
}
class InitialState(
- override val scenarioLoopsPerGraph: Int = 1,
- override val heapGraphCount: Int? = null
+ override val scenarioLoopsPerGraph: Int = DEFAULT_SCENARIO_LOOPS_PER_GRAPH,
) : HeapTraversalInput {
override val traversalCount = 0
@@ -28,9 +22,10 @@
check(scenarioLoopsPerGraph >= 1) {
"There should be at least 1 scenario loop per heap dump"
}
- check(heapGraphCount == null || heapGraphCount >= 2) {
- "There should be at least 2 heap dumps to detect growing objects"
- }
+ }
+
+ companion object {
+ const val DEFAULT_SCENARIO_LOOPS_PER_GRAPH = 1
}
}
@@ -71,7 +66,6 @@
) : HeapTraversalOutput {
override val traversalCount = 1
override val scenarioLoopsPerGraph = previousTraversal.scenarioLoopsPerGraph
- override val heapGraphCount: Int? = previousTraversal.heapGraphCount
}
class HeapGrowthTraversal(
@@ -88,15 +82,8 @@
val isGrowing: Boolean get() = growingObjects.isNotEmpty()
override val scenarioLoopsPerGraph = previousTraversal.scenarioLoopsPerGraph
- override val heapGraphCount: Int? = previousTraversal.heapGraphCount
override fun toString(): String {
- val traversal = if (heapGraphCount != null) {
- "traversal=$traversalCount/$heapGraphCount"
- } else {
- "traversal=$traversalCount"
- }
-
- return "HeapGrowthTraversal($traversal, " +
+ return "HeapGrowthTraversal(traversal=$traversalCount, " +
"isGrowing=$isGrowing, " +
"scenarioLoopsPerGraph=$scenarioLoopsPerGraph, " +
"growingNodes=\n${growingObjects.joinToString("\n")}\n" +
diff --git a/shark/shark/src/main/java/shark/JvmObjectGrowthDetector.kt b/shark/shark/src/main/java/shark/JvmObjectGrowthDetector.kt
index 5365d3f..837b28c 100644
--- a/shark/shark/src/main/java/shark/JvmObjectGrowthDetector.kt
+++ b/shark/shark/src/main/java/shark/JvmObjectGrowthDetector.kt
@@ -1,8 +1,7 @@
package shark
fun ObjectGrowthDetector.Companion.forJvmHeap(
- referenceMatchers: List<ReferenceMatcher> = JdkReferenceMatchers.defaults +
- HeapTraversalOutput.ignoredReferences
+ referenceMatchers: List<ReferenceMatcher> = JvmObjectGrowthReferenceMatchers.defaults
): ObjectGrowthDetector {
return ObjectGrowthDetector(
gcRootProvider = MatchingGcRootProvider(referenceMatchers),
diff --git a/shark/shark/src/main/java/shark/JvmObjectGrowthReferenceMatchers.kt b/shark/shark/src/main/java/shark/JvmObjectGrowthReferenceMatchers.kt
new file mode 100644
index 0000000..35e6922
--- /dev/null
+++ b/shark/shark/src/main/java/shark/JvmObjectGrowthReferenceMatchers.kt
@@ -0,0 +1,37 @@
+package shark
+
+import java.util.EnumSet
+import shark.ReferenceMatcher.Companion.ALWAYS
+import shark.ReferencePattern.Companion.instanceField
+
+enum class JvmObjectGrowthReferenceMatchers : ReferenceMatcher.ListBuilder {
+
+ JVM_LEAK_DETECTION_IGNORED_MATCHERS {
+ override fun add(references: MutableList<ReferenceMatcher>) {
+ references += JdkReferenceMatchers.defaults.filterIsInstance<IgnoredReferenceMatcher>()
+ }
+ },
+
+ HEAP_TRAVERSAL {
+ override fun add(references: MutableList<ReferenceMatcher>) {
+ references += HeapTraversalOutput.ignoredReferences
+ }
+ },
+
+ PARALLEL_LOCK_MAP {
+ override fun add(references: MutableList<ReferenceMatcher>) {
+ // Seems to be sometimes growing at a fast pace. JVM only ("Android-removed: Remove unused ParallelLoaders")
+ references += instanceField("java.lang.ClassLoader", "parallelLockMap")
+ .ignored(patternApplies = ALWAYS)
+ }
+ },
+
+ ;
+
+ companion object {
+ val defaults: List<ReferenceMatcher>
+ get() = ReferenceMatcher.fromListBuilders(
+ EnumSet.allOf(JvmObjectGrowthReferenceMatchers::class.java)
+ )
+ }
+}
diff --git a/shark/shark/src/main/java/shark/ObjectGrowthDetector.kt b/shark/shark/src/main/java/shark/ObjectGrowthDetector.kt
index db2ad84..4a2ec3e 100644
--- a/shark/shark/src/main/java/shark/ObjectGrowthDetector.kt
+++ b/shark/shark/src/main/java/shark/ObjectGrowthDetector.kt
@@ -2,16 +2,19 @@
package shark
+import androidx.collection.MutableLongList
+import androidx.collection.MutableLongLongMap
+import androidx.collection.MutableLongSet
+import androidx.collection.mutableLongListOf
import java.util.ArrayDeque
import java.util.Deque
-import shark.ByteSize.Companion.bytes
import shark.HeapObject.HeapClass
import shark.HeapObject.HeapInstance
import shark.HeapObject.HeapObjectArray
import shark.HeapObject.HeapPrimitiveArray
import shark.ReferenceLocationType.ARRAY_ENTRY
-import shark.ShortestPathObjectNode.Retained
-import shark.internal.hppc.LongScatterSet
+import shark.internal.unpackAsFirstInt
+import shark.internal.unpackAsSecondInt
/**
* Looks for objects that have grown in outgoing references in a new heap dump compared to a
@@ -31,18 +34,10 @@
"previousTraversal:$previousTraversal"
}
- val computeRetainedHeapSize = previousTraversal.heapGraphCount?.let { heapGraphCount ->
- // Compute retained size for the prior to last and the last graphs.
- previousTraversal.traversalCount >= heapGraphCount - 1
- } ?: (previousTraversal.traversalCount > 1)
-
// Estimate of how many objects we'll visit. This is a conservative estimate, we should always
// 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,
- computeRetainedHeapSize = computeRetainedHeapSize
- )
+ val state = TraversalState(estimatedVisitedObjects = estimatedVisitedObjects)
return heapGraph.use {
state.traverseHeapDiffingShortestPaths(
heapGraph,
@@ -59,9 +54,19 @@
}
}
+ // data class to be a properly implemented key.
+ private data class EdgeKey(
+ val nodeAndEdgeName: String,
+ val isLowPriority: Boolean
+ )
+
+ private class Edge(
+ val nonVisitedDistinctObjectIds: MutableLongList,
+ var isLeafObject: Boolean
+ )
+
private class TraversalState(
- estimatedVisitedObjects: Int,
- computeRetainedHeapSize: Boolean
+ estimatedVisitedObjects: Int
) {
var visitingLast = false
@@ -73,11 +78,14 @@
*/
val toVisitLastQueue: Deque<Node> = ArrayDeque()
- val visitedSet = LongScatterSet(estimatedVisitedObjects)
- val dominatorTree =
- if (computeRetainedHeapSize) DominatorTree(estimatedVisitedObjects) else null
+ val visitedSet = MutableLongSet(estimatedVisitedObjects)
- val tree = ShortestPathObjectNode("root", null, newNode = false).apply {
+ // Not using estimatedVisitedObjects because there could be a lot less nodes than objects.
+ // This is a list because order matters.
+ val dequeuedNodes = mutableListOf<DequeuedNode>()
+ val dominatorTree = DominatorTree(estimatedVisitedObjects)
+
+ val tree = ShortestPathObjectNode("root", null).apply {
selfObjectCount = 1
}
val queuesNotEmpty: Boolean
@@ -89,249 +97,300 @@
graph: CloseableHeapGraph,
previousTraversal: HeapTraversalInput
): HeapTraversalOutput {
-
- // First iteration, all nodes are growing.
- if (previousTraversal is InitialState) {
- tree.growing = true
- }
-
val previousTree = when (previousTraversal) {
is InitialState -> null
is HeapTraversalOutput -> previousTraversal.shortestPathTree
}
+ val firstTraversal = previousTree == null
+
+ val secondTraversal = previousTraversal is FirstHeapTraversal
val objectReferenceReader = referenceReaderFactory.createFor(graph)
- val roots = graph.groupRoots()
- enqueueRoots(previousTree, roots)
-
- val nodesMaybeGrowing = mutableListOf<Node>()
+ enqueueRoots(previousTree, graph)
while (queuesNotEmpty) {
val node = poll()
- if (previousTree != null) {
- if (node.previousPathNode == null) {
- // This is a new node, not seen in the previous iteration. If its parent is growing
- // then we'll consider this one as growing as well.
- nodesMaybeGrowing += node
- } else {
- if (node.previousPathNode.growing) {
- nodesMaybeGrowing += node
- }
- }
- }
-
- class ExpandedObject(
- val valueObjectId: Long,
- val nodeAndEdgeName: String,
- val isLowPriority: Boolean,
- val isLeafObject: Boolean
- )
+ val dequeuedNode = DequeuedNode(node)
+ dequeuedNodes.add(dequeuedNode)
+ val current = dequeuedNode.shortestPathNode
// Note: this is different from visitedSet.size(), which includes gc roots.
- var visitedObjectCount = 0
+ var countOfVisitedObjectForCurrentNode = 0
- val edges = node.objectIds.flatMap { objectId ->
+ val edgesByNodeName = mutableMapOf<EdgeKey, Edge>()
+ // Each object we've found for that node is returning a set of edges.
+ node.objectIds.forEach exploreObjectEdges@{ objectId ->
// This is when we actually visit.
val added = visitedSet.add(objectId)
+
if (!added) {
- emptySequence()
- } else {
- visitedObjectCount++
- if (node.isLeafObject) {
- emptySequence()
+ return@exploreObjectEdges
+ }
+
+ countOfVisitedObjectForCurrentNode++
+
+ if (node.isLeafObject) {
+ return@exploreObjectEdges
+ }
+
+ val heapObject = graph.findObjectById(objectId)
+ val refs = objectReferenceReader.read(heapObject)
+ refs.forEach recordEdge@{ reference ->
+ // dominatorTree is updated prior to enqueueing, because that's where we have the
+ // parent object id information. visitedSet is updated on dequeuing, because bumping
+ // node priority would be complex when as we'd need to move object ids between nodes
+ // rather than just move nodes.
+ dominatorTree.updateDominated(
+ objectId = reference.valueObjectId,
+ parentObjectId = objectId
+ )
+ // note: we only update visitedSet once dequeued. This could lead
+ // to duplicates in queue, but avoids having to bump priority of already
+ // enqueued low priority nodes.
+ if (reference.valueObjectId in visitedSet) {
+ return@recordEdge
+ }
+ val details = reference.lazyDetailsResolver.resolve()
+ val refType = details.locationType.name
+ val owningClassSimpleName =
+ graph.findObjectById(details.locationClassObjectId).asClass!!.simpleName
+ val refName = if (details.locationType == ARRAY_ENTRY) "[x]" else details.name
+ val referencedObjectName =
+ when (val referencedObject = graph.findObjectById(reference.valueObjectId)) {
+ is HeapClass -> "class ${referencedObject.name}"
+ is HeapInstance -> "instance of ${referencedObject.instanceClassName}"
+ is HeapObjectArray -> "array of ${referencedObject.arrayClassName}"
+ is HeapPrimitiveArray -> "array of ${referencedObject.primitiveType.name.lowercase()}"
+ }
+
+ val nodeAndEdgeName =
+ "$refType ${owningClassSimpleName}.${refName} -> $referencedObjectName"
+
+ val edgeKey = EdgeKey(nodeAndEdgeName, reference.isLowPriority)
+
+ val edge = edgesByNodeName[edgeKey]
+ if (edge == null) {
+ edgesByNodeName[edgeKey] = Edge(
+ nonVisitedDistinctObjectIds = mutableLongListOf(reference.valueObjectId),
+ isLeafObject = reference.isLeafObject,
+ )
} else {
- val heapObject = graph.findObjectById(objectId)
- val refs = objectReferenceReader.read(heapObject)
- refs.mapNotNull { reference ->
- // dominatorTree is updated prior to enqueueing, because that's where we have the
- // parent object id information. visitedSet is updated on dequeuing, because bumping
- // node priority would be complex when as we'd need to move object ids between nodes
- // rather than just move nodes.
- dominatorTree?.updateDominated(
- objectId = reference.valueObjectId,
- parentObjectId = objectId
- )
- if (reference.valueObjectId in visitedSet) {
- null
- } else {
- val details = reference.lazyDetailsResolver.resolve()
- val refType = details.locationType.name
- val owningClassSimpleName =
- graph.findObjectById(details.locationClassObjectId).asClass!!.simpleName
- val refName = if (details.locationType == ARRAY_ENTRY) "[x]" else details.name
- val referencedObjectName =
- when (val referencedObject = graph.findObjectById(reference.valueObjectId)) {
- is HeapClass -> "class ${referencedObject.name}"
- is HeapInstance -> "instance of ${referencedObject.instanceClassName}"
- is HeapObjectArray -> "array of ${referencedObject.arrayClassName}"
- is HeapPrimitiveArray -> "array of ${referencedObject.primitiveType.name.lowercase()}"
- }
- val nodeAndEdgeName =
- "$refType ${owningClassSimpleName}.${refName} -> $referencedObjectName"
- ExpandedObject(
- reference.valueObjectId, nodeAndEdgeName, reference.isLowPriority,
- reference.isLeafObject
- )
- }
+ // node is leaf object if all objects in node are leaf objects.
+ edge.isLeafObject = edge.isLeafObject && reference.isLeafObject
+ // Make it distinct
+ if (reference.valueObjectId !in edge.nonVisitedDistinctObjectIds) {
+ edge.nonVisitedDistinctObjectIds += reference.valueObjectId
}
}
}
- }.groupBy {
- it.nodeAndEdgeName + if (it.isLowPriority) "low-priority" else ""
}
- if (visitedObjectCount > 0) {
- val parent = node.shortestPathNode.parent!!
- val current = node.shortestPathNode
- parent._children += current
+ if (countOfVisitedObjectForCurrentNode > 0) {
+ val parent = node.parentPathNode
+ parent.addChild(current)
+ // First traversal, all nodes with children are growing.
+ if (firstTraversal) {
+ parent.growing = true
+ }
if (current.name == parent.name) {
var linkedListStartNode = current
while (linkedListStartNode.name == linkedListStartNode.parent!!.name) {
// Never null, we don't expect to ever see "root" -> "root"
linkedListStartNode = linkedListStartNode.parent!!
}
- linkedListStartNode.selfObjectCount += visitedObjectCount
+ linkedListStartNode.selfObjectCount += countOfVisitedObjectForCurrentNode
} else {
- current.selfObjectCount = visitedObjectCount
- }
- // First iteration, all nodes are growing.
- if (previousTree == null) {
- current.growing = true
+ current.selfObjectCount = countOfVisitedObjectForCurrentNode
}
}
- val previousNodeMap = node.previousPathNode?.let { shortestPathNode ->
- shortestPathNode._children.associateBy { it.name }
+ val previousNodeChildrenMapOrNull = node.previousPathNode?.let { previousPathNode ->
+ previousPathNode.children.associateBy { it.name }
}
- edges.forEach { (_, expandedObjects) ->
- val firstOfGroup = expandedObjects.first()
- val leafObject = expandedObjects.all { it.isLeafObject }
- val nodeAndEdgeName = firstOfGroup.nodeAndEdgeName
- val previousPathNode = if (previousNodeMap != null) {
- previousNodeMap[nodeAndEdgeName]
- } else {
- null
+ val edgesEnqueued = edgesByNodeName.count { (edgeKey, edge) ->
+ val previousPathNodeChildOrNull =
+ previousNodeChildrenMapOrNull?.get(edgeKey.nodeAndEdgeName)
+ val nonVisitedDistinctObjectIdsArray = LongArray(edge.nonVisitedDistinctObjectIds.size)
+ edge.nonVisitedDistinctObjectIds.forEachIndexed { index, objectId ->
+ nonVisitedDistinctObjectIdsArray[index] = objectId
}
+
enqueue(
- parentPathNode = node.shortestPathNode,
- previousPathNode = previousPathNode,
- objectIds = expandedObjects.map { it.valueObjectId },
- nodeAndEdgeName = nodeAndEdgeName,
- isLowPriority = firstOfGroup.isLowPriority,
- isLeafObject = leafObject
+ parentPathNode = current,
+ previousPathNode = previousPathNodeChildOrNull,
+ objectIds = nonVisitedDistinctObjectIdsArray,
+ nodeAndEdgeName = edgeKey.nodeAndEdgeName,
+ isLowPriority = edgeKey.isLowPriority,
+ isLeafObject = edge.isLeafObject
)
+ return@count true
+ }
+
+ if (edgesEnqueued > 0) {
+ current.createChildrenBackingList(edgesEnqueued)
}
}
- val growingNodes = if (previousTree != null) {
- val growingNodePairs = mutableListOf<Pair<Node, ShortestPathObjectNode>>()
- val growingNodes = nodesMaybeGrowing.mapNotNull { node ->
- val shortestPathNode = node.shortestPathNode
- val growing = if (node.previousPathNode != null) {
- // Existing node. Growing if was growing (already true) and edges increased at least detectedGrowth.
- // Why detectedGrowth? We perform N scenarios and only take N/detectedGrowth heap dumps, which avoids including
- // any side effects of heap dumps in our leak detection.
- shortestPathNode.childrenObjectCount >= node.previousPathNode.childrenObjectCount + previousTraversal.scenarioLoopsPerGraph
- } else {
- val parent = shortestPathNode.parent!!
- // New node. Growing if parent is growing.
- // New node always have a parent.
- // check for more than 0 because linked list structures will bubble their count up
- // and don't need to be marked as growing.
- parent.growing && shortestPathNode.selfObjectCount > 0
- }
- if (growing) {
- if (node.previousPathNode != null) {
- val previousChildrenByName =
- node.previousPathNode._children.associateBy { it.name }
- shortestPathNode._children.forEach { child ->
- val previousChild = previousChildrenByName[child.name]
- if (previousChild != null) {
- child.selfObjectCountIncrease =
- child.selfObjectCount - previousChild.selfObjectCount
- } else {
- child.selfObjectCountIncrease = child.selfObjectCount
- }
- }
- } else {
- shortestPathNode._children.forEach { child ->
- child.selfObjectCountIncrease = child.selfObjectCount
- }
- }
- // Mark as growing in the tree (useful for next iteration)
- shortestPathNode.growing = true
+ return if (previousTraversal is InitialState) {
+ // Iterating on last dequeued first means we'll get dominated first and progressively go
+ // up the dominator tree.
+ val objectSizeCalculator = AndroidObjectSizeCalculator(graph)
+ // A map that stores two ints, size and count, in a single long value with bit packing.
+ val retainedSizeAndCountMap = MutableLongLongMap(dequeuedNodes.size)
+ for (node in dequeuedNodes.asReversed()) {
+ var nodeRetainedSize = ZERO_BYTES
+ var nodeRetainedCount = 0
- val previouslyGrowing = !shortestPathNode.newNode
- val parentAlreadyReported = (shortestPathNode.parent?.growing) ?: false
+ for (objectId in node.objectIds) {
+ val objectShallowSize = objectSizeCalculator.computeSize(objectId)
- val repeatedlyGrowingNode = previouslyGrowing && !parentAlreadyReported
- // Return in list of growing nodes.
- if (repeatedlyGrowingNode) {
- if (dominatorTree != null) {
- growingNodePairs += node to shortestPathNode
- }
- shortestPathNode
- } else {
- null
- }
- } else {
- null
- }
- }
- dominatorTree?.let { dominatorTree ->
- val growingNodeObjectIds = growingNodePairs.flatMapTo(LinkedHashSet()) { (node, _) ->
- node.objectIds
- }
- val objectSizeCalculator = AndroidObjectSizeCalculator(graph)
- val retainedMap =
- dominatorTree.computeRetainedSizes(growingNodeObjectIds, objectSizeCalculator)
- growingNodePairs.forEach { (node, shortestPathNode) ->
- var heapSize = ByteSize.ZERO
- var objectCount = 0
- for (objectId in node.objectIds) {
- val (additionalByteSize, additionalObjectCount) = retainedMap.getValue(objectId)
- heapSize += additionalByteSize.bytes
- objectCount += additionalObjectCount
- }
- shortestPathNode.retainedOrNull = Retained(
- heapSize = heapSize,
- objectCount = objectCount
+ val packedSizeAndCount = retainedSizeAndCountMap.increase(
+ objectId, objectShallowSize, 1
)
- val previousRetained = node.previousPathNode?.retainedOrNull
- shortestPathNode.retainedIncreaseOrNull = if (previousRetained == null) {
- Retained(ByteSize.ZERO, 0)
- } else {
- Retained(
- heapSize - previousRetained.heapSize, objectCount - previousRetained.objectCount
- )
+
+ val retainedSize = packedSizeAndCount.unpackAsFirstInt
+ val retainedCount = packedSizeAndCount.unpackAsSecondInt
+
+ val dominatorObjectId = dominatorTree[objectId]
+ if (dominatorObjectId != ValueHolder.NULL_REFERENCE) {
+ retainedSizeAndCountMap.increase(dominatorObjectId, retainedSize, retainedCount)
}
+ nodeRetainedSize += retainedSize.bytes
+ nodeRetainedCount += retainedCount
+ }
+
+ if (node.shortestPathNode.growing) {
+ node.shortestPathNode.retained = Retained(
+ heapSize = nodeRetainedSize,
+ objectCount = nodeRetainedCount
+ )
+ // First traversal, can't compute an increase, nothing to diff on.
+ node.shortestPathNode.retainedIncrease = ZERO_RETAINED
}
}
- growingNodes
- } else {
- null
- }
-
- return if (growingNodes == null) {
- check(previousTraversal is InitialState)
FirstHeapTraversal(tree, previousTraversal)
} else {
- check(previousTraversal !is InitialState)
- HeapGrowthTraversal(previousTraversal.traversalCount + 1, tree, growingNodes, previousTraversal)
+ val reportedGrowingNodeObjectIdsForRetainedSize = MutableLongSet()
+ // Marks node as "growing" if we can find a corresponding previous node that was growing and
+ // we see at least one child node that increased its number of objects over our threshold.
+ val reportedGrowingNodes = dequeuedNodes.mapNotNull reportedGrowingNodeOrNull@{ node ->
+ val previousPathNode = node.previousPathNode
+ // if node wasn't previously growing, skip it.
+ if (previousPathNode == null || !previousPathNode.growing) {
+ return@reportedGrowingNodeOrNull null
+ }
+
+ val shortestPathNode = node.shortestPathNode
+
+ // Existing node. Growing if was growing (already true) and edges increased at least
+ // detectedGrowth for at least one children which was already growing.
+ // Why detectedGrowth? We perform N scenarios and only take N/detectedGrowth heap dumps
+ // which avoids including any side effects of heap dumps in our leak detection.
+ val previouslyGrowingChildren = if (secondTraversal) {
+ previousPathNode.children.asSequence()
+ } else {
+ previousPathNode.growingChildrenArray?.asSequence()
+ }
+
+ // Node had no previously growing children, skip.
+ if (previouslyGrowingChildren == null) {
+ return@reportedGrowingNodeOrNull null
+ }
+
+ val previousGrowingChildrenByName =
+ previouslyGrowingChildren.associateBy { it.name }
+
+ // Set size to max possible
+ val growingChildren = ArrayList<ShortestPathObjectNode>(shortestPathNode.children.size)
+ val growingChildrenIncreases = IntArray(shortestPathNode.children.size)
+ shortestPathNode.children.forEach growingChildren@{ child ->
+ val previousChild = previousGrowingChildrenByName[child.name]
+ ?: return@growingChildren
+ val childrenIncrease = child.selfObjectCount - previousChild.selfObjectCount
+
+ if (childrenIncrease < previousTraversal.scenarioLoopsPerGraph) {
+ // Child stopped growing
+ return@growingChildren
+ }
+
+ growingChildrenIncreases[growingChildren.size] = childrenIncrease
+ growingChildren += child
+ }
+
+ // No child grew beyond threshold, skip.
+ if (growingChildren.isEmpty()) {
+ return@reportedGrowingNodeOrNull null
+ }
+
+ shortestPathNode.growingChildrenArray = growingChildren.toTypedArray()
+ shortestPathNode.growingChildrenIncreasesArray =
+ growingChildrenIncreases.copyOf(growingChildren.size)
+
+ // Mark as growing in the tree (useful for next iteration): if we conditioned setting this
+ // to "parentGrowing", then adding an identical subgraph to an array would otherwise lead
+ // to each distinct paths from roots to a node of a subgraph to be surfaced as a distinct
+ // path.
+ // We're traversing from parents to child. If we didn't mark here, then a path of
+ // 3 growing nodes A->B->C would see B not marked as growing (because it's parent is)
+ // and then C would end up being reported as really growing.
+ shortestPathNode.growing = true
+
+ val parentGrowing = (shortestPathNode.parent?.growing) ?: false
+
+ // Parent already growing, there's no need to report its child node as a growing node.
+ if (parentGrowing) {
+ return@reportedGrowingNodeOrNull null
+ }
+
+ node.objectIds.forEach { objectId ->
+ reportedGrowingNodeObjectIdsForRetainedSize.add(objectId)
+ }
+ return@reportedGrowingNodeOrNull shortestPathNode
+ }
+ val objectSizeCalculator = AndroidObjectSizeCalculator(graph)
+ val retainedMap = dominatorTree.computeRetainedSizes(
+ reportedGrowingNodeObjectIdsForRetainedSize, objectSizeCalculator
+ )
+ dequeuedNodes.forEach reportedGrowingNodeRetainedSize@{ node ->
+ val shortestPathNode = node.shortestPathNode
+ // If not growing, or growing but with a parent that's growing, skip.
+ if (!shortestPathNode.growing ||
+ (shortestPathNode.parent != null && shortestPathNode.parent.growing)
+ ) {
+ return@reportedGrowingNodeRetainedSize
+ }
+
+ var heapSize = ZERO_BYTES
+ var objectCount = 0
+ for (objectId in node.objectIds) {
+ val packed = retainedMap[objectId]
+ val additionalByteSize = packed.unpackAsFirstInt
+ val additionalObjectCount = packed.unpackAsSecondInt
+ heapSize += additionalByteSize.bytes
+ objectCount += additionalObjectCount
+ }
+ shortestPathNode.retained = Retained(
+ heapSize = heapSize,
+ objectCount = objectCount
+ )
+ val previousRetained = node.previousPathNode?.retained ?: UNKNOWN_RETAINED
+ shortestPathNode.retainedIncrease = if (previousRetained.isUnknown) {
+ ZERO_RETAINED
+ } else {
+ Retained(
+ heapSize - previousRetained.heapSize, objectCount - previousRetained.objectCount
+ )
+ }
+ }
+ HeapGrowthTraversal(
+ previousTraversal.traversalCount + 1, tree, reportedGrowingNodes, previousTraversal
+ )
}
}
- private fun HeapGraph.groupRoots() =
- gcRootProvider.provideGcRoots(this).map { gcRootReference ->
- val name = "GcRoot(${gcRootReference.gcRoot::class.java.simpleName})"
- name to gcRootReference
- }
- // sort preserved
- .groupBy { it.first + if (it.second.isLowPriority) "low-priority" else "" }
-
private fun TraversalState.poll(): Node {
return if (!visitingLast && !toVisitQueue.isEmpty()) {
toVisitQueue.poll()
@@ -343,68 +402,68 @@
private fun TraversalState.enqueueRoots(
previousTree: ShortestPathObjectNode?,
- roots: Map<String, List<Pair<String, GcRootReference>>>
+ heapGraph: CloseableHeapGraph
) {
val previousTreeRootMap = previousTree?.let { tree ->
- tree._children.associateBy { it.name }
+ tree.children.associateBy { it.name }
}
- roots.forEach { (_, gcRootReferences) ->
- val firstOfGroup = gcRootReferences.first()
- val nodeAndEdgeName = firstOfGroup.first
- val previousPathNode = if (previousTreeRootMap != null) {
- previousTreeRootMap[nodeAndEdgeName]
- } else {
- null
- }
- val objectIds = gcRootReferences.map { it.second.gcRoot.id }
- dominatorTree?.let {
- objectIds.forEach { objectId ->
- it.updateDominatedAsRoot(objectId)
- }
+ val edgesByNodeName = mutableMapOf<EdgeKey, MutableLongList>()
+ gcRootProvider.provideGcRoots(heapGraph).forEach { gcRootReference ->
+ val objectId = gcRootReference.gcRoot.id
+ if (objectId == ValueHolder.NULL_REFERENCE) {
+ return@forEach
}
+ val name = "GcRoot(${gcRootReference.gcRoot::class.java.simpleName})"
+ val edgeKey = EdgeKey(name, gcRootReference.isLowPriority)
+
+ val edgeObjectIds = edgesByNodeName[edgeKey]
+ if (edgeObjectIds == null) {
+ edgesByNodeName[edgeKey] = mutableLongListOf(objectId)
+ } else {
+ if (objectId !in edgeObjectIds) {
+ edgeObjectIds += objectId
+ }
+ }
+ }
+ val enqueuedCount = edgesByNodeName.count { (edgeKey, edgeObjectIds) ->
+ val previousPathNode = previousTreeRootMap?.get(edgeKey.nodeAndEdgeName)
+
+ edgeObjectIds.forEach { objectId ->
+ dominatorTree.updateDominatedAsRoot(objectId)
+ }
+
+ val edgeObjectIdsArray = LongArray(edgeObjectIds.size)
+
+ edgeObjectIds.forEachIndexed { index, objectId ->
+ edgeObjectIdsArray[index] = objectId
+ }
enqueue(
parentPathNode = tree,
previousPathNode = previousPathNode,
- objectIds = objectIds,
- nodeAndEdgeName = nodeAndEdgeName,
- isLowPriority = firstOfGroup.second.isLowPriority,
+ objectIds = edgeObjectIdsArray,
+ nodeAndEdgeName = edgeKey.nodeAndEdgeName,
+ isLowPriority = edgeKey.isLowPriority,
isLeafObject = false
)
+ return@count true
}
+ tree.createChildrenBackingList(enqueuedCount)
}
private fun TraversalState.enqueue(
parentPathNode: ShortestPathObjectNode,
previousPathNode: ShortestPathObjectNode?,
- objectIds: List<Long>,
+ objectIds: LongArray,
nodeAndEdgeName: String,
isLowPriority: Boolean,
isLeafObject: Boolean
) {
- // TODO Maybe the filtering should happen at the callsite.
- // TODO we already filter visited on the traversal side. maybe crash?
- val filteredObjectIds = objectIds.filter { objectId ->
- objectId != ValueHolder.NULL_REFERENCE &&
- // note: we only update visitedSet once dequeued. This could lead
- // to duplicates in queue, but avoids having to bump priority of already
- // enqueued low priority nodes.
- objectId !in visitedSet
- }
- // Deduplicate object ids
- .toSet()
-
- if (filteredObjectIds.isEmpty()) {
- return
- }
-
- val shortestPathNode =
- ShortestPathObjectNode(nodeAndEdgeName, parentPathNode, newNode = previousPathNode == null)
-
val node = Node(
- objectIds = filteredObjectIds,
- shortestPathNode = shortestPathNode,
+ objectIds = objectIds,
+ parentPathNode = parentPathNode,
+ nodeAndEdgeName = nodeAndEdgeName,
previousPathNode = previousPathNode,
isLeafObject = isLeafObject
)
@@ -416,14 +475,46 @@
}
}
- private data class Node(
+ private fun MutableLongLongMap.increase(
+ objectId: Long,
+ addedValue1: Int,
+ addedValue2: Int,
+ ): Long {
+ val missing = ValueHolder.NULL_REFERENCE
+ val packedValue = getOrDefault(objectId, ValueHolder.NULL_REFERENCE)
+ return if (packedValue == missing) {
+ val newPackedValue = ((addedValue1.toLong()) shl 32) or (addedValue2.toLong() and 0xffffffffL)
+ put(objectId, newPackedValue)
+ newPackedValue
+ } else {
+ val existingValue1 = (packedValue shr 32).toInt()
+ val existingValue2 = (packedValue and 0xFFFFFFFF).toInt()
+ val newValue1 = existingValue1 + addedValue1
+ val newValue2 = existingValue2 + addedValue2
+ val newPackedValue = ((newValue1.toLong()) shl 32) or (newValue2.toLong() and 0xffffffffL)
+ put(objectId, newPackedValue)
+ newPackedValue
+ }
+ }
+
+ private class Node(
// All objects that you can reach through paths that all resolves to the same structure.
- val objectIds: Set<Long>,
- val shortestPathNode: ShortestPathObjectNode,
+ val objectIds: LongArray,
+ val parentPathNode: ShortestPathObjectNode,
+ val nodeAndEdgeName: String,
val previousPathNode: ShortestPathObjectNode?,
val isLeafObject: Boolean,
)
+ private class DequeuedNode(
+ node: Node
+ ) {
+ // All objects that you can reach through paths that all resolves to the same structure.
+ val objectIds = node.objectIds
+ val shortestPathNode = ShortestPathObjectNode(node.nodeAndEdgeName, node.parentPathNode)
+ val previousPathNode = node.previousPathNode
+ }
+
/**
* This allows external modules to add factory methods for configured instances of this class as
* extension functions of this companion object.
diff --git a/shark/shark/src/main/java/shark/RealLeakTracerFactory.kt b/shark/shark/src/main/java/shark/RealLeakTracerFactory.kt
index 42e47db..f0eb49d 100644
--- a/shark/shark/src/main/java/shark/RealLeakTracerFactory.kt
+++ b/shark/shark/src/main/java/shark/RealLeakTracerFactory.kt
@@ -15,6 +15,8 @@
*/
package shark
+import androidx.collection.LongLongMap
+import androidx.collection.MutableLongSet
import shark.HeapObject.HeapClass
import shark.HeapObject.HeapInstance
import shark.HeapObject.HeapObjectArray
@@ -27,20 +29,20 @@
import shark.LeakTraceObject.ObjectType.ARRAY
import shark.LeakTraceObject.ObjectType.CLASS
import shark.LeakTraceObject.ObjectType.INSTANCE
-import shark.internal.ReferencePathNode
-import shark.internal.ReferencePathNode.ChildNode
-import shark.internal.ReferencePathNode.RootNode
-import shark.internal.ShallowSizeCalculator
-import shark.internal.createSHA1Hash
-import shark.internal.lastSegment
-import java.util.ArrayList
import shark.RealLeakTracerFactory.Event.StartedBuildingLeakTraces
import shark.RealLeakTracerFactory.Event.StartedComputingJavaHeapRetainedSize
-import shark.RealLeakTracerFactory.Event.StartedComputingNativeRetainedSize
import shark.RealLeakTracerFactory.Event.StartedInspectingObjects
import shark.RealLeakTracerFactory.TrieNode.LeafNode
import shark.RealLeakTracerFactory.TrieNode.ParentNode
+import shark.internal.ReferencePathNode
+import shark.internal.ReferencePathNode.ChildNode
+import shark.internal.ReferencePathNode.RootNode
import shark.internal.ReferencePathNode.RootNode.LibraryLeakRootNode
+import shark.internal.createSHA1Hash
+import shark.internal.lastSegment
+import shark.internal.packedWith
+import shark.internal.unpackAsFirstInt
+import shark.internal.unpackAsSecondInt
// TODO kdoc
// TODO better name than "real"
@@ -50,16 +52,17 @@
private val shortestPathFinderFactory: ShortestPathFinder.Factory,
private val objectInspectors: List<ObjectInspector>,
private val listener: Event.Listener
-): LeakTracer.Factory {
+) : LeakTracer.Factory {
// TODO Enum or sealed? class makes it possible to report progress. Enum
// provides ordering of events.
sealed interface Event {
object StartedBuildingLeakTraces : Event
object StartedInspectingObjects : Event
+
@Deprecated("Event not sent anymore")
- object StartedComputingNativeRetainedSize: Event
- object StartedComputingJavaHeapRetainedSize: Event
+ object StartedComputingNativeRetainedSize : Event
+ object StartedComputingJavaHeapRetainedSize : Event
fun interface Listener {
fun onEvent(event: Event)
@@ -73,7 +76,7 @@
// traversed the whole graph.
// referenceMatchers are only needed for the NativeGlobalVariablePattern, which is related
// to GC roots
- return LeakTracer { objectIds->
+ return LeakTracer { objectIds ->
val helpers = FindLeakInput(
heapGraph,
shortestPathFinderFactory.createFor(heapGraph),
@@ -83,7 +86,6 @@
}
}
-
private class FindLeakInput(
val graph: HeapGraph,
val shortestPathFinder: ShortestPathFinder,
@@ -236,6 +238,7 @@
is ParentNode -> {
findResultsInTrie(childNode, outputPathResults)
}
+
is LeafNode -> {
outputPathResults += childNode.pathNode
}
@@ -270,7 +273,7 @@
private fun FindLeakInput.buildLeakTraces(
shortestPaths: List<ShortestPath>,
inspectedObjectsByPath: List<List<InspectedObject>>,
- retainedSizes: Map<Long, Pair<Int, Int>>?
+ retainedSizes: LongLongMap?
): Pair<List<ApplicationLeak>, List<LibraryLeak>> {
listener.onEvent(StartedBuildingLeakTraces)
@@ -345,20 +348,26 @@
private fun FindLeakInput.computeRetainedSizes(
inspectedObjectsByPath: List<List<InspectedObject>>,
dominatorTree: DominatorTree
- ): Map<Long, Pair<Int, Int>> {
+ ): LongLongMap {
val nodeObjectIds = inspectedObjectsByPath.flatMap { inspectedObjects ->
// TODO Stop at the first leaking object
inspectedObjects.filter { it.leakingStatus == UNKNOWN || it.leakingStatus == LEAKING }
.map { it.heapObject.objectId }
- }.toSet()
+ }
+
+ val nodeObjectIdsSet = MutableLongSet(nodeObjectIds.size)
+ nodeObjectIds.forEach {
+ nodeObjectIdsSet += it
+ }
+
listener.onEvent(StartedComputingJavaHeapRetainedSize)
val objectSizeCalculator = AndroidObjectSizeCalculator(graph)
- return dominatorTree.computeRetainedSizes(nodeObjectIds, objectSizeCalculator)
+ return dominatorTree.computeRetainedSizes(nodeObjectIdsSet, objectSizeCalculator)
}
private fun buildLeakTraceObjects(
inspectedObjects: List<InspectedObject>,
- retainedSizes: Map<Long, Pair<Int, Int>>?
+ retainedSizes: LongLongMap?
): List<LeakTraceObject> {
return inspectedObjects.map { inspectedObject ->
val heapObject = inspectedObject.heapObject
@@ -370,7 +379,18 @@
else -> INSTANCE
}
- val retainedSizeAndObjectCount = retainedSizes?.get(inspectedObject.heapObject.objectId)
+ var retainedHeapByteSize: Int? = null
+ var retainedObjectCount: Int? = null
+
+ if (retainedSizes != null) {
+ val missing = -1 packedWith -1
+ val retainedSizeAndObjectCount =
+ retainedSizes.getOrDefault(inspectedObject.heapObject.objectId, missing)
+ if (retainedSizeAndObjectCount != missing) {
+ retainedHeapByteSize = retainedSizeAndObjectCount.unpackAsFirstInt
+ retainedObjectCount = retainedSizeAndObjectCount.unpackAsSecondInt
+ }
+ }
LeakTraceObject(
type = objectType,
@@ -378,8 +398,8 @@
labels = inspectedObject.labels,
leakingStatus = inspectedObject.leakingStatus,
leakingStatusReason = inspectedObject.leakingStatusReason,
- retainedHeapByteSize = retainedSizeAndObjectCount?.first,
- retainedObjectCount = retainedSizeAndObjectCount?.second
+ retainedHeapByteSize = retainedHeapByteSize,
+ retainedObjectCount = retainedObjectCount
)
}
}
diff --git a/shark/shark/src/main/java/shark/RepeatingScenarioObjectGrowthDetector.kt b/shark/shark/src/main/java/shark/RepeatingScenarioObjectGrowthDetector.kt
index 7ebcdbc..d98e735 100644
--- a/shark/shark/src/main/java/shark/RepeatingScenarioObjectGrowthDetector.kt
+++ b/shark/shark/src/main/java/shark/RepeatingScenarioObjectGrowthDetector.kt
@@ -9,7 +9,7 @@
*/
private val heapGraphProvider: HeapGraphProvider,
objectGrowthDetector: ObjectGrowthDetector,
- maxHeapDumps: Int = DEFAULT_MAX_HEAP_DUMPS,
+ private val maxHeapDumps: Int = DEFAULT_MAX_HEAP_DUMPS,
scenarioLoopsPerDump: Int = DEFAULT_SCENARIO_LOOPS_PER_DUMP,
) {
@@ -17,7 +17,6 @@
private val initialState = InitialState(
scenarioLoopsPerGraph = scenarioLoopsPerDump,
- heapGraphCount = maxHeapDumps
)
/**
@@ -37,7 +36,7 @@
private fun dumpHeapOnNext(
repeatedScenario: () -> Unit,
): Sequence<CloseableHeapGraph> {
- val heapDumps = (1..initialState.heapGraphCount!!).asSequence().map {
+ val heapDumps = (1..maxHeapDumps).asSequence().map {
repeat(initialState.scenarioLoopsPerGraph) {
repeatedScenario()
}
diff --git a/shark/shark/src/main/java/shark/Retained.kt b/shark/shark/src/main/java/shark/Retained.kt
new file mode 100644
index 0000000..3dc12e4
--- /dev/null
+++ b/shark/shark/src/main/java/shark/Retained.kt
@@ -0,0 +1,45 @@
+package shark
+
+import shark.internal.packedWith
+import shark.internal.unpackAsFirstInt
+import shark.internal.unpackAsSecondInt
+
+/**
+ * Constructors can't be inlined so we used a function instead.
+ */
+inline fun Retained(
+ /**
+ * The minimum number of bytes which would be freed if all references to this object were
+ * released. Should not exceed [Int.MAX_VALUE] bytes.
+ */
+ heapSize: ByteSize,
+
+ /**
+ * The minimum number of objects which would be unreachable if all references to this object were
+ * released.
+ */
+ objectCount: Int,
+) = Retained(heapSize.inWholeBytes.toInt() packedWith objectCount)
+
+// DO NOT ADD A COMPANION OBJECT: a value class is supposed to be lightweight and its usage inlined
+// into few instructions. After adding a companion object, call sites get a lot more instructions.
+@JvmInline
+value class Retained @PublishedApi internal constructor(
+ @PublishedApi @JvmField
+ internal val packedValue: Long
+) {
+ inline val heapSize: ByteSize
+ get() = packedValue.unpackAsFirstInt.bytes
+
+ inline val objectCount: Int
+ get() = packedValue.unpackAsSecondInt
+
+ inline val isUnknown: Boolean
+ get() = this == UNKNOWN_RETAINED
+
+ inline val isZero: Boolean
+ get() = this == ZERO_RETAINED
+}
+
+val ZERO_RETAINED = Retained(ZERO_BYTES, 0)
+val UNKNOWN_RETAINED = Retained((-1).bytes, -1)
diff --git a/shark/shark/src/main/java/shark/ShortestPathObjectNode.kt b/shark/shark/src/main/java/shark/ShortestPathObjectNode.kt
index 3424354..0b38af2 100644
--- a/shark/shark/src/main/java/shark/ShortestPathObjectNode.kt
+++ b/shark/shark/src/main/java/shark/ShortestPathObjectNode.kt
@@ -1,67 +1,89 @@
package shark
-import shark.ByteSize.Companion.bytes
-
typealias GrowingObjectNodes = List<ShortestPathObjectNode>
class ShortestPathObjectNode(
val name: String,
val parent: ShortestPathObjectNode?,
- internal val newNode: Boolean
) {
- @Suppress("VariableNaming")
- internal val _children = mutableListOf<ShortestPathObjectNode>()
+ // Null at first, then created with capacity set to the number of edges enqueued from that node.
+ // This means we'll sometimes use a little more space than what we actually need, but the
+ // trade-off is that we only get to create the array once, and there's no array size doubling.
+ private var _children: MutableList<ShortestPathObjectNode>? = null
+
+ // Null on initial run (all children are growing). After the first run, set with only
+ // children that are constantly growing over the per children threshold.
+ internal var growingChildrenArray: Array<ShortestPathObjectNode>? = null
+ internal var growingChildrenIncreasesArray: IntArray? = null
+
val children: List<ShortestPathObjectNode>
- get() = _children
+ get() = _children ?: emptyList()
+
+ data class GrowingChildNode(
+ val child: ShortestPathObjectNode,
+ val objectCountIncrease: Int
+ )
+
+ /**
+ * Returns a list of pair of child [ShortestPathObjectNode] and associated object count
+ * increase, filtered to only the children nodes that were marked as growing, i.e. children
+ * that had an object count increase greater or equal to the scenario loop count.
+ */
+ val growingChildren: List<GrowingChildNode>
+ get() = growingChildrenArray!!.withIndex()
+ .map { indexedValue ->
+ GrowingChildNode(indexedValue.value, growingChildrenIncreasesArray!![indexedValue.index])
+ }
var selfObjectCount = 0
internal set
- var selfObjectCountIncrease = 0
- internal set
/**
* Set for growing nodes if the traversal requested the computation of retained sizes, otherwise
* null.
- * This is on the last 2 traversals.
*/
- var retainedOrNull: Retained? = null
+ var retained: Retained = UNKNOWN_RETAINED
internal set
/**
* Set for growing nodes if [retainedOrNull] is not null. Non 0 if the previous traversal also
* computed retained size.
- * This is on the last 2 traversals.
*/
- var retainedIncreaseOrNull: Retained? = null
+ var retainedIncrease: Retained = UNKNOWN_RETAINED
internal set
- val retained: Retained get() = retainedOrNull!!
-
- val retainedIncrease: Retained get() = retainedIncreaseOrNull!!
-
internal var growing = false
- val childrenObjectCount: Int
- get() = _children.sumOf { it.selfObjectCount }
+ internal fun createChildrenBackingList(maxChildren: Int) {
+ check(_children == null) {
+ "Expected createChildList() to be called at most once per node."
+ }
+ _children = ArrayList(maxChildren)
+ }
- val childrenObjectCountIncrease: Int
- get() = _children.sumOf { it.selfObjectCountIncrease }
+ internal fun addChild(child: ShortestPathObjectNode) {
+ val children = checkNotNull(_children) {
+ "Excepted createChildList() to have been called"
+ }
+ children.add(child)
+ }
fun copyResettingAsInitialTree(): ShortestPathObjectNode {
return copyResetRecursive(null)
}
private fun copyResetRecursive(newParent: ShortestPathObjectNode?): ShortestPathObjectNode {
- val newNode = ShortestPathObjectNode(name, newParent, true)
+ val newNode = ShortestPathObjectNode(name, newParent)
newNode.selfObjectCount = selfObjectCount
- newNode.selfObjectCountIncrease = 0
- newNode.retainedOrNull = retainedOrNull
- if (retainedOrNull != null) {
- newNode.retainedIncreaseOrNull = Retained(0L.bytes, 0)
+ newNode.retained = retained
+ if (!retained.isUnknown) {
+ newNode.retainedIncrease = ZERO_RETAINED
}
newNode.growing = true
- _children.forEach { child ->
- newNode._children += child.copyResetRecursive(newNode)
+ newNode.createChildrenBackingList(children.size)
+ val newChildren = newNode._children!!
+ children.forEach { child ->
+ newChildren += child.copyResetRecursive(newNode)
}
return newNode
}
@@ -91,7 +113,7 @@
result.append(pathNode.selfObjectCount)
result.append(" objects)")
if (index == pathAfterRoot.lastIndex) {
- if (retainedOrNull != null) {
+ if (!retained.isUnknown) {
result.appendLine()
result.append(" Retained size: ${retained.heapSize} (+ ${retainedIncrease.heapSize})")
result.appendLine()
@@ -102,17 +124,16 @@
result.appendLine()
result.append(" Children:")
result.appendLine()
- val childrenByMostIncreasedFirst =
- pathNode.children
- // TODO Ideally here we'd filter on the increase threshold (e.g. 5 instead of 0)
- .filter { it.selfObjectCountIncrease > 0 }
- .sortedBy { -it.selfObjectCountIncrease }
+
+ val childrenByMostIncreasedFirst = growingChildren
+ .sortedBy { -it.objectCountIncrease }
+
result.append(
childrenByMostIncreasedFirst.joinToString(
separator = "\n",
postfix = "\n"
- ) { child ->
- " ${child.selfObjectCount} objects (${child.selfObjectCountIncrease} new): ${child.name}"
+ ) { (child, increase) ->
+ " ${child.selfObjectCount} objects (${increase} new): ${child.name}"
})
} else {
result.appendLine()
@@ -121,18 +142,4 @@
}
return result.toString()
}
-
- class Retained(
- /**
- * The minimum number of bytes which would be freed if all references to this object were
- * released.
- */
- val heapSize: ByteSize,
-
- /**
- * The minimum number of objects which would be unreachable if all references to this object were
- * released.
- */
- val objectCount: Int,
- )
}
diff --git a/shark/shark/src/main/java/shark/internal/IntIntPairUtils.kt b/shark/shark/src/main/java/shark/internal/IntIntPairUtils.kt
new file mode 100644
index 0000000..d802d41
--- /dev/null
+++ b/shark/shark/src/main/java/shark/internal/IntIntPairUtils.kt
@@ -0,0 +1,13 @@
+package shark.internal
+
+@PublishedApi
+internal inline infix fun Int.packedWith(that: Int) =
+ ((toLong()) shl 32) or (that.toLong() and 0xffffffffL)
+
+@PublishedApi
+internal inline val Long.unpackAsFirstInt: Int
+ get() = (this shr 32).toInt()
+
+@PublishedApi
+internal inline val Long.unpackAsSecondInt: Int
+ get() = (this and 0xFFFFFFFF).toInt()
diff --git a/shark/shark/src/test/java/shark/LiveObjectGrowthDetectorTest.kt b/shark/shark/src/test/java/shark/LiveObjectGrowthDetectorTest.kt
deleted file mode 100644
index cfae958..0000000
--- a/shark/shark/src/test/java/shark/LiveObjectGrowthDetectorTest.kt
+++ /dev/null
@@ -1,209 +0,0 @@
-package shark
-
-import java.io.File
-import org.assertj.core.api.Assertions.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.rules.TemporaryFolder
-import shark.HprofHeapGraph.Companion.openHeapGraph
-
-class LiveObjectGrowthDetectorTest {
-
- class Leaky
-
- class MultiLeaky {
- val leaky = Leaky() to Leaky()
- }
-
- class CustomLinkedList(var next: CustomLinkedList? = null)
-
- @get:Rule
- val testFolder = TemporaryFolder()
-
- val leakies = mutableListOf<Leaky>()
-
- val stringLeaks = mutableListOf<String>()
-
- var customLeakyLinkedList = CustomLinkedList()
-
- val leakyHashMap = HashMap<String, Leaky>()
-
- val multiLeakies = mutableListOf<MultiLeaky>()
-
- @Test
- fun `empty scenario leads to no heap growth`() {
- val detector = simpleDetector().fromScenario()
-
- val emptyScenario = {}
-
- val growingNodes = detector.findRepeatedlyGrowingObjects(roundTripScenario = emptyScenario)
- .growingObjects
-
- assertThat(growingNodes).isEmpty()
- }
-
- @Test
- fun `leaky increase leads to heap growth`() {
- val detector = simpleDetector().fromScenario()
-
- val growingNodes = detector.findRepeatedlyGrowingObjects {
- leakies += Leaky()
- }.growingObjects
-
- assertThat(growingNodes).isNotEmpty
- }
-
- @Test
- fun `string leak increase leads to heap growth`() {
- val detector = simpleDetector().fromScenario()
-
- var index = 0
- val growingNodes = detector.findRepeatedlyGrowingObjects {
- stringLeaks += "Yo ${++index}"
- }.growingObjects
-
- assertThat(growingNodes).isNotEmpty
- }
-
- @Test
- fun `leak increase that ends leads to no heap growth`() {
- val maxHeapDumps = 10
- val stopLeakingIndex = maxHeapDumps / 2
- val detector = simpleDetector().fromScenario(maxHeapDumps = maxHeapDumps)
-
- var index = 0
- val growingNodes = detector.findRepeatedlyGrowingObjects {
- if (++index < stopLeakingIndex) {
- leakies += Leaky()
- }
- }.growingObjects
-
- assertThat(growingNodes).isEmpty()
- }
-
- @Test
- fun `multiple leaky scenarios per dump leads to heap growth`() {
- val scenarioLoopsPerDump = 5
- val detector =
- simpleDetector().fromScenario(scenarioLoopsPerDump = scenarioLoopsPerDump)
-
- val growingNodes = detector.findRepeatedlyGrowingObjects {
- leakies += Leaky()
- }.growingObjects
-
- assertThat(growingNodes).hasSize(1)
-
- val growingNode = growingNodes.first()
- assertThat(growingNode.childrenObjectCountIncrease).isEqualTo(scenarioLoopsPerDump)
- }
-
- @Test
- fun `custom leaky linked list leads to heap growth`() {
- val detector = simpleDetector().fromScenario()
-
- val growingNodes = detector.findRepeatedlyGrowingObjects {
- customLeakyLinkedList = CustomLinkedList(customLeakyLinkedList)
- }.growingObjects
-
- assertThat(growingNodes).isNotEmpty
- }
-
- @Test
- fun `custom leaky linked list reports descendant to root as flattened collection`() {
- val detector = simpleDetector().fromScenario()
-
- val growingNodes = detector.findRepeatedlyGrowingObjects {
- customLeakyLinkedList = CustomLinkedList(customLeakyLinkedList)
- customLeakyLinkedList = CustomLinkedList(customLeakyLinkedList)
- customLeakyLinkedList = CustomLinkedList(customLeakyLinkedList)
- customLeakyLinkedList = CustomLinkedList(customLeakyLinkedList)
- }.growingObjects
-
- assertThat(growingNodes).hasSize(1)
-
- val growingNode = growingNodes.first()
- assertThat(growingNode.children.size).isEqualTo(1)
- assertThat(growingNode.childrenObjectCountIncrease).isEqualTo(4)
- }
-
- @Test
- fun `growth along shared sub paths reported as single growth of shortest path`() {
- val detector = simpleDetector().fromScenario()
-
- val growingNodes = detector.findRepeatedlyGrowingObjects {
- multiLeakies += MultiLeaky()
- }.growingObjects
-
- assertThat(growingNodes).hasSize(1)
-
- val growingNode = growingNodes.first()
- assertThat(growingNode.name).contains("ArrayList")
- }
-
- @Test
- fun `OpenJdk HashMap virtualized as array`() {
- val detector = openJdkDetector().fromScenario(maxHeapDumps = 5)
-
- var index = 0
- val growingNodes = detector.findRepeatedlyGrowingObjects {
- leakyHashMap["key${++index}"] = Leaky()
- }.growingObjects
-
- val growingNode = growingNodes.first()
- assertThat(growingNode.name).contains("leakyHashMap")
- }
-
- @Test
- fun `OpenJdk ArrayList virtualized as array`() {
- val detector = openJdkDetector().fromScenario()
-
- val growingNodes = detector.findRepeatedlyGrowingObjects {
- leakies += Leaky()
- }.growingObjects
-
- val growingNode = growingNodes.first()
- assertThat(growingNode.name).contains("leakies")
- }
-
- private fun ObjectGrowthDetector.fromScenario(
- scenarioLoopsPerDump: Int = 1,
- maxHeapDumps: Int = 5
- ): RepeatingScenarioObjectGrowthDetector {
- return repeatingScenario(
- heapGraphProvider = ::dumpHeapGraph,
- maxHeapDumps = maxHeapDumps,
- scenarioLoopsPerDump = scenarioLoopsPerDump
- )
- }
-
- private fun simpleDetector(): ObjectGrowthDetector {
- val referenceMatchers = JdkReferenceMatchers.defaults + HeapTraversalOutput.ignoredReferences
- val referenceReaderFactory = ActualMatchingReferenceReaderFactory(referenceMatchers)
- val gcRootProvider = MatchingGcRootProvider(referenceMatchers)
- return ObjectGrowthDetector(gcRootProvider, referenceReaderFactory)
- }
-
- private fun openJdkDetector(): ObjectGrowthDetector {
- val referenceMatchers = JdkReferenceMatchers.defaults + HeapTraversalOutput.ignoredReferences
-
- val referenceReaderFactory = VirtualizingMatchingReferenceReaderFactory(
- referenceMatchers = referenceMatchers,
- virtualRefReadersFactory = { graph ->
- listOf(
- JavaLocalReferenceReader(graph, referenceMatchers),
- ) +
- OpenJdkInstanceRefReaders.values().mapNotNull { it.create(graph) }
- }
- )
-
- val gcRootProvider = MatchingGcRootProvider(referenceMatchers)
- return ObjectGrowthDetector(gcRootProvider, referenceReaderFactory)
- }
-
- private fun dumpHeapGraph(): CloseableHeapGraph {
- val hprofFolder = testFolder.newFolder()
- val hprofFile = File(hprofFolder, "${System.nanoTime()}.hprof")
- JvmTestHeapDumper.dumpHeap(hprofFile.absolutePath)
- return hprofFile.openHeapGraph()
- }
-}
diff --git a/shark/shark/src/test/java/shark/ObjectGrowthDetectorTest.kt b/shark/shark/src/test/java/shark/ObjectGrowthDetectorTest.kt
index 6623b87..8976ab9 100644
--- a/shark/shark/src/test/java/shark/ObjectGrowthDetectorTest.kt
+++ b/shark/shark/src/test/java/shark/ObjectGrowthDetectorTest.kt
@@ -3,25 +3,26 @@
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import shark.HprofHeapGraph.Companion.openHeapGraph
+import shark.ValueHolder.Companion.NULL_REFERENCE
class ObjectGrowthDetectorTest {
@Test
- fun `first traversal returns InitialHeapTraversal`() {
- val detector = newSimpleDetector()
+ fun `first traversal returns FirstHeapTraversal`() {
+ val detector = ObjectGrowthDetector.forJvmHeap()
- val heapTraversal = detector.findGrowingObjects(
+ val firstTraversal = detector.findGrowingObjects(
heapGraph = dump {
},
previousTraversal = InitialState(scenarioLoopsPerGraph = 1),
)
- assertThat(heapTraversal).isInstanceOf(FirstHeapTraversal::class.java)
+ assertThat(firstTraversal).isInstanceOf(FirstHeapTraversal::class.java)
}
@Test
fun `second traversal returns HeapTraversalWithDiff`() {
- val detector = newSimpleDetector()
+ val detector = ObjectGrowthDetector.forJvmHeap()
val first = detector.findGrowingObjects(
heapGraph = emptyHeapDump(),
previousTraversal = InitialState(scenarioLoopsPerGraph = 1),
@@ -37,7 +38,7 @@
@Test
fun `detect no growth on identical heaps`() {
- val detector = newSimpleDetector()
+ val detector = ObjectGrowthDetector.forJvmHeap().listRepeatingHeapGraph()
val dumps = listOf(
dump {
classWithStringsInStaticField("Hi")
@@ -47,15 +48,14 @@
}
)
- val growingNodes = detector.detectHeapGrowth(dumps)
+ val growingObjects = detector.findRepeatedlyGrowingObjects(dumps).growingObjects
- assertThat(growingNodes).isEmpty()
+ assertThat(growingObjects).isEmpty()
}
@Test
fun `detect no growth on structurally identical heap`() {
- val detector = newSimpleDetector()
-
+ val detector = ObjectGrowthDetector.forJvmHeap().listRepeatingHeapGraph()
val dumps = listOf(
dump {
classWithStringsInStaticField("Hi")
@@ -65,14 +65,14 @@
}
)
- val growingNodes = detector.detectHeapGrowth(dumps)
+ val growingObjects = detector.findRepeatedlyGrowingObjects(dumps).growingObjects
- assertThat(growingNodes).isEmpty()
+ assertThat(growingObjects).isEmpty()
}
@Test
fun `detect static field growth`() {
- val detector = newSimpleDetector()
+ val detector = ObjectGrowthDetector.forJvmHeap().listRepeatingHeapGraph()
val dumps = listOf(
dump {
classWithStringsInStaticField("Hello")
@@ -82,14 +82,135 @@
}
)
- val growingNodes = detector.detectHeapGrowth(dumps)
+ val growingObjects = detector.findRepeatedlyGrowingObjects(dumps).growingObjects
- assertThat(growingNodes).hasSize(1)
+ assertThat(growingObjects).hasSize(1)
+ }
+
+ @Test
+ fun `object growth computes retained size increase with 2 iterations`() {
+ val detector = ObjectGrowthDetector.forJvmHeap().listRepeatingHeapGraph()
+ val dumps = listOf(
+ dump {
+ classWithStringsInStaticField("Hello")
+ },
+ dump {
+ classWithStringsInStaticField("Hello", "World!")
+ }
+ )
+
+ val heapTraversal = detector.findRepeatedlyGrowingObjects(dumps)
+
+ val growingObject = heapTraversal.growingObjects.single()
+ assertThat(growingObject.retainedIncrease.objectCount).isEqualTo(1)
+ val expectedRetainedSizeIncrease = (12 + "World!".length * 2).bytes
+ assertThat(growingObject.retainedIncrease.heapSize).isEqualTo(expectedRetainedSizeIncrease)
+ }
+
+ @Test
+ fun `object growth computes retained size increase with 3 iterations`() {
+ val detector = ObjectGrowthDetector.forJvmHeap().listRepeatingHeapGraph()
+ val dumps = listOf(
+ dump {
+ classWithStringsInStaticField("Hello")
+ },
+ dump {
+ classWithStringsInStaticField("Hello", "World!")
+ },
+ dump {
+ classWithStringsInStaticField("Hello", "World!", "Turtles")
+ }
+ )
+
+ val heapTraversal = detector.findRepeatedlyGrowingObjects(dumps)
+
+ val growingObject = heapTraversal.growingObjects.single()
+ assertThat(growingObject.retainedIncrease.objectCount).isEqualTo(1)
+ val expectedRetainedSizeIncrease = (12 + "Turtles".length * 2).bytes
+ assertThat(growingObject.retainedIncrease.heapSize).isEqualTo(expectedRetainedSizeIncrease)
+ }
+
+
+ @Test
+ fun `detect growth of custom linked list`() {
+ val detector = ObjectGrowthDetector.forJvmHeap().listRepeatingHeapGraph()
+ val dumps = listOf(
+ dump {
+ val customLinkedListClass = clazz(
+ className = "CustomLinkedList",
+ fields = listOf("next" to ValueHolder.ReferenceHolder::class),
+ )
+ val linkedListTail = instance(customLinkedListClass, listOf(nullReference()))
+ val linkedListHead = instance(customLinkedListClass, listOf(linkedListTail))
+ clazz(
+ className = "ListHolder",
+ staticFields = listOf("staticList" to linkedListHead)
+ )
+ },
+ dump {
+ val customLinkedListClass = clazz(
+ className = "CustomLinkedList",
+ fields = listOf("next" to ValueHolder.ReferenceHolder::class),
+ )
+ val linkedListTail = instance(customLinkedListClass, listOf(nullReference()))
+ val linkedListMiddle = instance(customLinkedListClass, listOf(linkedListTail))
+ val linkedListHead = instance(customLinkedListClass, listOf(linkedListMiddle))
+ clazz(
+ className = "ListHolder",
+ staticFields = listOf("staticList" to linkedListHead)
+ )
+ }
+ )
+
+ val heapTraversal = detector.findRepeatedlyGrowingObjects(dumps)
+
+ assertThat(heapTraversal.growingObjects).hasSize(1)
+ }
+
+ @Test
+ fun `custom leaky linked list reports descendant to root as flattened collection`() {
+ val detector = ObjectGrowthDetector.forJvmHeap().listRepeatingHeapGraph()
+ val dumps = listOf(
+ dump {
+ val customLinkedListClass = clazz(
+ className = "CustomLinkedList",
+ fields = listOf("next" to ValueHolder.ReferenceHolder::class),
+ )
+ val linkedListTail = instance(customLinkedListClass, listOf(nullReference()))
+ val linkedListHead = instance(customLinkedListClass, listOf(linkedListTail))
+ clazz(
+ className = "ListHolder",
+ staticFields = listOf("staticList" to linkedListHead)
+ )
+ },
+ dump {
+ val customLinkedListClass = clazz(
+ className = "CustomLinkedList",
+ fields = listOf("next" to ValueHolder.ReferenceHolder::class),
+ )
+ val linkedListTail = instance(customLinkedListClass, listOf(nullReference()))
+ val linkedListMiddle1 = instance(customLinkedListClass, listOf(linkedListTail))
+ val linkedListMiddle2 = instance(customLinkedListClass, listOf(linkedListMiddle1))
+ val linkedListMiddle3 = instance(customLinkedListClass, listOf(linkedListMiddle2))
+ val linkedListMiddle4 = instance(customLinkedListClass, listOf(linkedListMiddle3))
+ val linkedListHead = instance(customLinkedListClass, listOf(linkedListMiddle4))
+ clazz(
+ className = "ListHolder",
+ staticFields = listOf("staticList" to linkedListHead)
+ )
+ }
+ )
+
+ val heapTraversal = detector.findRepeatedlyGrowingObjects(dumps)
+
+ val growingObject = heapTraversal.growingObjects.single()
+ val growingChild = growingObject.growingChildren.single()
+ assertThat(growingChild.objectCountIncrease).isEqualTo(4)
}
@Test
fun `detect no growth if more loops than node increase`() {
- val detector = newSimpleDetector()
+ val detector = ObjectGrowthDetector.forJvmHeap().listRepeatingHeapGraph()
val dumps = listOf(
dump {
classWithStringsInStaticField("Hello")
@@ -99,14 +220,17 @@
}
)
- val growingNodes = detector.detectHeapGrowth(dumps, 2)
+ val growingObjects = detector.findRepeatedlyGrowingObjects(
+ heapGraphs = dumps,
+ scenarioLoopsPerGraph = 2
+ ).growingObjects
- assertThat(growingNodes).isEmpty()
+ assertThat(growingObjects).isEmpty()
}
@Test
fun `detect static field growth counts`() {
- val detector = newSimpleDetector()
+ val detector = ObjectGrowthDetector.forJvmHeap().listRepeatingHeapGraph()
val heapDumpCount = 3
val scenarioLoopCount = 7
@@ -119,29 +243,262 @@
}
}
- val growingNodes = detector.detectHeapGrowth(dumps)
+ val growingObjects = detector.findRepeatedlyGrowingObjects(
+ heapGraphs = dumps,
+ scenarioLoopsPerGraph = scenarioLoopCount
+ ).growingObjects
- val growingNode = growingNodes.first()
+ val growingNode = growingObjects.first()
assertThat(growingNode.selfObjectCount).isEqualTo(1)
- assertThat(growingNode.childrenObjectCount).isEqualTo(heapDumpCount * scenarioLoopCount)
- assertThat(growingNode.childrenObjectCountIncrease).isEqualTo(scenarioLoopCount)
+ assertThat(growingNode.children.sumOf { it.selfObjectCount }).isEqualTo(
+ heapDumpCount * scenarioLoopCount
+ )
+ val growingChildren = growingNode.growingChildren
+ assertThat(growingChildren).hasSize(1)
+ assertThat(growingChildren.first().objectCountIncrease).isEqualTo(scenarioLoopCount)
assertThat(growingNode.children).hasSize(1)
}
- private fun ObjectGrowthDetector.detectHeapGrowth(
- heapDumps: List<CloseableHeapGraph>,
- scenarioLoopsPerGraph: Int = 1
- ): GrowingObjectNodes {
- return repeatingHeapGraph().findRepeatedlyGrowingObjects(
- initialState = InitialState(
- scenarioLoopsPerGraph = scenarioLoopsPerGraph,
- heapGraphCount = heapDumps.size
- ),
- heapGraphSequence = heapDumps.asSequence()
+ @Test
+ fun `no heap growth when node with no children grows`() {
+ val detector = ObjectGrowthDetector.forJvmHeap().listRepeatingHeapGraph()
+
+ val dumps = listOf(
+ dump {
+ clazz("SomeClass")
+ clazz(
+ "ClassWithStatics",
+ staticFields = listOf(
+ "strings" to objectArray(),
+ )
+ )
+ },
+ dump {
+ clazz("SomeClass")
+ clazz(
+ "ClassWithStatics",
+ staticFields = listOf(
+ "strings" to objectArray(
+ string("Hello 1"),
+ string("Hello 2")
+ ),
+ )
+ )
+ },
+ )
+ val growingObjects = detector.findRepeatedlyGrowingObjects(
+ heapGraphs = dumps,
+ scenarioLoopsPerGraph = 2
).growingObjects
+ assertThat(growingObjects).isEmpty()
}
+ @Test
+ fun `detect heap growth when node with existing children grows`() {
+ val detector = ObjectGrowthDetector.forJvmHeap().listRepeatingHeapGraph()
+
+ val dumps = listOf(
+ dump {
+ clazz("SomeClass")
+ clazz(
+ "ClassWithStatics",
+ staticFields = listOf(
+ "strings" to objectArray(
+ string("Hello 1"),
+ string("Hello 2")
+ ),
+ )
+ )
+ },
+ dump {
+ clazz("SomeClass")
+ clazz(
+ "ClassWithStatics",
+ staticFields = listOf(
+ "strings" to objectArray(
+ string("Hello 1"),
+ string("Hello 2"),
+ string("Hello 3"),
+ string("Hello 4"),
+ ),
+ )
+ )
+ },
+ )
+ val growingObjects = detector.findRepeatedlyGrowingObjects(
+ heapGraphs = dumps,
+ scenarioLoopsPerGraph = 2
+ ).growingObjects
+ assertThat(growingObjects).hasSize(1)
+ }
+
+ @Test
+ fun `detect no growth if sum of children over threshold but individual children under threshold`() {
+ val detector = ObjectGrowthDetector.forJvmHeap().listRepeatingHeapGraph()
+
+ val dumps = listOf(
+ dump {
+ clazz(
+ "ClassWithStatics",
+ staticFields = listOf("strings1" to objectArray(string("Hello")))
+ )
+ },
+ dump {
+ clazz(
+ "ClassWithStatics",
+ staticFields = listOf(
+ "strings1" to objectArray(string("Hello")),
+ "strings2" to objectArray(string("World")),
+ "strings3" to objectArray(string("!")),
+ )
+ )
+ }
+ )
+ val growingObjects = detector.findRepeatedlyGrowingObjects(
+ heapGraphs = dumps,
+ scenarioLoopsPerGraph = 2
+ ).growingObjects
+ assertThat(growingObjects).isEmpty()
+ }
+
+ @Test
+ fun `detect no growth if different individual children over threshold`() {
+ val detector = ObjectGrowthDetector.forJvmHeap().listRepeatingHeapGraph()
+ val dumps = listOf(
+ dump {
+ val otherType = clazz("SomeClass")
+ clazz("SomeClass")
+ clazz(
+ "ClassWithStatics",
+ staticFields = listOf(
+ "list" to objectArray(
+ string("Hello 1"),
+ string("Hello 2"),
+ instance(otherType),
+ instance(otherType),
+ ),
+ )
+ )
+ },
+ dump {
+ val otherType = clazz("SomeClass")
+ clazz("SomeClass")
+ clazz(
+ "ClassWithStatics",
+ staticFields = listOf(
+ "list" to objectArray(
+ string("Hello 1"),
+ string("Hello 2"),
+ string("Hello 3"),
+ string("Hello 4"),
+ instance(otherType),
+ instance(otherType),
+ ),
+ )
+ )
+ },
+ dump {
+ val otherType = clazz("SomeClass")
+ clazz("SomeClass")
+ clazz(
+ "ClassWithStatics",
+ staticFields = listOf(
+ "list" to objectArray(
+ string("Hello 1"),
+ string("Hello 2"),
+ string("Hello 3"),
+ string("Hello 4"),
+ instance(otherType),
+ instance(otherType),
+ instance(otherType),
+ instance(otherType),
+ ),
+ )
+ )
+ },
+ )
+
+ val heapGrowthTraversal = detector.findRepeatedlyGrowingObjects(
+ heapGraphs = dumps,
+ scenarioLoopsPerGraph = 2
+ )
+ assertThat(heapGrowthTraversal.traversalCount).isEqualTo(dumps.size)
+ val growingObjects = heapGrowthTraversal.growingObjects
+ assertThat(growingObjects).isEmpty()
+ }
+
+ @Test
+ fun `growth along shared sub paths reported as single growth of shortest path`() {
+ val detector = ObjectGrowthDetector.forJvmHeap().listRepeatingHeapGraph()
+ val dumps = listOf(
+ dump {
+ 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 pair = instance(pairClass, listOf(instance(objectClassId), instance(objectClassId)))
+ clazz(
+ "ClassWithStatics",
+ staticFields = listOf(
+ "list" to objectArray(
+ instance(growingClass, listOf(pair)),
+ ),
+ )
+ )
+ },
+ dump {
+ 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 pair1 = instance(pairClass, listOf(instance(objectClassId), instance(objectClassId)))
+ val pair2 = instance(pairClass, listOf(instance(objectClassId), instance(objectClassId)))
+ clazz(
+ "ClassWithStatics",
+ staticFields = listOf(
+ "list" to objectArray(
+ instance(growingClass, listOf(pair1)),
+ instance(growingClass, listOf(pair2)),
+ ),
+ )
+ )
+ },
+ )
+
+ val heapGrowthTraversal = detector.findRepeatedlyGrowingObjects(dumps)
+
+ val growingObject = heapGrowthTraversal.growingObjects.single()
+ assertThat(growingObject.name).startsWith("STATIC_FIELD ClassWithStatics.list")
+ }
+
+ class ListRepeatingHeapGraphObjectGrowthDetector(
+ objectGrowthDetector: ObjectGrowthDetector
+ ) {
+ private val delegate = objectGrowthDetector.repeatingHeapGraph()
+
+ fun findRepeatedlyGrowingObjects(
+ heapGraphs: List<CloseableHeapGraph>,
+ scenarioLoopsPerGraph: Int = InitialState.DEFAULT_SCENARIO_LOOPS_PER_GRAPH,
+ ): HeapGrowthTraversal {
+ return delegate.findRepeatedlyGrowingObjects(
+ initialState = InitialState(
+ scenarioLoopsPerGraph = scenarioLoopsPerGraph,
+ ),
+ heapGraphSequence = heapGraphs.asSequence()
+ ).apply {
+ check(traversalCount == heapGraphs.size) {
+ "Expected traversalCount $traversalCount to be equal to heapGraphs size ${heapGraphs.size} for $this"
+ }
+ }
+ }
+ }
+
+ private fun ObjectGrowthDetector.listRepeatingHeapGraph(): ListRepeatingHeapGraphObjectGrowthDetector =
+ ListRepeatingHeapGraphObjectGrowthDetector(this)
+
private fun HprofWriterHelper.classWithStringsInStaticField(vararg strings: String) {
clazz(
"ClassWithStatics",
@@ -157,19 +514,5 @@
return dump(HprofHeader(), block).openHeapGraph()
}
- private fun newSimpleDetector(): ObjectGrowthDetector {
- val referenceReaderFactory = ActualMatchingReferenceReaderFactory(
- referenceMatchers = emptyList()
- )
- val gcRootProvider = GcRootProvider { graph ->
- graph.gcRoots.asSequence().map {
- GcRootReference(
- gcRoot = it,
- isLowPriority = false,
- matchedLibraryLeak = null
- )
- }
- }
- return ObjectGrowthDetector(gcRootProvider, referenceReaderFactory)
- }
+ private fun nullReference() = ValueHolder.ReferenceHolder(NULL_REFERENCE)
}
diff --git a/shark/shark/src/test/java/shark/internal/DominatorTreeTest.kt b/shark/shark/src/test/java/shark/internal/DominatorTreeTest.kt
index 7b2d42e..b7771a5 100644
--- a/shark/shark/src/test/java/shark/internal/DominatorTreeTest.kt
+++ b/shark/shark/src/test/java/shark/internal/DominatorTreeTest.kt
@@ -1,5 +1,6 @@
package shark.internal
+import androidx.collection.mutableLongSetOf
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import shark.DominatorTree
@@ -39,18 +40,21 @@
val root = newObjectId().apply { tree.updateDominatedAsRoot(this) }
val child = newObjectId().apply { tree.updateDominated(this, root) }
- val sizes = tree.computeRetainedSizes(setOf(child), `10 bytes per object`)
+ val sizes = tree.computeRetainedSizes(mutableLongSetOf(child), `10 bytes per object`)
- assertThat(sizes).containsOnlyKeys(child)
+ val keys = mutableSetOf<Long>()
+ sizes.forEachKey { keys += it }
+
+ assertThat(keys).containsOnly(child)
}
@Test fun `single root has self size as retained size`() {
val tree = DominatorTree()
val root = newObjectId().apply { tree.updateDominatedAsRoot(this) }
- val sizes = tree.computeRetainedSizes(setOf(root), `10 bytes per object`)
+ val sizes = tree.computeRetainedSizes(mutableLongSetOf(root), `10 bytes per object`)
- assertThat(sizes[root]).isEqualTo(10 to 1)
+ assertThat(sizes[root]).isEqualTo(10 packedWith 1)
}
@Test fun `size of dominator includes dominated`() {
@@ -58,9 +62,9 @@
val root = newObjectId().apply { tree.updateDominatedAsRoot(this) }
tree.updateDominated(newObjectId(), root)
- val sizes = tree.computeRetainedSizes(setOf(root), `10 bytes per object`)
+ val sizes = tree.computeRetainedSizes(mutableLongSetOf(root), `10 bytes per object`)
- assertThat(sizes[root]).isEqualTo(20 to 2)
+ assertThat(sizes[root]).isEqualTo(20 packedWith 2)
}
@Test fun `size of chain of dominators is additive`() {
@@ -69,10 +73,10 @@
val child = newObjectId().apply { tree.updateDominated(this, root) }
tree.updateDominated(newObjectId(), child)
- val sizes = tree.computeRetainedSizes(setOf(root, child), `10 bytes per object`)
+ val sizes = tree.computeRetainedSizes(mutableLongSetOf(root, child), `10 bytes per object`)
- assertThat(sizes[root]).isEqualTo(30 to 3)
- assertThat(sizes[child]).isEqualTo(20 to 2)
+ assertThat(sizes[root]).isEqualTo(30 packedWith 3)
+ assertThat(sizes[child]).isEqualTo(20 packedWith 2)
}
@Test fun `diamond dominators don't dominate`() {
@@ -84,11 +88,11 @@
tree.updateDominated(grandChild, child1)
tree.updateDominated(grandChild, child2)
- val sizes = tree.computeRetainedSizes(setOf(root, child1, child2), `10 bytes per object`)
+ val sizes = tree.computeRetainedSizes(mutableLongSetOf(root, child1, child2), `10 bytes per object`)
- assertThat(sizes[child1]).isEqualTo(10 to 1)
- assertThat(sizes[child2]).isEqualTo(10 to 1)
- assertThat(sizes[root]).isEqualTo(40 to 4)
+ assertThat(sizes[child1]).isEqualTo(10 packedWith 1)
+ assertThat(sizes[child2]).isEqualTo(10 packedWith 1)
+ assertThat(sizes[root]).isEqualTo(40 packedWith 4)
}
@Test fun `two dominators dominated by common ancestor`() {
@@ -100,11 +104,11 @@
tree.updateDominated(grandChild, child1)
tree.updateDominated(grandChild, child2)
- val sizes = tree.computeRetainedSizes(setOf(root, child1, child2), `10 bytes per object`)
+ val sizes = tree.computeRetainedSizes(mutableLongSetOf(root, child1, child2), `10 bytes per object`)
- assertThat(sizes[child1]).isEqualTo(10 to 1)
- assertThat(sizes[child2]).isEqualTo(10 to 1)
- assertThat(sizes[root]).isEqualTo(40 to 4)
+ assertThat(sizes[child1]).isEqualTo(10 packedWith 1)
+ assertThat(sizes[child2]).isEqualTo(10 packedWith 1)
+ assertThat(sizes[root]).isEqualTo(40 packedWith 4)
}
@Test fun `two dominators dominated by lowest common ancestor`() {
@@ -118,12 +122,12 @@
tree.updateDominated(grandGrandChild, grandChild2)
val sizes =
- tree.computeRetainedSizes(setOf(root, child, grandChild1, grandChild2), `10 bytes per object`)
+ tree.computeRetainedSizes(mutableLongSetOf(root, child, grandChild1, grandChild2), `10 bytes per object`)
- assertThat(sizes[grandChild1]).isEqualTo(10 to 1)
- assertThat(sizes[grandChild1]).isEqualTo(10 to 1)
- assertThat(sizes[child]).isEqualTo(40 to 4)
- assertThat(sizes[root]).isEqualTo(50 to 5)
+ assertThat(sizes[grandChild1]).isEqualTo(10 packedWith 1)
+ assertThat(sizes[grandChild2]).isEqualTo(10 packedWith 1)
+ assertThat(sizes[child]).isEqualTo(40 packedWith 4)
+ assertThat(sizes[root]).isEqualTo(50 packedWith 5)
}
@Test fun `two separate trees do not share size`() {
@@ -138,10 +142,10 @@
}
val sizes =
- tree.computeRetainedSizes(setOf(root1, root2), `10 bytes per object`)
+ tree.computeRetainedSizes(mutableLongSetOf(root1, root2), `10 bytes per object`)
- assertThat(sizes[root1]).isEqualTo(110 to 11)
- assertThat(sizes[root2]).isEqualTo(110 to 11)
+ assertThat(sizes[root1]).isEqualTo(110 packedWith 11)
+ assertThat(sizes[root2]).isEqualTo(110 packedWith 11)
}
@Test fun `no common descendant does not include size`() {
@@ -155,10 +159,10 @@
tree.updateDominated(descendant, root2)
val sizes =
- tree.computeRetainedSizes(setOf(root1, root2), `10 bytes per object`)
+ tree.computeRetainedSizes(mutableLongSetOf(root1, root2), `10 bytes per object`)
- assertThat(sizes[root1]).isEqualTo(100 to 10)
- assertThat(sizes[root2]).isEqualTo(10 to 1)
+ assertThat(sizes[root1]).isEqualTo(100 packedWith 10)
+ assertThat(sizes[root2]).isEqualTo(10 packedWith 1)
}
@Test fun `only compute retained size for retained objects`() {
@@ -171,7 +175,7 @@
tree.updateDominated(grandGrandChild, root2)
val objectsWithComputedSize = mutableSetOf<Long>()
- tree.computeRetainedSizes(setOf(child)) { objectId ->
+ tree.computeRetainedSizes(mutableLongSetOf(child)) { objectId ->
objectsWithComputedSize += objectId
1
}